مطالب
سازگارسازی کلاس‌های اعتبارسنجی Twitter Bootstrap 3 با فرم‌های ASP.NET MVC
چندی پیش در همین وب‌سایت مطلبی تحت عنوان «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» منتشر شد. این مقاله مرتبط با نسخه دوم فریم‌ورک محبوب Bootstrap بود. قصد داریم به بازنویسی کدهای مرتبط بپردازیم و کلاس‌های مرتبط با نسخه سوم این فریم‌ورک را هم با فرم‌های خودمان سازگار کنیم. مثل مقاله‌ی ذکر شده توضیحات را با یک مثال همراه می‌کنم.

مدل برنامه
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace FormValidationWithBootstrap.Models
{
    [Table("Product")]
    public class ProductModel
    {
        [Key]
        public int Id { get; set; }
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")]
        [Display(Name = "نام کالا")]
        public string Name { get; set; }
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [Display(Name = "قیمت")]
        [DataType(DataType.Currency)]
        public double Price { get; set; }
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [Display(Name = "موجودی")]
        public int Qty { get; set; }
    }
}
قرار هست که جدولی داشته باشیم با نام Product برای ثبت محصولات. مدل برنامه شامل خاصیت‌های مرتبط و همچنین اعتبارسنجی‌های مد نظر ما هست.

کنترلر برنامه
using System.Web.Mvc;
using FormValidationWithBootstrap.Models;

namespace FormValidationWithBootstrap.Controllers
{
    public class ProductController : Controller
    {
        // GET: Product
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult New()
        {
            return View();
        }

        [HttpPost]
        public ActionResult New(ProductModel product)
        {
            if (!ModelState.IsValid)
                return View(product);

            if (product.Name != "پفک")
            {
                ModelState.AddModelError("", "لطفا مشکلات را برطرف کنید!");
                ModelState.AddModelError("Name", "فقط محصولی با نام پفک قابل ثبت است :)");
                return View(product);
            }
            // todo:save...
            return RedirectToAction("Index");
        }
    }
}
در قسمت کنترلر نیز اتفاق خاصی نیفتاده و کارهای پایه فقط انجام شده؛ ضمن اینکه آمدیم برای داشتن خطاهای سفارشی نام محصول را چک کردیم و گفتیم اگر نام محصول چیزی غیر از «پفک» بود، از سمت سرور خطایی را صادر کند و بگوید که فقط پفک قابل ثبت هست.

View برنامه
@model FormValidationWithBootstrap.Models.ProductModel
@{
    ViewBag.Title = "New";
}
<h2>کالای جدید</h2>
@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    <div>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "alert alert-danger" })
        <div>
            @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" })
            <div>
                @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" })
            </div>
        </div>
        <div>
            @Html.LabelFor(model => model.Price, htmlAttributes: new { @class = "control-label col-md-2" })
            <div>
                @Html.EditorFor(model => model.Price, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Price, "", new { @class = "text-danger" })
            </div>
        </div>
        <div>
            @Html.LabelFor(model => model.Qty, htmlAttributes: new { @class = "control-label col-md-2" })
            <div>
                @Html.EditorFor(model => model.Qty, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Qty, "", new { @class = "text-danger" })
            </div>
        </div>
        <div>
            <div>
                <input type="submit" value="ثبت" />
                <input type="reset" value="ریست" />
                @Html.ActionLink("بازگشت به لیست", "Index", "Product", null, new {@class="btn btn-default"})
            </div>
        </div>
    </div>
}
فایل View برنامه با Scafflod Templateها ساخته شده و چون از Visual Studio 2013 استفاده شده، به‌صورت پیش‌فرض با بوت‌استرپ سازگار هست. تغییری که ایجاد شده تعویض کلاس مربوط به ValidationSummary هست که به alert alert-danger تغییر پیدا کرده و همچین دو دکمه «ریست» و «بازگشت به لیست» هم به کنار دکمه «ثبت» اضافه شده.

در فرم بالا شاهد هستیم که با کلیک بر روی دکمه ثبت تنها خطاهای مرتبط با هر ردیف ظاهر شده‌اند و هیچ تغییر رنگی که حاصل از کلاس‌های مرتبط با Bootstrap باشند حاصل نشده. برای رفع این مشکل کافی‌‌است اسکریپت زیر، به انتهای فایل View برنامه اضافه شود تا پیش‌فرض‌های jQuery Validator را تغییر دهیم و آن‌ها را با بوت‌استرپ سازگار کنیم. همچنین در حالت ارسال فرم به سرور و Postback و نمایش خطاهای سفارشی، قسمت بررسی field-validation-error صورت می‌گیرد و در صورتیکه موردی را پیدا کند، به سطر مرتبط با آن کلاس has-error اضافه خواهد شد. 
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
    <script>
        // override jquery validate plugin defaults
        $.validator.setDefaults({
            highlight: function (element) {
                $(element).closest('.form-group').addClass('has-error');
            },
            unhighlight: function (element) {
                $(element).closest('.form-group').removeClass('has-error').addClass('has-success');
            },
            errorElement: 'span',
            errorClass: 'help-block',
            errorPlacement: function (error, element) {
                if (element.parent('.input-group').length) {
                    error.insertAfter(element.parent());
                } else {
                    error.insertAfter(element);
                }
            }
        });
        $(function () {
            $('form').each(function () {
                $(this).find('div.form-group').each(function () {
                    if ($(this).find('span.field-validation-error').length > 0) {
                        $(this).addClass('has-error');
                    }
                });
            });
        });
    </script>
}

با افزودن اسکریپت فوق، در حالت اعتبارسنجی فرم‌ها به شکل زیر می‌رسیم:

همچنین هنگامیکه کاربر فیلد را به درستی وارد کرد، رنگ فیلد و همچین آن ردیف به سبز تغییر خواهد کرد.

و همچنین در حالت رخ‌داد یک خطای سفارشی پس از postback از سمت سرور به حالت زیر خواهیم رسیذ.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید 

FormValidationWithBootstrap.rar 

مطالب
استفاده از Web API در ASP.NET Web Forms
گرچه ASP.NET Web API بهمراه ASP.NET MVC بسته بندی شده و استفاده می‌شود، اما اضافه کردن آن به اپلیکیشن‌های ASP.NET Web Forms کار ساده ای است. در این مقاله مراحل لازم را بررسی می‌کنیم.

برای استفاده از Web API در یک اپلیکیشن ASP.NET Web Forms دو قدم اصلی باید برداشته شود:

  • اضافه کردن یک کنترلر Web API که از کلاس ApiController مشتق می‌شود.
  • اضافه کردن مسیرهای جدید به متد Application_Start.


یک پروژه Web Forms بسازید

ویژوال استودیو را اجرا کنید و پروژه جدیدی از نوع ASP.NET Web Forms Application ایجاد کنید.


کنترلر و مدل اپلیکیشن را ایجاد کنید

کلاس جدیدی با نام Product بسازید و خواص زیر را به آن اضافه کنید.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
}
همانطور که مشاهده می‌کنید مدل مثال جاری نمایانگر یک محصول است. حال یک کنترلر Web API به پروژه اضافه کنید. کنترلر‌های Web API درخواست‌های HTTP را به اکشن متدها نگاشت می‌کنند. در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Add, New Item را انتخاب کنید.

در دیالوگ باز شده گزینه Web را از پانل سمت چپ کلیک کنید و نوع آیتم جدید را Web API Controller Class انتخاب نمایید. نام این کنترلر را به "ProductsController" تغییر دهید و OK کنید.

کنترلر ایجاد شده شامل یک سری متد است که بصورت خودکار برای شما اضافه شده اند، آنها را حذف کنید و کد زیر را به کنترلر خود اضافه کنید.

namespace WebForms
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;

    public class ProductsController : ApiController
    {

        Product[] products = new Product[] 
        { 
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, 
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products;
        }

        public Product GetProductById(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return product;
        }

        public IEnumerable<Product> GetProductsByCategory(string category)
        {
            return products.Where(
                (p) => string.Equals(p.Category, category,
                    StringComparison.OrdinalIgnoreCase));
        }
    }
}
کنترلر جاری لیستی از محصولات را بصورت استاتیک در حافظه محلی نگهداری می‌کند. متدهایی هم برای دریافت لیست محصولات تعریف شده اند.


