نظرات مطالب
آشنایی با Gridify
فرض کنید مدل زیر رو داریم
public class Order : AggregateRoot<int>
{
    private DateTime _orderDate;
    public Address Address { get; private set; }
    public int? GetBuyerId => _buyerId;
    public int? _buyerId;
    public OrderStatus OrderStatus { get; private set; }
    public  int _orderStatusId;
    private string _description;
    private bool _isDraft;


    private readonly List<OrderItem> _orderItems;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

    protected Order()
    {
        _orderItems = new List<OrderItem>();
        _isDraft = false;
    }
    public Order(string userId, string userName, Address address,
        int? buyerId = null) : this()
    {
        _buyerId = buyerId;
        _orderStatusId = OrderStatus.Submitted.Id;
        _orderDate = DateTime.UtcNow;
        Address = address;
        //AddOrderStartedDomainEvent(userId, userName);
    }
}

public class OrderStatus : Enumeration
{
    public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant());
    public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());
    public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant());
    public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant());
    public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant());
    public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant());
    public OrderStatus(int id, string name)
        : base(id, name)
    {
    }
}
حال چگونه میتوانیم از Order بر اساس OrderStatus.Id فیلترینگ انجام بدیم ؟
var query = _orderQueryRepository.GetAll(x => x._buyerId == 52).AsNoTracking();
var s = await query.GridifyAsync(request.queryFilter);
return s.Adapt<Paging<OrderQuery>>();
{
  "buyerid": 0,
  "queryFilter": {
    "page": 1,
    "pageSize": 5,
    "orderBy": "id",
    "filter": "Order_OrderStatus_Id==1"
  }
}
خروجی
 "message": "Property 'Order_OrderStatus_Id' not found.",

مطالب
ویژگی های کمتر استفاده شده در NET. - بخش اول

ObsoleteAttribute

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

با استفاده از پروپرتی Message آن پیامی را به کاربر استفاده کننده نشان خواهد داد و توصیه می‌شود در این پیام یک راه حل نیز ارائه شود.

پروپرتی IsError در صورتی که مقدار آن به true تعیین شده باشد و کامپایلر در صورتی که عنصری که این خصوصیت بر روی آن تعریف شده است، استفاده شده باشد، در پنجره Error List، پیام مربوط به Obsolete را نشان می‌دهد. برای مثال پس از استفاده از کلاس زیر، OrderDetailTotal به صورت warning و CalculateOrderDetailTotal به صورت Error در پنجره Error List نشان داده می‌شود.

public static class ObsoleteExample
{
    // Mark OrderDetailTotal As Obsolete.
    [ObsoleteAttribute("This property (OrderDetailTotal) is obsolete. Use InvoiceTotal instead.", false)]
    public static decimal OrderDetailTotal
    {
        get  {  return 12m; }
    }

    public static decimal InvoiceTotal
    {
        get  {  return 25m;  }
    }

    // Mark CalculateOrderDetailTotal As Obsolete.
    [ObsoleteAttribute("This method is obsolete. Call CalculateInvoiceTotal instead.", true)]
    public static decimal CalculateOrderDetailTotal()
    {
        return 0m;
    }

    public static decimal CalculateInvoiceTotal()
    {
        return 1m;
    }
}

DefaultValueAttribute

DefaultValueAttribute جهت تعیین مقدار پیش فرض یک پروپرتی استفاده می‌شود. شما می‌توانید یک DefaultValueAttribute را با هر مقداری ایجاد کنید. ایجاد مقدار پیش فرض برای یک پروپرتی باعث نمی‌شود که مقداردهی اولیه‌ای به آن انجام گیرد؛ برای این کار نیاز به کدنویسی می‌باشد.
مثال زیر نحوه استفاده و مقداردهی اولیه پروپرتی‌ها را نشان می‌دهد.
public class DefaultValueAttributeTest
{
    public DefaultValueAttributeTest()
    {
        // Use the DefaultValue propety of each property to actually set it, via reflection.
        foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(this))
        {
            var attr = prop.Attributes[typeof(DefaultValueAttribute)] as DefaultValueAttribute;
            if (attr != null)
                prop.SetValue(this, attr.Value);
        }
    }

    [DefaultValue(28)]
    public int Age { get; set; }

    [DefaultValue("Vahid")]
    public string FirstName { get; set; }

    [DefaultValue("Mohammad Taheri")]
    public string LastName { get; set; }

    public override string ToString()
    {
        return $"{this.FirstName} {this.LastName} is {this.Age}.";
    }
}

DebuggerBrowsableAttribute 

در صورت استفاده از DebuggerBrowsableAttribute ، شما می‌توانید نحوه نمایش یک عضو را در پنجره متغیرها، در زمان دیباگ، تعیین کنید.
public class DebuggerBrowsableTest
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)] // عدم نمایش در زمان دیباگ در پنجره متغیرها
    public string FirstName { get; set; }

    [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] // مقدار پیش فرض
    public string LastName { get; set; }

    [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] // عدم نمایش در زمان دیباگ در پنجره متغیرها
    public string FullName => FirstName + " " + LastName;

    [DebuggerBrowsable( DebuggerBrowsableState.RootHidden )] // تنها در زمانی که یک آرایه یا لیست باشد نمایش داده می‌شود
    public string[] FullNameArray => new string[] { FirstName + " " + LastName };
}

 اگر از کد مثال بالا استفاده کنید و با استفاده از کلید F11 به صورت خط به خط دستورات را اجرا کنید، مشاهده خواهید کرد متغیر FirstName و FullName در پنجره Autos نشان داده نخواهد شد.

 

Operator ??

عملگر ??  در صورتی که عملوند سمت چپ آن تهی (null) نباشد، مقدار آن را باز می‌گرداند و در غیر اینصورت مقدار عملوند سمت راست خود را باز می‌گرداند. نوع‌های تهی پذیر (nullable) می‌توانند دارای مقدار و یا به صورت تعریف نشده باشند. عملگر ?? وقتی که یک نوع تهی پذیر به یک نوع غیرتهی پذیر انتساب داده می‌شود، مقدار پیش فرض آن را باز می‌گرداند.

int? x = null;
int y = x ?? -1;
Console.WriteLine("y now equals -1 because x was null => {0}", y);
int i = DefaultValueOperatorTest.GetNullableInt() ?? default(int);
Console.WriteLine("i equals now 0 because GetNullableInt() returned null => {0}", i);
string s = DefaultValueOperatorTest.GetStringValue();
Console.WriteLine("Returns 'Unspecified' because s is null => {0}", s ?? "Unspecified");
مطالب
عمومی سازی الگوریتم‌ها با استفاده از Reflection
در این مقاله قصد داریم عملیات Reflection را بیشتر در انجام ساده‌تر عملیات ببینیم. عملیاتی که به همراه کار اضافه، تکراری و خسته کننده است و با استفاده از Reflection این کارها حذف شده و تعداد خطوط هم پایین می‌آید. حتی گاها ممکن است موجب استفاده‌ی مجدد از کدها شود که همگی این عوامل موجب بالا رفتن امتیاز Refactoring می‌شوند.
در مثال‌های زیر مجموعه‌ای از Reflection‌های ساده و کاملا کاربردی است که من با آن‌ها روبرو شده ام.


کوتاه سازی کدهای نمایش یک View در ASP.NET MVC با Reflection 

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

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

ساختار اطلاعاتی تصویر فوق به شرح زیر است:
 <div>
                              <div>
                                  <div>
                                      <p><span>First Name </span>: Jonathan</p>
                                  </div>
   </div>
                          </div>
که به دو فایل پارشال تقسیم شده است Bio_ و BioRow_ که محتویات هر پارشال هم به شرح زیر است:

BioRow_
@model System.Web.UI.WebControls.ListItem


<div>
    <p><span>@Model.Text </span>: @Model.Value</p>
</div>
در پارشال بالا ورودی از نوع listItem است که یک متن دارد و یک مقدار.(شاید به نظر شبیه حالت جفت کلید و مقدار باشد ولی در این کلاس خبری از کلید نیست).
پارشال پایینی هم دربرگیرنده‌ی پارشال بالاست که قرار است چندین و چند بار پارشال بالا در خودش نمایش دهد.
Bio_
@using System.Web.UI.WebControls
@using ZekrWepApp.Filters
@model ZekrModel.Admin

    <div>
        <h1>Bio Graph</h1>
        <div>
            
            @{
                ListItemCollection collection = GetCustomProperties.Get(Model,exclude:new string[]{"Poems","Id"});
                foreach (var item in collection)
                {
                    Html.RenderPartial(MVC.Shared.Views._BioRow, item);
                }
            }

                    </div>
    </div>
پارشال بالا یک مدل از کلاس Admin را می‌پذیرد که قرار است اطلاعات شخصی مدیر را نمایش دهد. در ابتدا متدی از یک کلاس ایستا وجود دارد که کدهای Reflection درون آن قرار دارند که یک مجموعه از ListItem‌ها را بر می‌گرداند و سپس با یک حلقه، پارشال BioRow_ را صدا می‌زند.

کد درون این کلاس ایستا را بررسی می‌کنیم؛ این کلاس دو متد دارد یکی عمومی و دیگری خصوصی است:
  public class GetCustomProperties
    {
        private static PropertyInfo[] getObjectsInfos(object obj,string[] inclue,string[] exclude )
        {
            var list = obj.GetType().GetProperties();

            PropertyInfo[] outputPropertyInfos = null;

            if (inclue != null)
            {           
                return list.Where(propertyInfo => inclue.Contains(propertyInfo.Name)).ToArray();
            }
            if (exclude != null)
            {
                return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray();
            }
            return list;
        }
    }
کد بالا که یک کد خصوصی است، سه پارامتر را می‌پذیرد. اولی مدل یا کلاسی است که به آن پاس کرده‌ایم. دو پارامتر بعدی اختیاری است و در کد پارشال بالا Exclude را تعریف کرده ایم و تنهای یکی از دو پارامتر بالا هم باید مورد استفاده قرار بگیرند و Include ارجحیت دارد. وظیفه‌ی این دو پارمتر این است که آرایه ای از رشته‌ها را دریافت می‌کنند که نام پراپرتی‌ها در آن‌ها ذکر شده است. آرایه Include می‌گوید که فقط این پراپرتی‌ها را برگردان ولی اگر دوست دارید همه‌ی پارامترها را نمایش دهید و تنها یکی یا چندتا از آن‌ها را حذف کنید، از آرایه Exclude استفاده کنید. در صورتی که این دو آرایه خالی باشند، همه‌ی پراپرتی‌ها بازگشت داده می‌شوند و در صورتی که یکی از آن‌ها وارد شده باشد، طبق دستورات Linq بالا بررسی می‌کند که (Include) آیا اسامی مشترکی بین آن‌ها وجود دارد یا خیر؟ اگر وجود دارد آن را در لیست قرار داده و بر می‌گرداند و در حالت Exclude این مقایسه به صورت برعکس انجام می‌گیرد و باید لیستی برگردانده شود که اسامی، نکته مشترکی نداشته باشند.

متد عمومی که در این کلاس قرار دارد به شرح زیر است:
 public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null)
        {
            var propertyInfos = getObjectsInfos(obj, inclue, exclude);
            if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null");

            var collection = new ListItemCollection();

            foreach (PropertyInfo propertyInfo in propertyInfos)
            {


                string name = propertyInfo.Name;

                foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true))
                {
                    DisplayAttribute displayAttribute = attribute as DisplayAttribute;

                    if (displayAttribute != null)
                    {
                        name = displayAttribute.Name;
                        break;
                    }                  
                }

                string value = "";
                object objvalue = propertyInfo.GetValue(obj);
                if (objvalue != null) value = objvalue.ToString();

                collection.Add(new ListItem(name,value));
            }
            return collection;
        }
این متد سه پارامتر را از کاربر دریافت و به سمت متد خصوصی ارسال می‌کند. موقعی‌که پراپرتی‌ها بازگشت داده می‌شوند، دو قسمت آن مهم است؛ یکی عنوان پراپرتی و دیگری مقدار پراپرتی. از آن جا که نام پراپرتی‌ها طبق سلیقه‌ی برنامه نویس و با حروف انگلیسی نوشته می‌شوند، در صورتی که برنامه نویس از متادیتای Display در مدل بهره برده باشد، به جای نام پراپرتی مقداری را که به متادیتای Display داده‌ایم، بر می‌گردانیم.

کد بالا پراپرتی‌ها را دریافت و یک به یک متادیتاهای آن را بررسی کرده و در صورتی که از متادیتای Display استفاده کرده باشند، مقدار آن را جایگزین نام پراپرتی خواهد کرد. در مورد مقدار هم از آنجا که اگر پراپرتی با Null پر شده باشد، تبدیل به رشته‌ای با پیام خطای روبرو خواهد شد. در نتیجه بهتر است یک شرط احتیاط هم روی آن پیاده شود. در آخر هم از متن و مقدار، یک آیتم ساخته و درون Collection اضافه می‌کنیم و بعد از اینکه همه پراپرتی‌ها بررسی شدند، Collection را بر می‌گردانیم.
 [Display(Name = "نام کاربری")]
public string UserName { get; set; }

کد کامل کلاس:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using System.Web.Mvc.Html;
using System.Web.UI.WebControls;
using Links;

namespace ZekrWepApp.Filters
{
    
    public class GetCustomProperties
    {
        public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null)
        {
            var propertyInfos = getObjectsInfos(obj, inclue, exclude);
            if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null");

            var collection = new ListItemCollection();

            foreach (PropertyInfo propertyInfo in propertyInfos)
            {
                string name = propertyInfo.Name;
                foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true))
                {
                    DisplayAttribute displayAttribute = attribute as DisplayAttribute;

                    if (displayAttribute != null)
                    {
                        name = displayAttribute.Name;
                        break;
                    }                  
                }

                string value = "";
                object objvalue = propertyInfo.GetValue(obj);
                if (objvalue != null) value = objvalue.ToString();

                collection.Add(new ListItem(name,value));
            }
            return collection;
        }
        private static PropertyInfo[] getObjectsInfos(object obj,string[] include,string[] exclude )
        {
            var list = obj.GetType().GetProperties();

            PropertyInfo[] outputPropertyInfos = null;

            if (include != null)
            {           
                return list.Where(propertyInfo => include.Contains(propertyInfo.Name)).ToArray();
            }
            if (exclude != null)
            {
                return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray();
            }
            return list;
        }  
    }   
}

لیستی از پارامترها با Reflection


مورد بعدی که ساده‌تر بوده و از کد بالا مختصرتر هم هست، این است که قرار بود برای یک درگاه، یک سری اطلاعات را با متد Post ارسال کنم که نحوه‌ی ارسال اطلاعات به شکل زیر بود:
amount=1000&orderId=452&Pid=xxx&....
کد زیر را من جهت ساخت قالب‌های این چنینی استفاده می‌کنم:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Utils
{
    public class QueryStringParametersList
    {
        private string Symbol = "&";
        private List<KeyValuePair<string, string>> list { get; set; }

        public QueryStringParametersList()
        {
            list = new List<KeyValuePair<string, string>>();
        }
        public QueryStringParametersList(string symbol)
        {
            Symbol = symbol;
           list = new List<KeyValuePair<string, string>>(); 
        }

        public int Size
        {
            get { return list.Count; }
        }
        public void Add(string key, string value)
        {
            list.Add(new KeyValuePair<string, string>(key, value));
        }

        public string GetQueryStringPostfix()
        {
            return string.Join(Symbol, list.Select(p => Uri.EscapeDataString(p.Key) + "=" + Uri.EscapeDataString(p.Value)));
        }
    }
}



یک متغیر به نام symbol دارد و در صورتی در شرایط متفاوت، قصد چسپاندن چیزی را به یکدیگر با علامتی خاص داشته باشید، این تابع می‌تواند کاربرد داشته باشد. این متد از یک لیست کلید و مقدار استفاده کرده و پارامترهایی را که به آن پاس می‌شود، نگهداری و سپس توسط متد GetQueryStringPostfix آن‌ها را با یکدیگر الحاق کرده و در قالب یک رشته بر می‌گرداند.
کاربرد Reflection در اینجا این است که من باید دوبار به شکل زیر، دو نوع اطلاعات متفاوت را پست کنم. یکی موقع ارسال به درگاه و دیگری موقع بازگشت از درگاه.
QueryStringParametersList queryparamsList = new QueryStringParametersList();

            ueryparamsList.Add("consumer_key", requestPayment.Consumer_Key);
            queryparamsList.Add("amount", requestPayment.Amount.ToString());
            queryparamsList.Add("callback", requestPayment.Callback);
            queryparamsList.Add("description", requestPayment.Description);
            queryparamsList.Add("email", requestPayment.Email);
            queryparamsList.Add("mobile", requestPayment.Mobile);
            queryparamsList.Add("name", requestPayment.Name);
            queryparamsList.Add("irderid", requestPayment.OrderId.ToString());