اطلاعات مسیریابی را اضافه کنید

مرحله بعدی اضافه کردن اطلاعات مسیریابی (routing) است. در مثال جاری می‌خواهیم آدرس هایی مانند "api/products/" به کنترلر Web API نگاشت شوند. فایل Global.asax را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.

using System.Web.Http;
حال کد زیر را به متد Application_Start اضافه کنید.
RouteTable.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = System.Web.Http.RouteParameter.Optional }
    );

برای اطلاعات بیشتر درباره مسیریابی در Web API به این لینک مراجعه کنید.


دریافت اطلاعات بصورت آژاکسی در کلاینت

تا اینجا شما یک API دارید که کلاینت‌ها می‌توانند به آن دسترسی داشته باشند. حال یک صفحهHTML خواهیم ساخت که با استفاده از jQuery سرویس را فراخوانی می‌کند. صفحه Default.aspx را باز کنید و کدی که بصورت خودکار در قسمت Content تولید شده است را حذف کرده و کد زیر را به این قسمت اضافه کنید:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" 
    AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebForms._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>Products</h2>
    <table>
    <thead>
        <tr><th>Name</th><th>Price</th></tr>
    </thead>
    <tbody id="products">
    </tbody>
    </table>
</asp:Content>
حال در قسمت HeaderContent کتابخانه jQuery را ارجاع دهید.
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
</asp:Content>

همانطور که می‌بینید در مثال جاری از فایل محلی استفاده شده است اما در اپلیکیشن‌های واقعی بهتر است از CDN‌‌ها استفاده کنید.

نکته: برای ارجاع دادن اسکریپت‌ها می‌توانید بسادگی فایل مورد نظر را با drag & drop به کد خود اضافه کنید.

زیر تگ jQuery اسکریپت زیر را اضافه کنید.

<script type="text/javascript">
    function getProducts() {
        $.getJSON("api/products",
            function (data) {
                $('#products').empty(); // Clear the table body.

                // Loop through the list of products.
                $.each(data, function (key, val) {
                    // Add a table row for the product.
                    var row = '<td>' + val.Name + '</td><td>' + val.Price + '</td>';
                    $('<tr/>', { text: row })  // Append the name.
                        .appendTo($('#products'));
                });
            });
        }

        $(document).ready(getProducts);
</script>

هنگامی که سند جاری (document) بارگذاری شد این اسکریپت یک درخواست آژاکسی به آدرس "api/products/" ارسال می‌کند. سرویس ما لیستی از محصولات را با فرمت JSON بر می‌گرداند، سپس این اسکریپت لیست دریافت شده را به جدول HTML اضافه می‌کند.

اگر اپلیکیشن را اجرا کنید باید با نمایی مانند تصویر زیر مواجه شوید:

نظرات مطالب
بررسی متد های یک طرفه در WCF
زمانی که ارتباط بین سرور و کلاینت به هر دلیلی قطع شود یا به بنا به دلایلی طی انجام عملیات سمت سرور Exception رخ دهد، وضعیت ChannelFactory برای آن سرویس به حالت Faulted  تغییر پیدا می‌کند. در نتیجه دیگر امکان استفاده از این کانال ارتباطی میسر نیست و باید یک کانال ارتباطی جدید تهیه نمایید. اما برای اینکه بعد از متوقف سازی عملیات سمت سرور حافظه مصرفی دوباره بازگردانده شود باید از مفهوم UnitOfWork بهره جست البته با مقداری تغییرات برای همگام سازی با درخواست‌های WCF. روش مورد استفاده من به صورت زیر است(با فرض اینکه از EntityFramework به عنوان ORM استفاده میکنید):
» ابتدا یک Extension برای OperationContext تعریف می‌کنیم(با فرض اینکه IDatabaseContext نماینده کلاس DbContext پروژه است):
private class OperationContainerExtension : IExtension<OperationContext>
        {
            public OperationContainerExtension( IDatabaseContext dbContext, string contextKey )
            {
                this.CurrentDbContext = dbContext;
                this.ContextKey = contextKey;
            }

            public IDatabaseContext CurrentDbContext
            {
                get;
                private set;
            }

            public string ContextKey
            {
                get;
                private set;
            }

            public void Attach( OperationContext owner )
            {
            }

            public void Detach( OperationContext owner )
            {
            }
        }
بعد در هنگام نمونه سازی از UnitOfWork در لایه سرویس Extension بالا به OperationContext جاری اضافه خواهد شد(اگر از IOC Container خاصی استفاده می‌کنید باید کد عملیات وهله سازی کلاس UnitOfWork را با کد‌های زیر مزین کنید):
 if ( OperationContext.Current != null )
            {
                OperationContext.Current.Extensions.Add( new OperationContainerExtension( dbContext , CONTEXTKEY ) );
                OperationContext.Current.OperationCompleted += CurrentOperationContext_OperationCompleted;
                 OperationContext.Current.Channel.Faulted += Channel_Faulted; 
             }
می بینید که برای رویداد OperationCompleted و رویداد Fault در Channel نیز کد‌های Dispose کردن UnitOfWork و همچنین DbContext را قرار دادم. به صورت زیر:
void Channel_Faulted( object sender, EventArgs e )
        {
            IDatabaseContext dbContext = GetDbContext();
            if ( dbContext != null )
            {
                dbContext.Dispose();
                GC.Collect();
            }
        }

        private void CurrentOperationContext_OperationCompleted( object sender, EventArgs e )
        {
            IDatabaseContext dbContext = GetDbContext();
            if ( dbContext != null )
            {
                dbContext.Dispose();
                GC.Collect();
            }
        }
اگر به کد‌های بالا دقت کنید متد GetDbContext نوشته شده برای به دست آوردن DbContext جاری در Session مربوطه است. کد آن نیز به صورت زیر می‌باشد
protected override IDatabaseContext GetDbContext()
        {
            if ( OperationContext.Current != null )
            {
                var operationContainerExtension = OperationContext.Current.Extensions.OfType<OperationContainerExtension>().FirstOrDefault( e => e.ContextKey == CONTEXTKEY );
                if ( operationContainerExtension != null )
                {
                    return operationContainerExtension.CurrentDbContext;
                }
                return staticDbContext;
            }
            else
                return staticDbContext;
        }
نکته آخر هم این است که CONTEXTKEY صرفا یک فیلد از نوع string ولی با مقدار Guid برای به دست آوردن Extension مربوطه می‌باشد و تعریف آن در سازنده کلاس خواهد بود.
private string CONTEXTKEY = Guid.NewGuid().ToString();
در این صورت به طور قطع تمام منابع مورد استفاده سرویس‌های سمت سرور بعد از اتمام عملیات یا حتی وقوع هر خطا آزاد خواهند شد. اما اگر NHibernate را به عنوان ORM ترجیح می‌دهید به جای  IDatabaseContext باید از ISession استفاده نمایید.
مطالب
گوگل ریدر و افزودن توضیحات

اگر به گوگل ریدر دقت کرده باشید، دو گزینه‌ی به اشتراک گذاری دارد: share و share with note .


اگر گزینه‌ی share with note را انتخاب کرده و توضیحی را ارسال یا اضافه کنیم، این توضیحات، به فید از نوع Atom اشتراک‌ها هم اضافه می‌شود. مثلا:

<?xml version="1.0"?>
<feed xmlns:media="http://search.yahoo.com/mrss/"
xmlns:gr="http://www.google.com/schemas/reader/atom/"
xmlns:idx="urn:atom-extension:indexing"
xmlns="http://www.w3.org/2005/Atom"
idx:index="no"
gr:dir="ltr">

...

<entry gr:crawl-timestamp-msec="1316627782108">
...
<gr:annotation>
<content type="html">text-text-text</content>
<author>
<name>Vahid</name>
</author>
</gr:annotation>
...
</entry>