ولی با استفاده از کد Reflection که در بالاتر عنوان شد، باید نام و مقدار پراپرتی را گرفته و در یک حلقه آن‌ها را اضافه کنیم، بدین شکل:
   private QueryStringParametersList ReadParams(object obj)
        {
            PropertyInfo[] propertyInfos = obj.GetType().GetProperties();

            QueryStringParametersList queryparamsList = new QueryStringParametersList();
            for (int i = 0; i < propertyInfos.Count(); i++)
            {
                queryparamsList.Add(propertyInfos[i].Name.ToLower(),propertyInfos[i].GetValue(obj).ToString() );              
            }
            return queryparamsList;
        }
در کد بالا هر بار پراپرتی‌های کلاس را خوانده و نام و مقدار آن‌ها را گرفته و به کلاس QueryString اضافه می‌کنیم. پارامتر ورودی این متد به این خاطر object در نظر گرفته شده است که تا هر کلاسی را بتوانیم به آن پاس کنیم که خودم در همین کلاس درگاه، دو کلاس را به آن پاس کردم.
مطالب
نوشتن TagHelperهای سفارشی برای ASP.NET Core
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» با مفهوم جدید Tag Helpers و همچنین نحوه‌ی استفاده‌ی از نمونه‌های پیش فرض و توکار آن در ASP.NET Core آشنا شدیم. در ادامه قصد داریم با نحوه‌ی پیاده سازی نمونه‌های سفارشی آن‌ها نیز آشنا شویم.


نوشتن یک Tag Helper سفارشی، برای رندر کردن لیست‌های بوت استرپی

فرض کنید می‌خواهیم یک tag helper جدید را جهت رندر کردن لیست بوت استرپی ذیل تهیه کنیم:
<ul class="list-group"> 
  <li class="list-group-item">Item 1</li> 
  <li class="list-group-item">Item 2</li> 
  <li class="list-group-item">Item 3</li> 
</ul>
برای اینکار یک کتابخانه‌ی جدید را به پروژه‌ی جاری اضافه کرده و سپس وابستگی‌های ذیل را نیز به آن اضافه می‌کنیم. این‌ها حداقل‌هایی هستند که جهت دسترسی به امکانات MVC و Tag Helpers، در یک پروژه‌ی مجزای Class library نیاز داریم:
{
  "version": "1.0.0-*",
 
    "dependencies": {
        "NETStandard.Library": "1.6.0",
        "Microsoft.AspNetCore.Http.Extensions": "1.0.0",
        "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.1",
        "Microsoft.AspNetCore.Mvc.Core": "1.0.1",
        "Microsoft.AspNetCore.Mvc.ViewFeatures": "1.0.1",
        "Microsoft.AspNetCore.Razor.Runtime": "1.0.0"
    },
 
  "frameworks": {
    "netstandard1.6": {
      "imports": "dnxcore50"
    }
  }
}


بررسی آناتومی یک کلاس TagHelper

یک کلاس Tag Helper سفارشی، در حالت کلی می‌تواند شکل زیر را داشته باشد:
namespace Core1RtmEmptyTest.TagHelpers
{
    [HtmlTargetElement("list-group")]
    public class ListGroupTagHelper : TagHelper
    {
        [HtmlAttributeName("asp-items")]
        public List<string> Items { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
        }
    }
}
در اینجا نام کلاس، به TagHelper ختم می‌شود و همچنین این کلاس از کلاس پایه‌ی TagHelper ارث بری می‌کند. ذکر HtmlTargetElement الزامی بوده و در صورت عدم تعریف آن، TagHelper تعریف شده توسط ASP.NET Core شناسایی و بارگذاری نخواهد شد.
توسط HtmlTargetElement نام نهایی تگ مرتبط با TagHelper سفارشی را تعریف و سفارشی سازی کرده‌ایم. در این حالت این TagHelper جدید در Viewهای برنامه، توسط تگ ذیل شنایی می‌شود (بجای نام پیش فرض کلاس):
 <list-group></list-group>
همچنین در اینجا، یک خاصیت عمومی نیز تعریف شده‌است. تمام خواص عمومی تعریف شده‌ی در اینجا به صورت ویژگی‌هایی در تگ نهایی TagHelper قابل دسترسی و مقدار دهی خواهند بود:
 <list-group asp-items="Model.Items"></list-group>
 برای لغو این حالت می‌توان از ویژگی HtmlAttributeNotBound استفاده کرد.
برای اینکه نام این ویژگی را نیز بتوانیم سفارشی سازی کنیم، می‌توان از ویژگی HtmlAttributeName استفاده کرد. در صورت عدم ذکر آن، از نام پیش فرض این خاصیت عمومی جهت تعریف ویژگی‌های تگ نهایی استفاده می‌گردد.
عملیات نهایی افزودن تگ‌های HTML، به View برنامه، در متد Process انجام می‌شود. در اینجا توسط متدهایی مانند output.Content.AppendHtml می‌توان خروجی دلخواهی را به صفحه اضافه کرد.


تکمیل کدهای Tag Helper سفارشی رندر کردن لیست‌های بوت استرپی

پس از آشنایی با ساختار کلی یک کلاس TagHelper، اکنون می‌توان کدهای آن را به نحو ذیل تکمیل کرد:
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace Core1RtmEmptyTest.TagHelpers
{
    [HtmlTargetElement("list-group")]
    public class ListGroupTagHelper : TagHelper
    {
        [HtmlAttributeName("asp-items")]
        public List<string> Items { get; set; }
 
        protected HttpRequest Request => ViewContext.HttpContext.Request;
 
        [ViewContext, HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
 
            if (output == null)
            {
                throw new ArgumentNullException(nameof(output));
            }
 
            if (Items == null)
            {
                throw new InvalidOperationException($"{nameof(Items)} must be provided");
            }
 
            output.TagName = "ul";
            output.TagMode = TagMode.StartTagAndEndTag;
            output.Attributes.Add("class", "list-group");
 
            foreach (var item in Items)
            {
                TagBuilder itemBuilder = new TagBuilder("li");
                itemBuilder.AddCssClass("list-group-item");
                itemBuilder.InnerHtml.Append(item);
                output.Content.AppendHtml(itemBuilder);
            }
        }
    }
}
توضیحات:
 - چون می‌خواهیم تگ نهایی آن، list-group نام داشته باشد، آن‌را توسط ویژگی HtmlTargetElement به صورت صریحی مشخص کرده‌ایم.
 - همچنین علاقمندیم تا ویژگی دریافت لیست آیتم‌ها، نامی معادل asp-items داشته باشد. بنابراین آن‌را نیز توسط ویژگی HtmlAttributeName، دقیقا مشخص کرده‌ایم.
 - در این کلاس، یک خاصیت اضافه‌ی ViewContext را نیز مشاهده می‌کنید. ویژگی ViewContext اعمالی به آن، سبب خواهد شد تا اطلاعات درخواست جاری، به این خاصیت عمومی، به صورت خودکار تزریق شود. بنابراین اگر نیاز به اطلاعاتی مانند Request جاری دارید، شیء ViewContext.HttpContext.Request، این مقادیر را در اختیار شما قرار می‌دهد. به علاوه اگر دقت کرده باشید، این خاصیت با ویژگی HtmlAttributeNotBound مزین شده‌است. از این جهت که نمی‌خواهیم این خاصیت عمومی، در لیست ویژگی‌های تگ نهایی TagHelper در حال تهیه، ظاهر شود.
 - پس از آن کاری که انجام شده، تکمیل متد Process است. در اینجا توسط output.TagName مشخص می‌کنیم که TagHelper جاری، در بین تگ‌های ul قرار گیرد (مفهوم TagMode.StartTagAndEndTag ذکر شده) و همچنین این تگ محصور کننده دارای کلاس list-group بوت استرپ نیز خواهد بود.
 - سپس بر روی لیست آیتم‌های دریافت شده، یک حلقه را تشکیل داده و به کمک TagBuilder، تگ‌های li داخل ul برونی را تکمیل می‌کنیم. این TagBuilder در نهایت توسط متد output.Content.AppendHtml به View برنامه اضافه خواهد شد. در اینجا، متد Append هم وجود دارد. اگر از آن استفاده شود، خروجی نهایی HTML Encoded خواهد بود.
 

ثبت و استفاده‌ی از TagHelper سفارشی تهیه شده

پس از تعریف TagHelper سفارشی فوق، ابتدا ارجاعی از اسمبلی آن‌را به پروژه‌ی جاری اضافه می‌کنیم و سپس به فایل ViewImports.cshtml_ برنامه مراجعه و یک سطر ذیل را به آن اضافه می‌کنیم:
 @addTagHelper *,Core1RtmEmptyTest.TagHelpers
در اینجا عبارت Core1RtmEmptyTest.TagHelpers همان نام فضای نام اصلی پروژه‌ی Class library دربرگیرنده‌ی TagHelper سفارشی است.
اکنون که این TagHelper در Viewهای برنامه قابل شناسایی است، روش افزودن آن، بر اساس همان سفارشی سازی‌هایی است که انجام دادیم:

مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 15 - بررسی تغییرات Caching
در نگارش‌های پیشین ASP.NET MVC با استفاده از Output Cache، امکان کش کردن خروجی یک اکشن متد، وجود دارد. مکانیزم Output Cache از ASP.NET Core حذف شده‌است؛ اما جایگزین‌های قابل توجهی برای آن تدارک دیده شده‌اند.


معرفی Response Cache

جایگزین ویژگی حذف شده‌ی OutputCache در ASP.NET Core، ویژگی جدیدی است به نام ResponseCache و هدف آن تنظیم هدرهای مرتبط با caching مخصوص HTTP Response ارائه شده‌است. به همین جهت با مکانیزم OutputCache قدیمی ASP.NET MVC که اطلاعات را در حافظه‌ی سرور کش می‌کرد، کاملا متفاوت است.
البته قرار است میان افزار OutputCache را در نگارش‌های آتی ASP.NET Core نیز ارائه کنند.
[ResponseCache(Duration = 60)]
public IActionResult Contact()
{
   ViewData["Message"] = "Your contact page.";
   return View();
}
در اینجا مثالی را از نحوه‌ی تعریف این ویژگی جدید، ملاحظه می‌کنید که در آن مقدار خاصیت مدت زمان کش شدن، برحسب ثانیه است. استفاده‌ی از آن سبب خواهد شد تا هدر HTTP ذیل به خروجی از سرور اضافه شود:
 Cache-Control: public,max-age=60

یک نکته:
این ویژگی را هم می‌توان به کل کنترلر اعمال کرد و هم به یک اکشن متد خاص. اگر این ویژگی هم به کنترلر و هم به اکشن متدی در آن کنترلر اعمال شده باشد، تنظیمات در سطح متدها، تنظیمات در سطح کلاس را بازنویسی می‌کنند.


تعیین مکان کش شدن خروجی یک اکشن متد

در هدر فوق، عبارت public را مشاهده می‌کنید. این public بودن به این معنا است که امکان کش شدن این خروجی، توسط کش سرورهای اشتراکی بین راه هم وجود دارد.
اگر می‌خواهید این امکان را غیرفعال کنید، نیاز است این public به private تنظیم شود:
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Client)]
تنظیم Location فوق به Client به معنای private شدن هدر تنظیم شده و صرفا کش شدن خروجی، توسط کش مرورگر کاربر می‌باشد.


غیرفعال کردن کش شدن خروجی یک اکشن متد

اگر خواستید از کش شدن خروجی یک اکشن متد تحت هر حالتی جلوگیری کنید، مکان آن‌را به None و NoStore آن‌را به true تنظیم کنید:
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
   return View();
}
این تنظیم سبب افزوده شدن یک چنین هدر HTTP ایی به خروجی از سرور می‌شود:
Cache-Control: no-store,no-cache
Pragma: no-cache


امکان تعریف پروفایل‌های کش

بجای اینکه تنظیمات کش کردن تکراری را به انواع و اقسام اکشن متدها اعمال کنیم، می‌توان برای آن‌ها پروفایل ایجاد کرده و از نام این پروفایل، جهت به اشتراک گذاری تنظیمات استفاده کنیم. برای این منظور به کلاس آغازین برنامه مراجعه کرده و جایی که سرویس ASP.NET MVC را فعال سازی کرده‌اید، پروفایل کش جدیدی را تعریف کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.CacheProfiles.Add("PrivateCache",
            new CacheProfile
            {
                Duration = 60,
                Location = ResponseCacheLocation.Client
            }); 
    });
پس از آن برای استفاده‌ی از این تنظیمات اشتراکی، فقط کافی است تا نام پروفایل مرتبطی را ذکر کنیم:
[ResponseCache(CacheProfileName = "PrivateCache")]


معرفی سرویس کش درون حافظه‌ای

در نگارش‌های پیشین ASP.NET، متدهایی برای کش کردن موقتی اطلاعات در حافظه و سپس بازیابی آن‌ها وجود داشتند. در ASP.NET Core، این متدها توسط سرویس ارائه کننده‌ی IMemoryCache در اختیار برنامه قرار می‌گیرند. برای فعال سازی این سرویس جدید باید مراحل ذیل طی شوند:
الف) ابتدا بسته‌ی Microsoft.Extensions.Caching.Memory را به لیست وابستگی‌های پروژه در فایل project.json اضافه کنید:
{
    "dependencies": {
      //same as before
      "Microsoft.Extensions.Caching.Memory": "1.0.0"
 },
ب) سپس به کلاس آغازین برنامه مراجعه کرده و سرویس آ‌ن‌را معرفی و ثبت کنید:
public void ConfigureServices(IServiceCollection services)
{
  services.AddMemoryCache();
ج) پس از آن سرویس پیاده سازی کننده‌ی IMemoryCache،  در تمام اجزای برنامه در دسترس خواهد بود. برای مثال:
[Route("DNT/[controller]")]
public class AboutController : Controller
{
    private readonly IMemoryCache _memoryCache;
 
    public AboutController(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }
 
    [Route("")]
    public ActionResult Hello()
    {
 
        string cacheKey = "my-cache-key";
        string greeting;
 
        if (!_memoryCache.TryGetValue(cacheKey, out greeting))
        {
             greeting = "Hello";
            // store in the cache
            _memoryCache.Set(cacheKey, greeting,
                new MemoryCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(1)));
        }
 
        return Content($"{greeting} from DNT!");
    }
در مثال فوق، ابتدا وابستگی سرویس کش درون حافظه‌ای، به سازنده‌ی کنترلر تزریق شده‌است. تامین آن هم توسط سرویسی که در کلاس آغازین برنامه ثبت کردیم، انجام می‌شود. پس از آن در اکشن متد Hello، سعی کرده‌ایم بر اساس کلید کشی که مشخص کرده‌ایم، مقداری را بازیابی کنیم. اگر این مقدار وجود نداشته باشد، آن‌را توسط متد Set تنظیم خواهیم کرد تا برای دفعات آتی فراخوانی این متد، مورد استفاده قرار گیرد.
تنظیمات منقضی شدن کش نیز به حالت absolute تنظیم شده‌است. یعنی پس از یک دقیقه حتما منقضی می‌شود. اگر فراخوانی‌های این متد زیاد است، می‌توان حالت منقضی شدن sliding را تنظیم کرد:
 new MemoryCacheEntryOptions()
  .SetSlidingExpiration(TimeSpan.FromMinutes(5))
در این حالت اگر پیش از اتمام 5 دقیقه‌ی تنظیم شده، درخواستی به سرور رسید، این کش برای 5 دقیقه‌ی بعد نیز مجددا تمدید می‌شود.
اگر خواستیم تا این کش سر ساعت منقضی شود، اما در طی این یک ساعت به صورت sliding عمل کند، می‌توان از ترکیب دو حالت مطلق و لغزشی استفاده کرد:
 new MemoryCacheEntryOptions()
  .SetSlidingExpiration(TimeSpan.FromMinutes(5))
  .SetAbsoluteExpiration(TimeSpan.FromHours(1))