...

</feed>



این افزونه استاندارد نیست و همانطور که در قسمت xmlns:gr اطلاعات فوق مشخص است، در فضای نام http://www.google.com/schemas/reader/atom/ معنا پیدا می‌کند. از دات نت سه و نیم به بعد هم کلاسی جهت خواندن فیدهای استاندارد وجود دارد (تعریف شده در فضای نام System.ServiceModel.Syndication). اما چگونه می‌توان این افزونه‌ی غیر استاندارد را با کمک امکانات توکار دات نت خواند؟
روش کار با استفاده از ElementExtensions هر آیتم یک فید است؛ به صورت زیر :

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Xml;
using System.Xml.Linq;

namespace Linq2Rss
{
public class RssEntry
{
public string Title { set; get; }
public string Description { set; get; }
public string Link { set; get; }
public DateTime PublicationDate { set; get; }
public string Author { set; get; }
public string BlogName { set; get; }
public string BlogAddress { set; get; }
public string Annotation { set; get; }
}

public static class AtomReader
{
private static string getAtomAnnotation(this SyndicationElementExtensionCollection items)
{
if (!items.Any()) return string.Empty;
var item = items.Where(x => x.OuterName.ToLowerInvariant() == "annotation").FirstOrDefault();
if (item == null) return string.Empty;

var element = item.GetObject<XElement>();
var content = element.Element("{http://www.w3.org/2005/Atom}content");
return content == null ? string.Empty : content.Value;
}

public static IList<RssEntry> GetEntries(string feedUrl)
{
using (var reader = XmlReader.Create(feedUrl))
{
var feed = SyndicationFeed.Load(reader);
if (feed == null) return null;

return feed.Items.Select(x =>
new RssEntry
{
Title = x.Title.Text,
Author = x.Authors.Any() ? x.Authors.First().Name : string.Empty,
Description = x.Content == null ? string.Empty : ((TextSyndicationContent)x.Content).Text,
Link = x.Links.Any() ? x.Links.First().Uri.AbsoluteUri : string.Empty,
PublicationDate = x.PublishDate.UtcDateTime,
BlogName = x.SourceFeed.Title.Text,
BlogAddress = x.SourceFeed.Links.Any() ? x.SourceFeed.Links.First().Uri.AbsoluteUri : string.Empty,
Annotation = x.ElementExtensions.getAtomAnnotation()

}).ToList();
}
}
}
}

در این مثال به کمک متد الحاقی getAtomAnnotation، مجموعه‌ی SyndicationElementExtensionCollection هر آیتم یک فید بررسی شده، در بین این‌ها، موردی که از نوع annotation باشد انتخاب و سپس content آن استخراج می‌گردد.


نکته‌ای دیگر:
اکثر کلاس‌های موجود در فضاهای نام مرتبط با XML در دات نت امکان خواندن اطلاعات را از یک Uri هم دارند؛ مانند مثال فوق و متد XmlReader.Create بکارگرفته شده در آن. اما اگر بخواهیم حین خواندن اطلاعات، یک پروکسی را نیز به پروسه جاری اضافه کنیم، به نظر خاصیت یا متدی جهت انجام اینکار وجود ندارد. برای رفع این مشکل می‌توان یک پروکسی سراسری را تعریف کرد. تنها کافی است خاصیت System.Net.WebRequest.DefaultWebProxy مقدار دهی شود. پس از آن به صورت خودکار بر روی کل برنامه تاثیر خواهد گذاشت.


مطالب
کار با شیوه‌نامه‌های فرم‌ها در بوت استرپ 4
بوت استرپ، به همراه کلاس‌هایی است، برای نمایش زیباتر فرم‌ها، که شامل کلاس‌های اعتبارسنجی و حتی کنترل نحوه‌ی چیدمان و اندازه‌ی آن‌ها نیز می‌شود.


ایجاد فرم‌های مقدماتی، با بوت استرپ 4

بوت استرپ به همراه کلاس‌هایی مانند form-group و form-control است که از آن‌ها می‌توان برای ایجاد یک فرم مقدماتی استفاده کرد. در ابتدا مثال غیر تزئین شده‌ی زیر را در نظر بگیرید:
<body>
    <div class="container">
        <h2>Medical Questionnaire</h2>
        <form>
            <fieldset>
                <legend>Owner Info</legend>
                <div>
                    <label for="ownername">Owner name</label>
                    <input type="text" id="ownername" placeholder="Your Name">
                </div>
                <div>
                    <label for="owneremail">Email address</label>
                    <input type="email" id="owneremail" aria-describedby="emailHelp"
                        placeholder="Enter email">
                    <small id="emailHelp">We'll never share your email</small>
                </div>
            </fieldset>

            <fieldset>
                <legend>Pet Info</legend>
                <div>
                    <label for="petname">Pet name</label>
                    <input type="text" id="petname" placeholder="Your Pet's name">
                </div>
                <div>
                    <label for="pettype">Pet type</label>
                    <select id="pettype">
                        <option>Choose</option>
                        <option value="cat">Dog</option>
                        <option value="cat">Cat</option>
                        <option value="bird">Other</option>
                    </select>
                </div>
                <div>
                    <label for="reasonforvisit">Reason for today's visit</label>
                    <textarea id="reasonforvisit" rows="3"></textarea>
                </div>
                <div>
                    <label>Has your pet been spayed or neutered?</label>
                    <label><input type="radio" name="spayneut" value="yes"
                            checked> Yes</label>
                    <label><input type="radio" name="spayneut" value="no"> No</label>
                </div>
                <div>
                    <label>Has the pet had any of the following in the past 30
                        days</label>
                    <label><input type="checkbox"> Abdominal pain</label>
                    <label><input type="checkbox"> Lack of appetite</label>
                    <label><input type="checkbox"> Weakness</label>
                </div>
            </fieldset>
            <button type="submit">Submit</button>
        </form>

    </div><!-- content container -->
</body>
که چنین خروجی ابتدایی را نیز به همراه دارد:


در ادامه شروع می‌کنیم به تزئین کردن این فرم، با کلاس‌های بوت استرپ 4:
- ابتدا به fieldsetهای تعریف شده، کلاس form-goup را انتساب می‌دهیم. این مورد سبب می‌شود تا اندکی فاصله بین آن‌ها ایجاد شود.
- سپس به تمام divهایی که المان‌ها را در بر گرفته‌اند نیز کلاس form-group را اعمال می‌کنیم.
با اینکار فاصله‌ی مناسبی بین المان‌های تعریف شده‌ی در صفحه ایجاد می‌شود:


- در ادامه به تمام المان‌های input، select و textarea (منهای checkboxها) کلاس form-control را نسبت می‌دهیم:


با اینکار، ظاهر این المان‌ها بسیار شکیل‌تر شده‌است و همچنین این فرم واکنشگرا نیز می‌باشد.

- پس از آن، تمام المان‌های label را انتخاب کرده و کلاس form-control-label را به آن‌ها انتساب می‌دهیم. هرچند با اینکار ظاهر فعلی فرم تغییری نمی‌کند، اما چنین تعریفی برای فعالسازی کلاس‌های اعتبارسنجی ضروری است.
اگر به هر دلیلی نخواستید این برچسب‌ها را نمایش دهید، می‌توانید از کلاس sr-only استفاده کنید که صرفا سبب نمایش آن‌ها به screen readers می‌شود.
- ذیل فیلد ورود ایمیل، متنی وجود دارد. این متن را با کلاس‌های form-text text-muted مزین می‌کنیم:


- به دکمه‌ی پایین صفحه نیز کلاس‌های btn btn-primary را اضافه می‌کنیم که در مطلب «بررسی شیوه‌نامه‌های المان‌های پر کاربرد بوت استرپ 4» بیشتر به آن‌ها پرداختیم.


مزین سازی checkboxها و radio-buttonها در بوت استرپ 4