یک نکته: اگر فشار حافظه‌ی سرور زیاد شود، مدیر حافظه‌ی این کش، شروع به منقضی کردن آیتم‌هایی با حق تقدم پایین می‌کند. بالاترین حق تقدم را حالت NeverRemove ذیل دارد:
 new MemoryCacheEntryOptions()
  .SetPriority(CacheItemPriority.NeverRemove))


معرفی Tag Helpers مخصوص کش کردن قسمتی از صفحه

در ادامه‌ی مبحث معرفی Tag Helpers، تعدادی از آن‌ها جهت کش کردن محتوای قسمتی از صفحه، طراحی شده‌اند:
<cache expires-after="@TimeSpan.FromMinutes(10)">
    @Html.Partial("_WhatsNew")
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>
تگ جدید cache محتوای دربرگیرنده‌ی آن‌را «در حافظه‌ی سرور» کش می‌کند (و در پشت صحنه از همان کش درون حافظه‌ای که پیشتر بحث شد، استفاده می‌کند). تگ cache در خروجی HTML نهایی مشاهده نمی‌شود و صرفا مفهومی سمت سرور است.
برای نمونه در مثال فوق، محتوای پارشال ویوو رندر شده و همچنین تاریخی که پس از آن نمایش داده شده‌است، به مدت 10 دقیقه در حافظه‌ی سرور کش می‌شوند. اگر این زمان تنظیم نشود، تا زمانیکه برنامه در سرور مشغول به کار است، این قسمت منقضی نخواهد شد.
در اینجا اگر expires-after ذکر شده بود، یعنی پس از این مدت زمان، کش منقضی می‌شود.
<cache expires-after="@TimeSpan.FromSeconds(5)">
    <!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>
 اگر expires-on آن ذکر شود، می‌توان تاریخ و زمان مشخصی را در اینجا ذکر کرد (برای مثال فردا ساعت 10، با فراخوانی DateTime.Today.AddDays).
<cache expires-on="@DateTime.Today.AddDays(1).AddTicks(-1)">
  <!--View Component or something that gets data from the database-->
 *last updated  @DateTime.Now.ToLongTimeString()
</cache>
همچنین می‌توان از expires-sliding نیز استفاده کرد. به این معنا که اگر در طی مدتی خاص این صفحه درخواست نشد، آنگاه این کشی منقضی می‌شود.
<cache expires-sliding="@TimeSpan.FromMinutes(5)">
    <!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>
همچنین در اینجا می‌توان کش کردن را به ازای کاربران مختلف، کوئری استرینگ‌های مختلف و امثال آن انجام داد (با ارائه‌ی محتوای متفاوتی به ازای پارامترهای مختلف):
<cache vary-by-user="true">
    <!--View Component or something that gets data from the database-->
    *last updated @DateTime.Now.ToLongTimeString()
</cache>
در این حالت دیگر نیازی نیست تا نگران این باشیم که آیا محتوای قسمت کش شده‌ی از صفحه برای تمام کاربران در دسترس است یا خیر؟ در اینجا هر کاربر لاگین شده‌ی به سیستم، نگارش کش شده‌ی خاص خودش را دریافت می‌کند.

<cache vary-by-route="id">
    <!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>
در اینجا به ازای پارامتر آی‌دی مسیریابی، نگارش‌های مختلف کش شده‌ای از صفحه تامین می‌شوند. در اینجا می‌توان لیستی از پارامترهای جدا شده‌ی با کاما را مشخص کرد.

<cache vary-by-query="search">
    <!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>
امکان کش کردن محتوای صفحه به ازای کوئری استرینگ‌های مختلف تنظیم شده نیز وجود دارد.

<cache vary-by-cookie="MyAppCookie">
    <!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>
در اینجا به ازای محتواهای مختلف کوکی خاصی به نام MyAppCookie، نگارش‌های مختلف کش شده‌ای از صفحه ذخیره می‌شوند.

 <cache vary-by-header="User-Agent">
    <!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>
در اینجا می‌توان به ازای هدرهای مختلف پروتکل HTTP نگارش‌های کش شده‌ی متفاوتی را ارائه داد.

<cache vary-by="@ViewBag.ProductId">
    <!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>
اگر خواستید کلید کش را خودتان تعیین کنید از vary-by استفاده کنید.

 <cache vary-by-user="true" vary-by-route="id">
    <!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>
امکان ترکیب این موارد با هم نیز وجود دارد.

به علاوه چون زیر ساخت این Tag Helper همان Microsoft.Extensions.Caching.Memory است، امکان تنظیم حق تقدم حذف شدن آیتم‌های کش شده نیز وجود دارد:
<cache expires-sliding="@TimeSpan.FromMinutes(10)"
priority="@Microsoft.Extensions.Caching.Memory.CacheItemPriority.NeverRemove">
    <!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()
</cache>


مبحث تکمیلی

امکان ذخیره سازی آیتم‌های کش شده در بانک اطلاعاتی (بجای حافظه‌ی فرار) نیز پیش بینی شده‌است که تحت عنوان «کش توزیع شده» در دسترس است.
Working with a Distributed Cache
مطالب
نحوه استفاده از TransactionFlow در WCF
شش مرحله برای ایجاد WCFTransactions  در WCF 
 مقدمه و هدف:

هدف از مطلب  فوق اجرا نمودن عملیات Insert، Update و غیرو... بوسیله چندین Connection  در یک Transaction  در زمان اجرای سرویسهای WCF  میباشد. برای پیاده سازی و شرح Transaction ، سه پروژه ایجاد می‌نماییم. دو پروژه WCF  سرویس و یک پروژهClient ، هر سه پروژه را در یک Solution  به نام WCFTransaction  اضافه می‌نماییم. در هر دو پروژه WCF  بطور جداگانه Connection  رویDatabase  ایجاد می‌نماییم. سپس سعی می‌کنیم بوسیله Transaction  عملیات Insert  هر دو Service  را کنترل نماییم. بطوریکه اگر یکی از Service ‌ها در زمان عملیات Insert  دچار مشکل شود. دیگری نیز Commit  نگردد. به عبارتی در قدیم نمی‌توانستیم بیش از یک Connection  در یک Transaction  ایجاد نماییم. اما بوسیله Transactionscope ، انجام عملیات Insert، Update و غیرو...  بوسیله چندین Connection   به یکDatabase  بطور همزمان در یک Transaction  فراهم شده است. برای نمایش دادن عملیات Rollback  نیز،به عمد خطایی ایجاد می‌کنیم،تا نحوه Rollback  شدن در Transaction  را مشاهده نماییم.

سعی شده است پیاده سازی و استفاده از  Transaction در شش مرحله انجام شود.

مرحله اول: ایجاد دو پروژه WCFService و یک پروژه Client جهت فراخوانی (Call) کردن سرویسها

در این مرحله همانطور که از قیل نیز توضیح داده شده است، دو پروژه WCF  به نامهای WCFService1  و WCFService2  ایجاد شده است و یک پروژه Client  به نام WCFTransactions  نیز ایجاد می‌کنیم.

مرحله دوم : افزودن   Attribute ی به نام   TransactionFlow به  Interface سرویسها.

در این مرحله در Interface  هریک از سرویس‌ها متد جدیدی به نام UpdateData  اضافه می‌نماییم. که عملیات Insert into  درون Database  را انجام می‌دهد. حال بالای متد UpdateData   از صفت TransactionFlow  استفاده می‌نماییم. تا قابلیت Transaction  برای متد فوق فعال گردد و متد فوق اجازه می‌یابد از Transaction  استفاده نماید.

<ServiceContract()> _
Public Interface IService1

    <OperationContract()> _
    Function GetData(ByVal value As Integer) As String

    <OperationContract()> _
    Function GetDataUsingDataContract(ByVal composite As CompositeType) As CompositeType

    <OperationContract()> _
    <TransactionFlow(TransactionFlowOption.Allowed)> _
     Sub UpdateData()

End Interface

مرحله سوم:

در این مرحله متد UpdateData  را پیاده سازی می‌نماییم. بطوریکه یک Insert Into  ساده در Database  انجام می‌دهیم.و بالای متد فوق نیز کد زیر را می‌افزاییم.

 <OperationBehavior(TransactionScopeRequired:=True)> 