روش مزین سازی checkboxها و radio-buttonها در بوت استرپ، با سایر المان‌ها اندکی متفاوت است:
<div class="form-check">
    <label class="form-check-label">
        <input class="form-check-input" type="checkbox">
        Lack of appetite
    </label>
</div>
در اینجا تفاوتی نمی‌کند که بخواهیم با checkboxها کار کنیم و یا radio-buttonها، هر دوی این المان‌ها ابتدا داخل یک div با کلاس form-check قرار می‌گیرند. سپس برچسب آن‌ها دارای کلاس form-check-label می‌شود و در آخر به خود این المان‌های input، کلاس form-check-input اضافه خواهد شد.

یک نکته: اگر نیاز است این المان‌ها کنار یکدیگر نمایش داده شوند، می‌توان بر روی div آن‌ها از کلاس‌های form-check form-check-inline استفاده کرد. در این حالت اگر می‌خواهید برچسب برای مثال radio-button تعریف شده، در یک سطر و گزینه‌ها آن در سطری دیگر باشند، از کلاس d-block بر روی این برچسب استفاده کنید:
<div class="form-group">
    <label class="d-block">Has your pet been spayed or
        neutered?</label>
    <div class="form-check form-check-inline">
        <label class="form-check-label">
            <input class="form-check-input" type="radio" name="spayneut"
                   value="yes" checked>
            Yes
        </label>
    </div>
    <div class="form-check form-check-inline">
        <label class="form-check-label">
            <input class="form-check-input" type="radio" name="spayneut"
                   value="no"> No
        </label>
    </div>
</div>
با این خروجی:



کلاس‌های کنترل اندازه و اعتبارسنجی المان‌های فرم‌های بوت استرپ 4

- با استفاده از کلاس form-control-sm می‌توان اندازه‌ی فیلدهای input را با ارتفاع کوچکتری نمایش داد و یا توسط کلاس form-control-lg می‌توان آن‌ها را بزرگتر کرد.
- کلاس form-inline سبب می‌شود تا یک form-group به صورت inline نمایش داده شود. یعنی برچسب و کنترل‌های درون آن، در طی یک سطر نمایش داده خواهند شد. در این حالت، نیاز به کلاس‌های Margin مانند mx-sm-2 خواهد بود تا فاصله‌ی بین کنترل‌ها را بتوان کنترل کرد.
- برای نمایش نتایج اعتبارسنجی کنترل‌ها:
  - اگر کل فرم اعتبارسنجی شده‌است، کلاس was-validated را به المان form اضافه کنید.
  - اگر اعتبارسنجی کنترلی با موفقیت روبرو شود، کلاس is-valid و اگر خیر کلاس is-invalid را به آن نسبت دهید.
  - اگر می‌خواهید پیام خاصی را پس از موفقیت اعتبارسنجی نمایش دهید، آن‌را درون یک div با کلاس valid-feedback قرار دهید و یا برعکس از کلاس invalid-feedback استفاده کنید.
  - برای تغییر رنگ برچسب المان‌ها نیز از کلاس‌های text-color همانند قبل استفاده کنید؛ مانند text-success.

یک مثال:
<div class="form-group">
    <label for="owneremail" class="text-success">Email address</label>
    <input class="form-control is-valid" type="email" id="owneremail"
        aria-describedby="emailHelp" placeholder="Enter email">
    <small class="form-text text-muted" id="emailHelp">We'll
        never share your email</small>
    <div class="valid-feedback">
        Looks good!
    </div>
</div>
با این خروجی:



تغییر نحوه‌ی چیدمان عناصر فرم‌ها در بوت استرپ 4

فرم زیر را در نظر بگیرید:


قصد داریم با استفاده از کلاس‌های ویژه‌ی بوت استرپ 4، آن‌را دو ستونی کنیم؛ به طوریکه برچسب‌ها در یک ستون و فیلدهای ورودی، در ستونی دیگر نمایش داده شوند. همچنین این فرم واکنشگرا نیز باشد؛ به این معنا که این دو ستونی شدن، فقط در اندازه‌های پس از md رخ دهد:
<body>
    <div class="container">
        <h2>Medical Questionnaire</h2>
        <form>
            <fieldset class="form-group">
                <legend>Owner Info</legend>
                <div class="form-group row">
                    <label class="form-control-label col-md-2 col-form-label text-md-right"
                        for="ownername">Owner</label>
                    <div class="col-md-10">
                        <input class="form-control" type="text" id="ownername"
                            placeholder="Your Name">
                    </div>
                </div>
                <div class="form-group row">
                    <label class="form-control-label col-md-2 col-form-label text-md-right"
                        for="owneremail">Address</label>
                    <div class="col-md-10">
                        <input class="form-control" type="text" id="owneremail"
                            placeholder="Address">
                    </div>
                </div>
                <div class="form-group row">
                    <div class="form-group col-6 offset-md-2">
                        <label class="form-control-label sr-only" for="ownercity">City</label>
                        <input class="form-control" type="text" id="ownercity"
                            placeholder="City">
                    </div>
                    <div class="form-group col-md-4 col-6">
                        <label class="form-control-label sr-only" for="ownerzip">Zip</label>
                        <input class="form-control" type="text" id="ownerzip"
                            placeholder="Zip">
                    </div>
                </div>

                <div class="form-group row">
                    <div class="offset-md-2 col-md-10">
                        <button class="btn btn-primary" type="submit">Submit</button>
                    </div>
                </div>
            </fieldset>
        </form>
    </div>
</body>
با این خروجی در اندازه‌ی پس از md:


توضیحات:
برای ستونی کردن فرم‌ها، ابتدا کلاس row، به form-group قرار گرفته‌ی داخل container اصلی اضافه می‌شود:
                <div class="form-group row">
                    <label class="form-control-label col-md-2 col-form-label text-md-right"
                        for="ownername">Owner</label>
                    <div class="col-md-10">
                        <input class="form-control" type="text" id="ownername"
                            placeholder="Your Name">
                    </div>
                </div>
سپس توسط کلاس col-md-2 تعریف شده‌ی بر روی برچسب، سبب خواهیم شد تا در اندازه‌ی صفحه‌ی بیش از md، این برچسب در یک ستون با عرض دو واحد قرار گیرد. در یک چنین حالتی، ذکر col-form-label نیز ضروری است. همچنین اگر مایل باشیم تا این برچسب، در سمت راست این ستون قرار گیرد، می‌توان از کلاس واکنشگرای text-md-right استفاده کرد.
پس از آن نوبت به تعریف ستون فیلد تعریف شده‌است که با ایجاد یک div و تعریف تعداد واحدی را که به خود اختصاص می‌دهد (col-md-10)، انجام می‌شود.

در اینجا برچسب‌های فیلدهای city و zip با کلاس sr-only مشخص شده‌اند. به همین جهت فقط به screen readers نمایش داده می‌شوند.
<div class="form-group row">
   <div class="form-group col-6 offset-md-2">
   <label class="form-control-label sr-only" for="ownercity">City</label>
   <input class="form-control" type="text" id="ownercity"placeholder="City">
</div>
در یک چنین حالتی، برای اینکه این فیلدها در ستون دوم ظاهر شوند، از کلاس offset-md-2 استفاده شده‌است. از این offset برای تراز کردن دکمه، با ستون دوم نیز استفاده کرده‌ایم:
<div class="form-group row">
    <div class="offset-md-2 col-md-10">
        <button class="btn btn-primary" type="submit">Submit</button>
    </div>
</div>

ایجاد گروهی از ورودی‌ها در بوت استرپ 4

برای افزودن آیکن‌هایی به فیلدهای ورودی، از روش ایجاد گروهی از ورودی‌ها در بوت استرپ 4 استفاده می‌شود:
<div class="form-group">
    <label class="form-control-label" for="donationamt">
        Donation Amount
    </label>
    <div class="input-group">
        <div class="input-group-prepend">
            <span class="input-group-text">$</span>
        </div>
        <input type="text" class="form-control" id="donationamt"
            placeholder="Amount">
        <div class="input-group-append">
            <span class="input-group-text">.00</span>
        </div>
    </div>