کد متد UpdateData   

   <OperationBehavior(TransactionScopeRequired:=True)> _
    Public Sub UpdateData() Implements IService1.UpdateData
        Dim objConnection As SqlConnection = New SqlConnection(strConnection)
        objConnection.Open()
        Dim objCommand As SqlCommand = New SqlCommand("insert into T(ID,Age) values(10,10)", objConnection)
        objCommand.ExecuteNonQuery()
        objConnection.Close()
End Sub

مرحله دوم و سوم را برای Service دوم نیز تکرار می‌نماییم.

مرحله چهارم:

در این مرحله  TransactionFlow  را در Web.Config  دو سرویس فعال می‌نماییم. تا قابلیت استفاده از  TransactionFlow   برای سرویسها نیز فعال گردد. نحوه فعال نمودن بصورت زیر میباشد:

برای  WCFService1خواهیم داشت:

<bindings>
                <wsHttpBinding>
                                <binding name="TransactionalBind" transactionFlow="true"/>
                </wsHttpBinding>
</bindings>
و در ادامه داریم:
<endpoint address="" binding="wsHttpBinding" 
bindingConfiguration="TransactionalBind" 
contract="WcfService1.IService1">

برای  WCFService2نیز خواهیم داشت:

<bindings>
                <wsHttpBinding>
                                <binding name="TransactionalBind" transactionFlow="true"/>
                </wsHttpBinding>
</bindings>

و در ادامه داریم:

<endpoint address="" binding="wsHttpBinding" 
bindingConfiguration="TransactionalBind" 
contract="WcfService2.IService1">

مرحله پنجم:

در این مرحله دو سرویس فوق را به پروژه  WCFTransactions  اضافه نموده و قطعه کد زیر را درون فرم Load  می‌نویسیم.

Private Sub frmmain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        Using ts As New TransactionScope(TransactionScopeOption.Required)
            Try
                Dim obj As ServiceReference1.Service1Client = New ServiceReference1.Service1Client()
                obj.UpdateData()
                Dim obj1 As ServiceReference2.Service1Client = New ServiceReference2.Service1Client()
                obj1.UpdateData()
                ts.Complete()

            Catch ex As Exception
                ts.Dispose()
            End Try

        End Using
End Sub


پس از اجرای برنامه دو رکورد در جدول درج خواهد شد.

مرحله ششم:

حال برای RollBack   کردن کل عملیات و مشاهده آنها کافیست در یکی از متدهای UpdateData  یک  Throw Exception  ایجاد نماییم.

سعی می‌کنیم با کمی تغییر در متد UpdateData در WCFService2 ، خطایی ایجاد شود، تا نحوه RollBack را مشاهده نماییم.

Public Sub UpdateData() Implements IService1.UpdateData
        Throw New Exception()
        Dim objConnection As SqlConnection = New SqlConnection(strConnection)
        objConnection.Open()
        Dim objCommand As SqlCommand = New SqlCommand("insert into T(ID,Age) values(101,101)", objConnection)
        objCommand.ExecuteNonQuery()
        objConnection.Close()
End Sub

فقط کد زیر به متد UpdateData اضافه شده است:

Throw New Exception()

و در رویداد Load  فرم نیز پیاده سازی آن بشکل زیر خواهد بود:


Using ts As New TransactionScope(TransactionScopeOption.Required)
            Try
                Dim obj As ServiceReference1.Service1Client = New ServiceReference1.Service1Client()
                obj.UpdateData()
                Throw New Exception("There was Error")
                Dim obj1 As ServiceReference2.Service1Client = New ServiceReference2.Service1Client()
                obj1.UpdateData()
                ts.Complete()

            Catch ex As Exception
                ts.Dispose()
            End Try
 End Using 

وقتی برنامه را اجرا نمایید، مشاهده می‌کنید که هیچ رکوردی دورن دیتابیس درج نشده است.

بسبار مهم: برای اینکه بتوانید بصورت Distibuted  عملیات Transaction  را انجام دهید می‌بایست تنظیماتی را روی سرور که دیتایس و سرویسها و کامپیوتر کلاینت انجام دهید که بصورت زیر می‌باشد:

نحوه تنظیم:

1- سرویسDistribute Transaction Coordinator  را روی هر دو Server‌های WCFService ، Database و کامپیوتر کلاینت، Start می‌نماییم.    

البته در شرایطی که Service‌های WCF و برنامه Client و Database روی یک سیستم باشد، تنظیمات فوق فقط روی همان سیستم انجام می‌شود.

برای دسترسی به قسمت Service ‌های Windows  ابتدا Administrative Tools  و سپس Service   را باز نمایید و روی Start کلیک کنید.

2- در ادامه روی MY Computer کلیک راست نموده و تب MSDTC را انتخاب نمایید:

در ادامه روی Security Configuration  کلیک نمایید. تا فرم زیر نمایش داده شود.


مطمئن شوید که آیتمهای زیر انتخاب شده باشند:

· Network DTC Access

· Allow Remote Clients

· Allow Inbound

· Allow Outbound

· Enable Transaction Internet Protocol(TIP) Transactions 

سپس با OK کردن Service،سرویس بطور خودکار Restart می‌شود.
در ضمن اگر از SQL Server 2000 استفاده می‌نمایید. لازم است تنظیم زیر را انجام دهید.
روی SQL Server Service Manager کلیک نموده و کامبوی Service را Dropdown نمایید و Distribute Transaction Coordinator  را انتخاب کنید. اما برای ورژن‌های بالاتر از SQL Server 2000 نیاز به انتخاب Distribute Transaction Coordinator  نمی‌باشد.
امیدوارم مطلب فوق مفید واقع شود، چنانچه کم و کاستی مشاهده نمودید، اینجانب را از نظرات خود بهره مند سازید.
منبع:
نظرات مطالب
ساخت DropDownList های مرتبط به کمک jQuery Ajax در MVC
با سلام
من از کد زیر استفاده کردم تا موارد تکراری حذف بشه اما dropdown بدون value هست در  هر آیتمش.
ViewBag.city = new SelectList(_db.regions.Select(r => r.region_main).Distinct(), "region_main");
لطفا راهنمایی بفرمایید
با تشکر
مطالب
بهینه سازی کوئری‌های LINQ - بخش اول
یکی از جذاب‌ترین لحظات کار با LINQ و EF زمانی است که به خاطر افزایش حجم دیتا، کوئری خود را بازنگری کرده و آن را بهینه می‌کنید.

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

برای نمونه دو Entity زیر را در مدل EF خود داریم:
public class User
{
   public int ID { get; set; }
   public string Name { get; set; }
   public int Age { get; set; }
}

public class Login
{
   public int ID { get; set; }
   public DateTime Date { get; set; }
   public int UserID { get; set; }
   public User User { get; set; }
}
موجودیت User، اطلاعات کاربر و موجودیت Login، اطلاعات مربوط به لوگین‌های هر کاربر را نگه می‌دارد. برای تست، یک دیتاست را به صورت تصادفی تولید کردیم که حاوی 1200 کاربر و 21000 لوگین هست.

برای تولید اطلاعات تصادفی می‌توان از کد زیر در LINQPad استفاده کرد:
int usersCount = 1200;
Random rnd = new Random();
for(int i=0; i<usersCount; i++)
{
   Users.Add(new User()
     {
       Name = $"User {i + 1}",
       Age = rnd.Next(10, i + 10) / 10
     });
}

SaveChanges();

$"Users: {Users.Count()}".Dump();

var usersID = Users.Select(x => x.ID).ToArray();

int loginsCount  = 20000;

for(int i=0; i<loginsCount; i++)
{
    Logins.Add(new Login()
    {
        UserID = usersID[rnd.Next(0, usersID.Length - 1)],
        Date = DateTime.Now.AddDays(rnd.Next(0, i))
    });

    if(i % 1000 == 0)
   {
      SaveChanges();
      $"Save {i + 1}".Dump();
   }
}

SaveChanges();
$"Logins: {Logins.Count()}".Dump();

$"Users: {Users.Count()}".Dump();
$"Logins: {Logins.Count()}".Dump();

Users: 1200
Logins: 21000

مسئله: نمایش اطلاعات پروفایل هر کاربر، به همراه تاریخ آخرین لوگین و تعداد کل لوگین‌های فرد

در سناریوهای این سبکی، باید خیلی با دقت عمل کرد و از تمام اطلاعات موجود استفاده کرد. اطلاعاتی که در اینجا برای ما مفید است، تعداد نسبی رکوردهای جداول دیتابیس است. مثلا در حال حاضر تعداد رکوردهای Logins تقریبا 17 برابر Users است و در آینده هم رشد Logins چند برابر Users خواهد بود. از طرفی در صورت مسئله، اطلاعات هر کاربر را می‌خواهیم، که به سادگی یک SELECT است. ولی بخش سنگین‌تر کوئری، محاسبه‌ی تعداد لوگین‌ها و تاریخ آخرین لوگین‌های هر فرد است که باز هم به جدول Logins بر می‌گردد.

روش اول:

راه حل اولی که به ذهن می‌رسد، JOIN کردن این دو جدول و محاسبه موارد لازم از ترکیب این دو جدول است:
var data =
(
   from u in Users
   join x in Logins on u.ID equals x.UserID into g
   from x in g.DefaultIfEmpty()
   select new
     {
        UserID = u.ID,
        Name = u.Name,
        Age = u.Age,
        Date = x.Date
     }
);

var result =
(
   from d in data
   group d by d.UserID into g
   select new
   {
       UserID = g.Key,
       Name = g.FirstOrDefault().Name,
       LoginsCount = g.Count(x => x.Date != null),
       LastLogin = g.Max(x => (DateTime?) x.Date) ?? null
   }
);
کد SQL تولید شده‌ی در این روش، ترکیبی از 11 دستور SELECT تو در تو و 4 دستور LEFT OUTER JOIN است که ممکن است در حجم اطلاعات بیشتر، کوئری را با کندی همراه کند. نکته‌ی جالب توجه اینست که دستور group by ما در خروجی ظاهر نشده است و تبدیل به دستور SELECT تو در تو شده است که مورد انتظار ما نبوده است.

Generated SQL
SELECT 
    [Project7].[ID] AS [ID], 
    [Project7].[C2] AS [C1], 
    [Project7].[C3] AS [C2], 
    [Project7].[C1] AS [C3]
    FROM ( SELECT 
        [Project6].[ID] AS [ID], 
        CASE WHEN ([Project6].[C3] IS NULL) THEN CAST(NULL AS datetime2) ELSE [Project6].[C4] END AS [C1], 
        [Project6].[C1] AS [C2], 
        [Project6].[C2] AS [C3]
        FROM ( SELECT 
            [Project5].[ID] AS [ID], 
            [Project5].[C1] AS [C1], 
            [Project5].[C2] AS [C2], 
            [Project5].[C3] AS [C3], 
            (SELECT 
                MAX( CAST( [Extent9].[Date] AS datetime2)) AS [A1]
                FROM  [dbo].[Users] AS [Extent8]
                LEFT OUTER JOIN [dbo].[Logins] AS [Extent9] ON [Extent8].[ID] = [Extent9].[UserID]
                WHERE [Project5].[ID] = [Extent8].[ID]) AS [C4]
            FROM ( SELECT 
                [Project4].[ID] AS [ID], 
                [Project4].[C1] AS [C1], 
                [Project4].[C2] AS [C2], 
                (SELECT 
                    MAX( CAST( [Extent7].[Date] AS datetime2)) AS [A1]
                    FROM  [dbo].[Users] AS [Extent6]
                    LEFT OUTER JOIN [dbo].[Logins] AS [Extent7] ON [Extent6].[ID] = [Extent7].[UserID]
                    WHERE [Project4].[ID] = [Extent6].[ID]) AS [C3]
                FROM ( SELECT 
                    [Project3].[ID] AS [ID], 
                    [Project3].[C1] AS [C1], 
                    (SELECT 
                        COUNT(1) AS [A1]
                        FROM [dbo].[Logins] AS [Extent5]
                        WHERE [Project3].[ID] = [Extent5].[UserID]) AS [C2]
                    FROM ( SELECT 
                        [Distinct1].[ID] AS [ID], 
                        (SELECT TOP (1) 
                            [Extent3].[Name] AS [Name]
                            FROM  [dbo].[Users] AS [Extent3]
                            LEFT OUTER JOIN [dbo].[Logins] AS [Extent4] ON [Extent3].[ID] = [Extent4].[UserID]
                            WHERE [Distinct1].[ID] = [Extent3].[ID]) AS [C1]
                        FROM ( SELECT DISTINCT 
                            [Extent1].[ID] AS [ID]
                            FROM  [dbo].[Users] AS [Extent1]
                            LEFT OUTER JOIN [dbo].[Logins] AS [Extent2] ON [Extent1].[ID] = [Extent2].[UserID]
                        )  AS [Distinct1]
                    )  AS [Project3]
                )  AS [Project4]
            )  AS [Project5]
        )  AS [Project6]
    )  AS [Project7]
    ORDER BY [Project7].[C3] ASC, [Project7].[ID] ASC

روش دوم:
روش دوم اینست که داده‌های سنگین‌تر (اطلاعات Logins) را ابتدا محاسبه کرده و سپس JOIN را انجام دهیم:
var data =
(
  from x in Logins
  group x by x.UserID into g
  orderby g.Key descending
  select new
  {
    UserID = g.Key,
    LoginsCount = g.Count(),
    LastLogin = g.Max(d => d.Date)
  }
);

var result =
(
  from u in Users
  join d in data on u.ID equals d.UserID into g
  from d in g.DefaultIfEmpty()
  select new
  {
    UserID = u.ID,
    LoginsCount = d != null ? d.LoginsCount : 0,
    LastLogin = d != null ? (DateTime?)d.LastLogin : null
  }
);
در روش دوم، ابتدا فقط به Logins کوئری می‌زنیم و برای محاسبه‌ی تعداد لوگین و آخرین لوگین، از Group By استفاده می‌کنیم. استفاده از این دستور باعث می‌شود که محاسبه‌ی سنگین ما در سریعترین حالت ممکن توسط  SQL انجام شود. در مرحله‌ی بعد، این اطلاعات را با جدول Users از طریق LEFT OUTER JOIN ترکیب می‌کنیم. علت استفاده از DefaultIfEmpty بدین سبب است که برخی از کاربران ممکن است تاکنون لوگینی را انجام نداده باشند؛ در نتیجه باید تعداد صفر و تاریخ null برای آنها نمایش داده شود.

اکنون اگر کد SQL روش دوم را بررسی کنیم خواهیم دید که تنها 2 دستور SELECT ، یک LEFT OUTER JOIN به همراه یک GROUP BY تولید شده است که با توجه به ماهیت مسئله و ساختار دیتای ما، این دستورات منطقی‌ترین و بهینه‌ترین دستورات ممکن به نظر می‌رسد.

Generated SQL
SELECT 
    [Project1].[ID] AS [ID], 
    [Project1].[C1] AS [C1], 
    [Project1].[C2] AS [C2]
    FROM ( SELECT 
        [Extent1].[ID] AS [ID], 
        CASE WHEN ([GroupBy1].[K1] IS NOT NULL) THEN [GroupBy1].[A1] ELSE 0 END AS [C1], 
        CASE WHEN ([GroupBy1].[K1] IS NOT NULL) THEN  CAST( [GroupBy1].[A2] AS datetime2) END AS [C2]
        FROM  [dbo].[Users] AS [Extent1]
        LEFT OUTER JOIN  (SELECT 
            [Extent2].[UserID] AS [K1], 
            COUNT(1) AS [A1], 
            MAX([Extent2].[Date]) AS [A2]
            FROM [dbo].[Logins] AS [Extent2]
            GROUP BY [Extent2].[UserID] ) AS [GroupBy1] ON [Extent1].[ID] = [GroupBy1].[K1]
    )  AS [Project1]
    ORDER BY [Project1].[C1] ASC, [Project1].[ID] ASC
پس، همواره کد SQL دستورات LINQ خود را یا از طریق SQL Profiler یا برنامه‌ای مثل LINQPad حتما تست کنید و کوئری خود را در مقابل حجم زیاد اطلاعات هم بررسی کنید. چرا که LINQ به علت سادگی و قدرتی که دارد، گاهی شما را به اشتباه می‌اندازد و باعث می‌شود شما کوئری ای بزنید که جواب شما را می‌دهد، ولی فقط برای حجم کم دیتای کنونی بهینه است و در صورت افزایش رکوردها، یا خیلی کند می‌شود یا کلا شما را با  Timeout مواجه می‌کند.
مطالب
C# 12.0 - Using aliases for any type
دات‌نت 8 به همراه بهبودهای قابل ملاحظه‌ای در کارآیی برنامه‌های دات‌نتی است و در این بین تعدادی قابلیت جدید را نیز به زبان سی‌شارپ اضافه کرده‌است. در این مطلب ویژگی جدید «Alias any type» آن‌را بررسی می‌کنیم. پیشنیاز کار با این قابلیت تنها نصب SDK دات‌نت 8 است.