</div>
در مثال فوق، روش تعریف یک input-group را مشاهده می‌کنید. داخل آن یک input-group-prepend و سپس input-group-text تعریف می‌شود که می‌تواند شامل یک متن و یا آیکن باشد. اگر نیاز به تعریف دکمه‌ای وجود داشت، از این کلاس استفاده نکنید. با این خروجی:


در بوت استرپ 4، کلاس‌های input-group-addon و input-group-btn  بوت استرپ 3 حذف و با کلاس‌های input-group-prepend و input-group-append جایگزین شده‌اند. از prepend برای قرار دادن آیکنی پیش از فیلد ورودی و از append همانند مثال فوق، برای قرار دادن آیکنی اختیاری پس از فیلد ورودی استفاده می‌شود.

نمونه‌ی متداول دیگر آن، نحوه‌ی تعریف ویژه‌ی فیلد جستجوی سایت، در منوی راهبری آن است:
    <nav class="navbar bg-dark navbar-dark navbar-expand-sm">
        <div class="container">
            <div class="navbar-brand d-none d-sm-inline-block">
                Wisdom Pet Medicine
            </div>
            <div class="navbar-nav mr-auto">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>
                <a class="nav-item nav-link" href="#">Services</a>
                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link" href="#">Testimonials</a>
            </div>
            <form class="form-inline d-none d-md-inline-block">
                <div class="input-group">
                    <label for="search" class="form-control-label sr-only"></label>
                    <input type="text" id="search" class="form-control"
                        placeholder="Search ...">
                    <div class="input-group-append">
                        <button class="btn btn-outline-light" type="submit">Go</button>
                    </div>
                </div>
            </form>
        </div>
    </nav>
با این خروجی که در آن دکمه، توسط کلاس input-group-append، با فیلد ورودی کنار آن، یکپارچه به نظر می‌رسد:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: Bootstrap4_10.zip
بازخوردهای دوره
بایدها و نبایدهای استفاده از IoC Containers
برای مواردی که در پروژه پیش میاد و مجبور هستیم تا یک نمونه کلاس بدون پارامتر رو ایجاد کنیم چه راهکاری رو پیشنهاد می‌کنید؟ چون عملاً اون کلاس نمی‌تونه وابستگی‌های خودش رو از طریق تابع سازنده اش اعلام کنه.
به عنوان مثال در کد زیر من می‌خوام یک Custom Route Constraint تعریف کنم:
routes.MapRoute(
   name: "PagesById",
   url: "Page/{id}",
   defaults: new { controller = "Route", action = "PageById", id = UrlParameter.Optional },
   constraints: new { id = new CustomPageByIdRoute() }
);
و اینجا نمی‌تونم پارامتری به کلاس CustomPageByIdRoute بدم . کلاس CustomPageByIdRoute به این صورت هست:
    public class CustomPageByIdRoute : IRouteConstraint
    {
        private readonly IUnitOfWork _uow;
        private readonly IPage _page;

        public CustomPageByIdRoute(IUnitOfWork uow, IPage page)
        {
            _uow = uow;
            _page = page;
        }

        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
            return _page.FindBy(x => x.Id == Convert.ToInt32(values[parameterName]) && x.DeletedBy == 0).FirstOrDefault() != null;
        }
    }

نظرات مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
با سلام
برای مدل User شما از Int استفاده کرده اید من با توجه به ساختار کل پروژه که از Guid استفاده می‌کنیم مدل user  رو به صورت زیر تغییر داده ام
    public class User : IdentityUser<Guid, UserLogin, UserRole, UserClaim>
 حال در سرویس  ApplicationUserManager در متد GetCurrentUserId به صورت زیر پیاده سازی شده است
public Guid GetCurrentUserId()
{
    return _identity.GetUserId<Guid>();
}
که بعد از کامپایل خطای زیر داده می‌شود
The type 'System.Guid' cannot be used as type parameter 'T' in the generic type 
or method 'Microsoft.AspNet.Identity.IdentityExtensions.GetUserId<T>(System.Security.Principal.IIdentity)'. 
There is no boxing conversion from 'System.Guid' to 'System.IConvertible'
آیا فقط نوع داده باید int باشد؟
مطالب
الگوهای طراحی API - مکانیزم جلوگیری از پردازش تکراری درخواست ها - Request Deduplication

در فضایی که همواره هیچ تضمینی وجود ندارد که درخواست ارسال شده‌ی به یک API، همواره مسیر خود را همانطور که انتظار می‌رود طی کرده و پاسخ مورد نظر را در اختیار ما قرار می‌دهد، بی‌شک تلاش مجدد برای پردازش درخواست مورد نظر، به دلیل خطاهای گذرا، یکی از راهکارهای مورد استفاده خواهد بود. تصور کنید قصد طراحی یک مجموعه API عمومی را دارید، به‌نحوی که مصرف کنندگان بدون نگرانی از ایجاد خرابی یا تغییرات ناخواسته، امکان تلاش مجدد در سناریوهای مختلف مشکل در ارتباط با سرور را داشته باشند. حتما توجه کنید که برخی از متدهای HTTP مانند GET، به اصطلاح Idempotent هستند و در طراحی آنها همواره باید این موضوع مدنظر قرار بگیرد و خروجی مشابهی برای درخواست‌های تکراری همانند، مهیا کنید.

در تصویر بالا، حالتی که درخواست، توسط کلاینت ارسال شده و در آن لحظه ارتباط قطع شده‌است یا با یک خطای گذرا در سرور مواجه شده‌است و همچنین سناریویی که درخواست توسط سرور دریافت و پردازش شده‌است ولی کلاینت پاسخی را دریافت نکرده‌است، قابل مشاهده‌است.

نکته: Idempotence یکی از ویژگی های پایه‌ای عملیاتی در ریاضیات و علوم کامپیوتر است و فارغ از اینکه چندین بار اجرا شوند، نتیجه یکسانی را برای آرگومان‌های همسان، خروجی خواهند داد. این خصوصیت در کانتکست‌های مختلفی از جمله سیستم‌های پایگاه داده و وب سرویس‌ها قابل توجه می‌باشد.

Idempotent and Safe HTTP Methods

طبق HTTP RFC، متدهایی که پاسخ یکسانی را برای درخواست‌های همسان مهیا می‌کنند، به اصطلاح Idempotent هستند. همچنین متدهایی که باعث نشوند تغییری در وضعیت سیستم در سمت سرور ایجاد شود، به اصطلاح Safe در نظر گرفته خواهند شد. برای هر دو خصوصیت عنوان شده، سناریوهای استثناء و قابل بحثی وجود دارند؛ به‌عنوان مثال در مورد خصوصیت Safe بودن، درخواست GET ای را تصور کنید که یکسری لاگ آماری هم ثبت می‌کند یا عملیات بازنشانی کش را نیز انجام می‌دهد که در خیلی از موارد به عنوان یک قابلیت شناسایی خواهد شد. در این سناریوها و طبق RFC، باتوجه به اینکه هدف مصرف کننده، ایجاد Side-effect نبوده‌است، هیچ مسئولیتی در قبال این تغییرات نخواهد داشت. لیست زیر شامل متدهای مختلف HTTP به همراه دو خصوصیت ذکر شده می باشد:

HTTP MethodSafeIdempotent
GETYesYes
HEADYesYes
OPTIONSYesYes
TRACEYesYes
PUTNoYes
DELETENoYes
POSTNoNo
PATCHNoNo

Request Identifier as a Solution

راهکاری که عموما مورد استفاده قرار می‌گیرد، استفاده از یک شناسه‌ی یکتا برای درخواست ارسالی و ارسال آن به سرور از طریق هدر HTTP می باشد. تصویر زیر از کتاب API Design Patterns، روش استفاده و مراحل جلوگیری از پردازش درخواست تکراری با شناسه‌ای همسان را نشان می‌دهد:

در اینجا ابتدا مصرف کننده درخواستی با شناسه «۱» را برای پردازش به سرور ارسال می‌کند. سپس سرور که لیستی از شناسه‌های پردازش شده‌ی قبلی را نگهداری کرده‌است، تشخیص می‌دهد که این درخواست قبلا دریافت شده‌است یا خیر. پس از آن، عملیات درخواستی انجام شده و شناسه‌ی درخواست، به همراه پاسخ ارسالی به کلاینت، در فضایی ذخیره سازی می‌شود. در ادامه اگر همان درخواست مجددا به سمت سرور ارسال شود، بدون پردازش مجدد، پاسخ پردازش شده‌ی قبلی، به کلاینت تحویل داده می شود.

Implementation in .NET

ممکن است پیاده‌سازی‌های مختلفی را از این الگوی طراحی در اینترنت مشاهده کنید که به پیاده سازی یک Middleware بسنده کرده‌اند و صرفا بررسی این مورد که درخواست جاری قبلا دریافت شده‌است یا خیر را جواب می دهند که ناقص است. برای اینکه اطمینان حاصل کنیم درخواست مورد نظر دریافت و پردازش شده‌است، باید در منطق عملیات مورد نظر دست برده و تغییراتی را اعمال کنیم. برای این منظور فرض کنید در بستری هستیم که می توانیم از مزایای خصوصیات ACID دیتابیس رابطه‌ای مانند SQLite استفاده کنیم. ایده به این شکل است که شناسه درخواست دریافتی را در تراکنش مشترک با عملیات اصلی ذخیره کنیم و در صورت بروز هر گونه خطا در اصل عملیات، کل تغییرات برگشت خورده و کلاینت امکان تلاش مجدد با شناسه‌ی مورد نظر را داشته باشد. برای این منظور مدل زیر را در نظر بگیرید:

public class IdempotentId(string id, DateTime time)
{
    public string Id { get; private init; } = id;
    public DateTime Time { get; private init; } = time;
}

هدف از این موجودیت ثبت و نگهداری شناسه‌های درخواست‌های دریافتی می‌باشد. در ادامه واسط IIdempotencyStorage را برای مدیریت نحوه ذخیره سازی و پاکسازی شناسه‌های دریافتی خواهیم داشت:

public interface IIdempotencyStorage
{
    Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken);
    Task CleanupOutdated(CancellationToken cancellationToken);
    bool IsKnownException(Exception ex);
}

در اینجا متد TryPersist سعی می‌کند با شناسه دریافتی یک رکورد را ثبت کند و اگر تکراری باشد، خروجی false خواهد داشت. متد CleanupOutdated برای پاکسازی شناسه‌هایی که زمان مشخصی (مثلا ۱۲ ساعت) از دریافت آنها گذشته است، استفاده خواهد شد که توسط یک وظیفه‌ی زمان‌بندی شده می تواند اجرا شود؛ به این صورت، امکان استفاده‌ی مجدد از آن شناسه‌ها برای کلاینت‌ها مهیا خواهد شد. پیاده سازی واسط تعریف شده، به شکل زیر خواهد بود:

/// <summary>
/// To prevent from race-condition, this default implementation relies on primary key constraints.
/// </summary>
file sealed class IdempotencyStorage(
    AppDbContext dbContext,
    TimeProvider dateTime,
    ILogger<IdempotencyStorage> logger) : IIdempotencyStorage
{
    private const string ConstraintName = "PK_IdempotentId";

    public Task CleanupOutdated(CancellationToken cancellationToken)
    {
        throw new NotImplementedException(); //TODO: cleanup the outdated ids based on configurable duration
    }

    public bool IsKnownException(Exception ex)
    {
        return ex is UniqueConstraintException e && e.ConstraintName.Contains(ConstraintName);
    }

    // To tackle race-condition issue, the implementation relies on storage capabilities, such as primary constraint for given IdempotentId.
    public async Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken)
    {
        try
        {
            dbContext.Add(new IdempotentId(idempotentId, dateTime.GetUtcNow().UtcDateTime));
            await dbContext.SaveChangesAsync(cancellationToken);

            return true;
        }
        catch (UniqueConstraintException e) when (e.ConstraintName.Contains(ConstraintName))
        {
            logger.LogInformation(e, "The given idempotentId [{IdempotentId}] already exists in the storage.", idempotentId);
            return false;
        }
    }
}

همانطور که مشخص است در اینجا سعی شده‌است تا با شناسه‌ی دریافتی، یک رکورد جدید ثبت شود که در صورت بروز خطای UniqueConstraint، خروجی با مقدار false را خروجی خواهد داد که می توان از آن نتیجه گرفت که این درخواست قبلا دریافت و پردازش شده‌است (در ادامه نحوه‌ی استفاده از آن را خواهیم دید).

در این پیاده سازی از کتابخانه MediatR استفاده می کنیم؛ در همین راستا برای مدیریت تراکنش ها به صورت زیر می توان TransactionBehavior را پیاده سازی کرد:

internal sealed class TransactionBehavior<TRequest, TResponse>(
    AppDbContext dbContext,
    ILogger<TransactionBehavior<TRequest, TResponse>> logger) :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IBaseCommand
    where TResponse : IErrorOr
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        string commandName = typeof(TRequest).Name;
        await using var transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken);

        TResponse? result;
        try
        {
            logger.LogInformation("Begin transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command);

            result = await next();
            if (result.IsError)
            {
                await transaction.RollbackAsync(cancellationToken);

                logger.LogInformation("Rollback transaction {TransactionId} for handling {CommandName} ({@Command}) due to failure result.", transaction.TransactionId, commandName, command);

                return result;
            }

            await transaction.CommitAsync(cancellationToken);

            logger.LogInformation("Commit transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command);
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync(cancellationToken);

            logger.LogError(ex, "An exception occured within transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command);

            throw;
        }

        return result;
    }
}

در اینجا مستقیما AppDbContext تزریق شده و با استفاده از خصوصیت Database آن، کار مدیریت تراکنش انجام شده‌است. همچنین باتوجه به اینکه برای مدیریت خطاها از کتابخانه‌ی ErrorOr استفاده می کنیم و خروجی همه‌ی Command های سیستم، حتما یک وهله از کلاس ErrorOr است که واسط IErrorOr را پیاده سازی کرده‌است، یک محدودیت روی تایپ جنریک اعمال کردیم که این رفتار، فقط برروی IBaseCommand ها اجرا شود. تعریف واسط IBaseCommand به شکل زیر می‌باشد:

 
/// <summary>
/// This is marker interface which is used as a constraint of behaviors.
/// </summary>
public interface IBaseCommand
{
}

public interface ICommand : IBaseCommand, IRequest<ErrorOr<Unit>>
{
}

public interface ICommand<T> : IBaseCommand, IRequest<ErrorOr<T>>
{
}

public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, ErrorOr<Unit>>
    where TCommand : ICommand
{
    Task<ErrorOr<Unit>> IRequestHandler<TCommand, ErrorOr<Unit>>.Handle(TCommand request, CancellationToken cancellationToken)
    {
        return Handle(request, cancellationToken);
    }

    new Task<ErrorOr<Unit>> Handle(TCommand command, CancellationToken cancellationToken);
}

public interface ICommandHandler<in TCommand, T> : IRequestHandler<TCommand, ErrorOr<T>>
    where TCommand : ICommand<T>
{
    Task<ErrorOr<T>> IRequestHandler<TCommand, ErrorOr<T>>.Handle(TCommand request, CancellationToken cancellationToken)
    {
        return Handle(request, cancellationToken);
    }

    new Task<ErrorOr<T>> Handle(TCommand command, CancellationToken cancellationToken);
}

در ادامه برای پیاده‌سازی IdempotencyBehavior و محدود کردن آن، واسط IIdempotentCommand را به شکل زیر خواهیم داشت:

/// <summary>
/// This is marker interface which is used as a constraint of behaviors.
/// </summary>
public interface IIdempotentCommand
{
    string IdempotentId { get; }
}

public abstract class IdempotentCommand : ICommand, IIdempotentCommand
{
    public string IdempotentId { get; init; } = string.Empty;
}

public abstract class IdempotentCommand<T> : ICommand<T>, IIdempotentCommand
{
    public string IdempotentId { get; init; } = string.Empty;
}

در اینجا یک پراپرتی، برای نگهداری شناسه‌ی درخواست دریافتی با نام IdempotentId در نظر گرفته شده‌است. این پراپرتی باید از طریق مقداری که از هدر درخواست HTTP دریافت می‌کنیم مقداردهی شود. به عنوان مثال برای ثبت کاربر جدید، به شکل زیر باید عمل کرد:

[HttpPost]
public async Task<ActionResult<long>> Register(
     [FromBody] RegisterUserCommand command,
     [FromIdempotencyToken] string idempotentId,
     CancellationToken cancellationToken)
{
     command.IdempotentId = idempotentId;
     var result = await sender.Send(command, cancellationToken);

     return result.ToActionResult();
}

در اینجا از همان Command به عنوان DTO ورودی استفاده شده‌است که وابسته به سطح Backward compatibility مورد نیاز، می توان از DTO مجزایی هم استفاده کرد. سپس از طریق FromIdempotencyToken سفارشی، شناسه‌ی درخواست، دریافت شده و بر روی command مورد نظر، تنظیم شده‌است.

رفتار سفارشی IdempotencyBehavior از ۲ بخش تشکیل شده‌است؛ در قسمت اول سعی می شود، قبل از اجرای هندلر مربوط به command مورد نظر، شناسه‌ی دریافتی را در storage تعبیه شده ثبت کند:

internal sealed class IdempotencyBehavior<TRequest, TResponse>(
    IIdempotencyStorage storage,
    ILogger<IdempotencyBehavior<TRequest, TResponse>> logger) :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IIdempotentCommand
    where TResponse : IErrorOr
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        string commandName = typeof(TRequest).Name;

        if (string.IsNullOrWhiteSpace(command.IdempotentId))
        {
            logger.LogWarning(
                "The given command [{CommandName}] ({@Command}) marked as idempotent but has empty IdempotentId",
                commandName, command);
            return await next();
        }

        if (await storage.TryPersist(command.IdempotentId, cancellationToken) == false)
        {
            return (dynamic)Error.Conflict(
                $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed.");
        }

        return await next();
    }
}