امکان تعریف alias، قابلیت جدیدی نیست!

در نگارش‌های پیشین زبان #C نیز می‌توان برای نوع‌های نام‌دار دات‌نت، alias/«نام مستعار» تعریف کرد؛ برای مثال:
using MyConsole = System.Console;

MyConsole.WriteLine("Test console");
Aliasها در قسمت using تعاریف یک کلاس معرفی می‌شوند و یکی از اهدف آن‌ها، کوتاه کردن تعاریف فضاهای نام طولانی است و یا رفع تداخل‌ها؛ همچنین تنها به Named types، محدود هستند و Named types فقط شامل این موارد می‌شوند: classes ،delegates ،interfaces ،records و structs

بنابراین دو حالت تعریف Namespace alias برای کوتاه سازی فضاهای نام طولانی و یا تعریف Type alias برای معرفی یک نام مستعار جدید برای نوعی مشخص، میسر است:
// Namespace alias
using SuperJSON = System.Text.Json;
var document = SuperJSON.JsonSerializer.Serialize("{}");

// Type alias
using SuperJSON = System.Text.Json.JsonSerializer;
var document = SuperJSON.Serialize("{}");

تنها کارکرد نام‌های مستعار، کوتاه و زیبا سازی نام‌های طولانی نیستند. برای مثال گاهی از اوقات ممکن است که بین نام نوع‌های موجود در usingهای جاری، تداخل حاصل شود و برنامه کامپایل نشود. برای مثال فرض کنید که دو using زیر را تعریف کرده‌اید:
using UnityEngine;
using System;

Random rnd = new Random();
کامپایل این برنامه میسر نیست. چون هر دو نوع System.Random و UnityEngine.Random پیشتر تعریف شده‌اند و در اینجا دقیقا مشخص نیست که تامین کننده‌ی شیء Random، کدام فضای نام است. در این حالت می‌توان برای مثال در حین نمونه سازی، فضای نام را صراحتا ذکر کرد:
var rnd = new System.Random();
و یا می‌توان برای آن نام مستعاری نیز تعریف کرد:
using Random = System.Random;
در این حالت دیگر تداخلی وجود نداشته و کامپایلر دقیقا می‌داند که تامین کننده‌ی Random، کدام کتابخانه و کدام فضای نام است.


امکان تعریف alias برای هر نوعی در C# 12.0

محدودیت امکان تعریف alias برای نوع‌های نام‌دار دات‌نت در C# 12.0 برطرف شده و اکنون می‌توان برای انواع و اقسام نوع‌ها مانند آرایه‌ها، tuples و غیره نیز alias تعریف کرد:
using Ints = int[];
using DatabaseInt = int?;
using OptionalFloat = float?;
using Grade= decimal;
using Point3D = (int, int, int);
using Person = (string name, int age, string country);
using unsafe P = char*;
using Matrix = int[][];

Matrix aMatrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
در اینجا امکان تعریف alias را برای آرایه‌ها، nullable value types، نوع‌های توکار، tupleها و حتی نوع‌های unsafe، مشاهده می‌کنید.
یک نکته: امکان تعریف alias برای nullable reverence types وجود ندارد.


بررسی یک مثال C# 12.0

در اینجا محتویات یک فایل Program.cs یک برنامه‌ی کنسول دات‌نت 8 را مشاهده می‌کنید:
using MyConsole = System.Console;
using Person = (string name, int age, string country);

Person person = new("User 1", 33, "Iran");
Console.WriteLine(person);
PrintPerson(person);

MyConsole.WriteLine("Test console");

static void PrintPerson(Person person)
{
   MyConsole.WriteLine($"{person.name}, {person.age}, {person.country}");
}
در این مثال برای یک نوع tuple سفارشی، یک alias به نام Person تعریف شده و سپس از آن برای نمونه سازی یک شیء جدید و یا ارسال آن به عنوان یک پارامتر متد، استفاده شده‌است. خروجی برنامه‌ی فوق به صورت زیر است:
(User 1, 33, Iran)
User 1, 33, Iran
Test console


چه زمانی بهتر است از قابلیت تعریف نام‌های مستعار نوع‌ها و یا فضاهای نام استفاده شود؟

اگر یک نام طولانی را بتوان به این صورت خلاصه کرد، مفید هستند؛ برای مثال ساده سازی تعریف یک لیست طولانی به صورت زیر:
using Companies = System.Collections.Generic.List<Company>;

Companies GetCompanies()
{
   // logic here
}

class Company
{
   public string Name;
   public int Id;
}
و مثالی دیگر در این زمینه، کوتاه سازی تعاریف متداول جنریک طولانی است:
using EventHandlers = System.Collections.Generic.IEnumerable<System.Func<System.Threading.Tasks.Task>>;

 و یا اگر بتوانند رفع تداخلی را حاصل کنند، بکارگیری آن‌ها ضروری است (مانند مثال شیء Random ابتدای بحث) و یا اگر بتوانند از تکرار تعریف یک tuple جلوگیری کنند، ذکر آن‌ها یک refactoring مثبت به‌شمار می‌رود؛ مانند مثال زیر که در آن از تعریف نوع tuple ای، دوبار استفاده شده‌است:
using Country = (string Abbreviation, string Name);

Country GetCountry(string abbreviation)
{
   // Logic here
}

List<Country> GetCountries()
{
   // Logic here
}
 اما ... آیا واقعا تعاریفی مانند ذیل، مفید یا ضروری هستند؟
using Ints = int[];
using DatabaseInt = int?;
using OptionalFloat = float?;
اینجا فقط قطعه کدی اضافی را که بیشتر سبب سردرگمی و بالا بردن درجه‌ی پیچیدگی برنامه می‌شود، تولید کرده‌ایم. خوانایی و سادگی درک برنامه در این حالت کاهش پیدا می‌کند.


میدان دید نام‌های مستعار

به صورت پیش‌فرض، تمام نام‌های مستعار تنها در داخل همان فایلی که تعریف شده‌اند، قابل استفاده می‌باشند. از زمان C# 10.0 ، می‌توان پیش از واژه‌ی کلیدی using از واژه‌ی کلیدی global نیز استفاده کرد تا تعریف آن‌ها فقط در پروژه‌ی جاری به صورت سراسری قابل دسترسی شود.
به همین جهت اگر نوعی قرار است در سایر پروژه‌ها استفاده شود، بهتر است از global using استفاده نشده و از همان روش‌های متداول تعریف records و یا classes استفاده شود.
مطالب
ایجاد یک Abstract Factory با استفاده از جنریک‌ها
همان طور که میدانید از الگوی Factory به عنوان روشی برای کاهش وابستگی اجزای یک سیستم استفاده میشود. در این مقاله میخواهیم با استفاده از جنریک‌ها، الگوی Abstract Factory را پیاده سازی کنیم.
1) ایجاد یک کلاس به نام AbstractFactory و یک متد جنریک به نام CreateObject
public class AbstractFactory
    {
        public static T CreateObject<T>() where T : class , new()
        {
            return new T();
        }
    }
2) ساخت کلاسهای مورد نظر
public class Product
    {
        public void DisplayInfo()
        {
            Console.WriteLine("Product Class Craeted. ");
        }
    }

public class Category
    {
        public void DisplayInfo()
        {
            Console.WriteLine("Category Class Created.");
        }
    }

3) حال در یک برنامه‌ی کنسول ویندوز، از کلاس AbstractFactory به شکل زیر استفاده میکنیم
static void Main(string[] args)
        {
            var p = AbstractFactory.CreateObject< Product>();
            p.DisplayInfo();
            Console.WriteLine("======");

            var c = AbstractFactory.CreateObject<Category>();
            c.DisplayInfo();
            Console.WriteLine("======");
            
            Console.ReadKey();
        }
خروجی کد بالا