در اینجا IIdempotencyStorage تزریق شده و در صورتی که امکان ذخیره سازی وجود نداشته باشد، خطای Confilict که به‌خطای 409 ترجمه خواهد شد، برگشت داده می‌شود. در غیر این صورت ادامه‌ی عملیات اصلی باید اجرا شود. پس از آن اگر به هر دلیلی در زمان پردازش عملیات اصلی،‌ درخواست همزمانی با همان شناسه، توسط سرور دریافت شده و پردازش شود، عملیات جاری با خطای UniqueConstaint برروی PK_IdempotentId در زمان نهایی سازی تراکنش جاری، مواجه خواهد شد. برای این منظور بخش دوم این رفتار به شکل زیر خواهد بود:

internal sealed class IdempotencyExceptionBehavior<TRequest, TResponse>(IIdempotencyStorage storage) :
    IPipelineBehavior<TRequest, TResponse>
    where TRequest : IIdempotentCommand
    where TResponse : IErrorOr
{
    public async Task<TResponse> Handle(
        TRequest command,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(command.IdempotentId)) return await next();

        string commandName = typeof(TRequest).Name;
        try
        {
            return await next();
        }
        catch (Exception ex) when (storage.IsKnownException(ex))
        {
            return (dynamic)Error.Conflict(
                $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed.");
        }
    }
}

در اینجا عملیات اصلی در بدنه try اجرا شده و در صورت بروز خطایی مرتبط با Idempotency، خروجی Confilict برگشت داده خواهد شد. باید توجه داشت که نحوه ثبت رفتارهای تعریف شده تا اینجا باید به ترتیب زیر انجام شود:

services.AddMediatR(config =>
{
   config.RegisterServicesFromAssemblyContaining(typeof(DependencyInjection));

   // maintaining the order of below behaviors is crucial.
   config.AddOpenBehavior(typeof(LoggingBehavior<,>));
   config.AddOpenBehavior(typeof(IdempotencyExceptionBehavior<,>));
   config.AddOpenBehavior(typeof(TransactionBehavior<,>));
   config.AddOpenBehavior(typeof(IdempotencyBehavior<,>));
});

به این ترتیب بدنه اصلی هندلرهای موجود در سیستم هیچ تغییری نخواهند داشت و به صورت ضمنی و انتخابی، امکان تعیین command هایی که نیاز است به صورت Idempotent اجرا شوند را خواهیم داشت.

References

https://www.mscharhag.com/p/rest-api-design

https://www.manning.com/books/api-design-patterns

https://codeopinion.com/idempotent-commands/

مطالب
C# 8.0 - Using declarations
یکی دیگر از ویژگی‌های جدید C# 8.0، پشتیبانی از using declarations (اعلان‌های using) در مقابل using statements (عبارات using) پیشین است که سبب می‌شود بتوان کدهای کمتری را برای تعریف آن‌ها نوشت.


مثالی از using declarations

تا پیش از C# 8.0، روش متداول کار با عبارات using به صورت زیر است و به آن استفاده از using statements گفته می‌شود:
    class Program
    {
        static void UsingOld()
        {
            using (var file = new FileStream("input.txt", FileMode.Open))
            using (var reader = new StreamReader(file))
            {
                var s = reader.ReadToEnd();

                // Do something with data
            }
        }
که در نهایت پس از پایان این قطعه کد، هر دو شیء file و reader به صورت خودکار Dispose می‌شوند.
اکنون در C# 8.0 می‌توان قطعه کد فوق را به کمک using declarations به صورت زیر خلاصه کرد:
    class Program
    {
        static void UsingNew(string[] args)
        {
            using Stream file = new FileStream("input.txt", FileMode.Open);
            using StreamReader reader = new StreamReader(file);

            var s = reader.ReadToEnd();

            // Do something with data
        }
که در اینجا پرانتزها و همچنین {} ها، حذف شده‌اند.


میدان دید using declarations

پس از این تغییرات، سؤال مهمی که مطرح می‌شود این است: متغیرهایی که توسط using declaration تعریف می‌شوند، تا چه زمانی زنده نگه داشته می‌شوند. به عبارتی متد UsingOldScope آیا همانند متد UsingNewScope عمل می‌کند؟ آیا متغیر buffer آن همانند متد UsingOldScope خارج از میدان دید usingها قرار می‌گیرد؟
    class Program
    {
        static void UsingNewScope()
        {
            string buffer = null;
            using Stream file = new FileStream("input.txt", FileMode.Open);
            using StreamReader reader = new StreamReader(file);

            buffer = reader.ReadToEnd();

            // Do something with data

            buffer = null;
        }

        static void UsingOldScope(string[] args)
        {
            string buffer = null;

            using (var file = new FileStream("input.txt", FileMode.Open))
            using (var reader = new StreamReader(file))
            {
                buffer = reader.ReadToEnd();
            }

            // Do something with data

            buffer = null;
        }
زمانیکه از using statements استفاده می‌شود (مانند متد UsingOldScope)، توسط آن یک scope نیز تعریف می‌شود (داخل {} ها) که در پایان آن، کار فراخوانی متد Dispose اشیاء IDisposable ارجاعی، به صورت خودکار انجام می‌شود. این فراخوانی نیز توسط کامپایلر در داخل یک قطعه کد try/finally صورت می‌گیرد تا حتی اگر در این بین استثنائی نیز رخ داد، حتما متد Dispose فراخوانی گردد.
اما زمانیکه از using declarations استفاده می‌شود (مانند متد UsingNewScope)، دیگر این {} را نداریم. اینبار scope تعریف شده، تا «پایان متد» ادامه پیدا می‌کند و سپس متد Dispose اشیاء ارجاعی، فراخوانی می‌گردد. بدیهی است در اینجا نیز همانند قبل، همان قطعه کد try/finally توسط کامپایلر جهت فراخوانی متد Dispose، تشکیل خواهد شد. بنابراین اگر بخواهیم متد UsingNewScope را توسط using statements پیشین بازنویسی کنیم، به یک چنین قطعه کدی خواهیم رسید که scope پس از using declarations، تا آخر متد ادامه پیدا می‌کند:
    string buffer = null; 
    using (var file = new FileStream("input.txt", FileMode.Open)) 
    { 
        using (var reader = new StreamReader(file)) 
        { 
            buffer = reader.ReadToEnd(); 
            buffer = null; 
        } 
    }


سؤال: آیا امکان محدود کردن میدان دید using declarations وجود دارد؟

پاسخ: بله. می‌توان با تعریف یک {}، میدان دید متغیرهای ارجاعی توسط using declarations را محدود کرد:
private static void UsingDeclarationWithScope()
{
    {
        using var r1 = new AResource();
        r1.UseIt();
    }  // r1 is disposed here!
    Console.WriteLine("r1 is already disposed");
}
در اینجا جائیکه {} بسته می‌شود، متغیر r1 از میدان دید خارج شده و بلافاصله Dispose خواهد شد.


سؤال: آیا using declarations تمام قابلیت‌های using statements را ارائه می‌دهند؟

پاسخ: خیر. فرض کنید کلاس AResource از نوع IDisposable تعریف شده‌است:
    public class AResource : IDisposable
    {
        public void UseIt() => Console.WriteLine(nameof(UseIt));
        public void Dispose() => Console.WriteLine($"Dispose {nameof(AResource)}");
    }
و سپس متدی، وهله‌ای از این کلاس را باز می‌گرداند:
    class Program
    {
        public static AResource GetTheResource() => new AResource();
با استفاده از using statements، نوشتن چنین قطعه کدی بدون تعریف متغیری مجاز است:
using (GetTheResource())
{
   // do something here
}  // resource is disposed here
اما اگر اینکار را توسط using declarations انجام دهیم، به چندین خطای کامپایلر خواهیم رسید:
using GetTheResource(); // Compiler error
علت اینجا است که برخلاف using statements، ذکر متغیرهای scope برای using declarations اجباری است. برای رفع آن می‌توان از یک discard استفاده کرد:
using var _ = GetTheResource(); // Works fine
مطالب
انتخاب پویای فیلد ها در LINQ

LINQ یک DLS  بر مبنای .NET  می باشد که برای پرس و جو در منابع داده ای مانند پایگاه‌های داده ، فایل‌های XML و یا لیستی از اشیاء درون حافظه کاربرد دارد.

یکی از بزرگترین مزیت‌های آن Syntax  آسان و خوانا آن می‌باشد.

LINQ  از 2 نوع نمادگذاری پشتیبانی می‌کند:

  • Inline LINQ یا query expressions : 
var result = 
    from product in dbContext.Products
    where product.Category.Name == "Toys"
    where product.Price >= 2.50
    select product.Name;
  • Fluent Syntax : 
var result = dbContext.Products
    .Where(p => p.Category.Name == "Toys" && p.Price >= 250)
    .Select(p => p.Name);

در پرس و چو‌های بالا فیلد‌های مورد نیاز در قسمت Select در زمان Compile شناخته شده هستند . اما گاهی ممکن است فیلد‌های مورد نیاز در زمان اجرا مشخص شوند.

به عنوان مثال یک گزارش ساز پویا که کاربر مشخص می‌کند چه ستون هایی در خروجی نمایش داده شوند یا یک جستجوی پیشرفته که ستون‌های خروجی به اختیار کاربر در زمان اجرا مشخص می‌شوند. 

این مدل را در نظر داشته باشید :

    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Field1 { get; set; }
        public string Field2 { get; set; }
        public string Field3 { get; set; }


        public static IEnumerable<Student> GetStudentSource()
        {
            for (int i = 0; i < 10; i++)
            {
                yield return new Student
                                 {
                                     Id = i,
                                     Name = "Name " + i,
                                     Field1 = "Field1 " + i,
                                     Field2 = "Field2 " + i,
                                     Field3 = "Field3 " + i
                                 };
            }
        }
    }

ستون‌های کلاس Student  را در رابط کاربری برنامه جهت انتخاب به کاربر نمایش می‌دهیم. سپس کاربر یک یا چند ستون را انتخاب می‌کند که قسمت Select  کوئری برنامه باید  بر اساس فیلد‌های مورد نظر کاربر مشخص شود.

یکی از روش هایی که می‌توان از آن بهره برد استفاده از کتاب خانه Dynamic LINQ معرفی شده در اینجا می باشد.

این کتابخانه جهت سهولت در نصب به کمک NuGet در این آدرس قرار دارد.

فرض بر این است که فیلد‌های انتخاب شده توسط کاربر با "," از یکدیگر جدا شده اند. 

    public class Program
    {
        private static void Main(string[] args)
        {
            System.Console.WriteLine("Specify the desired fields : ");
            string fields = System.Console.ReadLine();
            IEnumerable<Student> students = Student.GetStudentSource();
            IQueryable output = students.AsQueryable().Select(string.Format("new({0})", fields));
            foreach (object item in output)
            {
                System.Console.WriteLine(item);
            }
          
            System.Console.ReadKey();
        }
  
    }

همانطور که در عکس ذیل مشاهده می‌کنید پس از اجرای برنامه ، فیلد‌های انتخاب شده توسط کاربر از منبع داده‌ی دریافت شده و در خروجی نمایش داده شده اند.

این روش مزایا و معایب خودش را دارد ، به عنوان مثال خروجی یک لیست از شیء Student  نیست یا این Select  فقط برای روی یک شیء IQueryable  قابل انجام است.

روش دیگری که می‌توان از آن بهره جست استفاده از یک متد کمکی جهت تولید پویای عبارت Lambda  ورودی Select  می باشد :  

    public  class SelectBuilder <T>
    {
        public static Func<T, T> CreateNewStatement(string fields)
        {
            // input parameter "o"
            var xParameter = Expression.Parameter(typeof(T), "o");


            // new statement "new T()"
            var xNew = Expression.New(typeof(T));

            // create initializers
            var bindings = fields.Split(',').Select(o => o.Trim())
                .Select(o =>
                {

                    // property "Field1"
                    var property = typeof(T).GetProperty(o);

                    // original value "o.Field1"
                    var xOriginal = Expression.Property(xParameter, property);

                    // set value "Field1 = o.Field1"
                    return Expression.Bind(property, xOriginal);
                }
            ).ToList();

            // initialization "new T { Field1 = o.Field1, Field2 = o.Field2 }"
            var xInit = Expression.MemberInit(xNew, bindings);

            // expression "o => new T { Field1 = o.Field1, Field2 = o.Field2 }"
            var lambda = Expression.Lambda<Func<T, T>>(xInit, xParameter);

            // compile to Func<T, T>
            return lambda.Compile();
        }
    }
برای استفاده از متد CreateNewStatement باید اینگونه عمل کرد :  
       IEnumerable<Student> result = students.Select(SelectBuilder<Student>.CreateNewStatement("Field1, Field2")).ToList();

            foreach (Student student in result)
            {
                System.Console.WriteLine(student.Field1);
            }
خروجی یک لیست از Student  می باشد.
 نحوه‌ی کارکرد CreateNewStatement :

ابتدا فیلد‌های انتخابی کاربر که با "," جدا شده اند به ورودی پاس داده می‌شود سپس یک statement  خالی ایجاد می‌شود

o=>new Student()
فیلد‌های ورودی از یکدیگر تفکیک می‌شوند و به کمک Reflection پراپرتی معادل فیلد رشته ای در کلاس Student پیدا می‌شود :  
var property = typeof(T).GetProperty(o);
سپس عبارت Select و تولید شیء جدید بر اساس فیلد‌های ورودی تولید می‌شود و برای استفاده Compile  به Func می‌شود. در نهایت Func  تولید شده به Select پاس داده می‌شود و لیستی از Student  بر مبنای فیلد‌های انتخابی تولید می‌شود. 

دریافت مثال : DynamicSelect.zip