مطالب
نگاشت خودکار اشیاء توسط AutoMapper و Reflection - ایده شماره 2
پیش نیاز این مطلب، قسمت قبل آن است. در قسمت قبل، یک کلاس جنریک را به نام BaseDto ایجاد کردیم که با ارث بری Dto‌های پروژه از این کلاس، علاوه بر متد‌های ToEntity و FromEntity جهت ساده سازی عملیات نگاشت، Mapping‌های لازم بین Dto‌ها و Entity‌های مربوطه، توسط Reflection به صورت خودکار انجام می‌شد.
در این قسمت می‌خواهیم مکانیزم Mapping خودکار را کمی تغییر داده و قابلیت سفارشی سازی Mapping‌ها را فراهم کنیم. سورس کامل مثال را می‌توانید در این  ریپازیتوری  مشاهده کنید. 
ابتدا یک اینترفیس را به نام IHaveCustomMapping به نحو زیر ایجاد می‌کنیم.
public interface IHaveCustomMapping
{
    void CreateMappings(AutoMapper.Profile profile);
}
هر کلاسی که این اینترفیس را پیاده سازی کند، در متد CreateMappings آن، یک شیء از نوع Profile را دریافت می‌کند و می‌تواند تمامی کانفیگ Mapping‌های دلخواه را اعمال کند.
به عنوان مثال کلاس زیر، Mapping لازم برای PostDto و Post را درون متد CreateMappings خود اعمال می‌کند.
public class PostDtoMapping : IHaveCustomMapping
{
    public void CreateMappings(Profile profile)
    {
        profile.CreateMap<PostDto, Post>().ReverseMap();
    }
}
اکنون لازم است تدبیری بیاندیشیم تا کلاس‌هایی را که از اینترفیس IHaveCustomMapping مشتق شده‌اند، به AutoMapper معرفی کنیم. در واقع باید کلاس‌های مذکور (مانند PostDtoMapping) را یافته، یک وهله از آنها را ایجاد کنیم، سپس متد CreateMappings آنها فراخوانی کرده و شیء ای از نوع Profile را به عنوان ورودی به آن پاس دهیم.
بدین منظور کلاسی را به نام CustomMappingProfile به نحو زیر تعریف می‌کنیم.
public class CustomMappingProfile : Profile
{
    public CustomMappingProfile(IEnumerable<IHaveCustomMapping> haveCustomMappings)
    {
        foreach (var item in haveCustomMappings)
            item.CreateMappings(this);
    }
}
  • این کلاس از AutoMapper.Profile ارث بری کرده‌است.
  • درون سازنده‌ی خود لیستی از اشیاء اینترفیس IHaveCustomMapping را دریافت کرده و بر روی آنها گردش می‌کند.
  • و متد CreateMappings هرکدام را فراخوانی کرده و خودش (this : شی جاری) را (که از نوع Profile شده) به عنوان پارامتر ورودی پاس می‌دهد.
اکنون کلاس AutoMapperConfiguration قسمت قبل را به نحو زیر اصلاح می‌کنیم.
public static class AutoMapperConfiguration
{
    public static void InitializeAutoMapper()
    {
        Mapper.Initialize(config =>
        {
            config.AddCustomMappingProfile();
        });

        //Compile mapping after configuration to boost map speed
        Mapper.Configuration.CompileMappings();
    }

    public static void AddCustomMappingProfile(this IMapperConfigurationExpression config)
    {
        config.AddCustomMappingProfile(Assembly.GetEntryAssembly());
    }

    public static void AddCustomMappingProfile(this IMapperConfigurationExpression config, params Assembly[] assemblies)
    {
        var allTypes = assemblies.SelectMany(a => a.ExportedTypes);

        //Find all classes that implement IHaveCustomMapping inteface and create new instance of each
        var list = allTypes.Where(type => type.IsClass && !type.IsAbstract &&
            type.GetInterfaces().Contains(typeof(IHaveCustomMapping)))
            .Select(type => (IHaveCustomMapping)Activator.CreateInstance(type));

        //Create a new automapper Profile for this list to create mapping then add to the config
        var profile = new CustomMappingProfile(list);
        config.AddProfile(profile);
    }
}
  • توضیحات متد های InitializeAutoMapper و AddCustomMappingProfile، مشابه مطلب قبل است و لازم به ذکر مجدد نیست.
  • متد AddCustomMappingProfile آرایه‌ای از اسمبلی‌ها را دریافت و سپس تمامی نوع‌های قابل دسترس آنها را (ExportedTypes) واکشی می‌کند.
  • سپس توسط شرط Where، نوع‌هایی که کلاس بوده، abstract نیستند و از اینترفیس IHaveCustomMapping مشتق شده‌اند فیلتر می‌شوند. 
  • سپس توسط متد Activator.CreateInstance، وهله‌ای از آنها ایجاد و به نوع IHaveCustomMapping تبدیل می‌شوند و نهایتا لیستی از اشیاء وهله سازی شده را باز می‌گرداند.
  • سپس وهله‌ای از نوع CustomMappingProfile (که مسئول اعمال Mapping‌های اشیاء دریافتی است و قبلا بررسی کردیم) ایجاد می‌کنیم و لیست مذکور را به سازنده آن پاس می‌دهیم.
  • نهایتا profile ساخته شده (حاوی تمامی Mapping‌های اعمال شده) را توسط متد config.AddProfile به AutoMapper معرفی می‌کنیم (در این لحظه تمامی Mapping‌های تعریف شده داخل profile، به AutoMapper اعمال می‌شوند).
توسط این مکانیزم، هر کلاسی که اینترفیس IHaveCustomMapping را پیاده سازی کرده باشد، به صورت خودکار یافت شده و Mapping به آنها اعمال می‌شود. حال می‌توان این مکانیزم را با BaseDto قسمت قبل ترکیب کرده و کلاس BaseDto را به نحو زیر اصلاح کنیم.
public abstract class BaseDto<TDto, TEntity, TKey> : IHaveCustomMapping
        where TEntity : BaseEntity<TKey>
{
    [Display(Name = "ردیف")]
    public TKey Id { get; set; }

    /// <summary>
    /// Maps this dto to a new entity object.
    /// </summary>
    public TEntity ToEntity()
    {
        return Mapper.Map<TEntity>(CastToDerivedClass(this));
    }

    /// <summary>
    /// Maps this dto to an exist entity object.
    /// </summary>
    public TEntity ToEntity(TEntity entity)
    {
        return Mapper.Map(CastToDerivedClass(this), entity);
    }

    /// <summary>
    /// Maps the specified entity to a new dto object.
    /// </summary>
    public static TDto FromEntity(TEntity model)
    {
        return Mapper.Map<TDto>(model);
    }

    protected TDto CastToDerivedClass(BaseDto<TDto, TEntity, TKey> baseInstance)
    {
        return Mapper.Map<TDto>(baseInstance);
    }

    //Get automapper Profile then create mapping and ignore unmapped properties
    public void CreateMappings(Profile profile)
    {
        var mappingExpression = profile.CreateMap<TDto, TEntity>();

        var dtoType = typeof(TDto);
        var entityType = typeof(TEntity);

        //Ignore mapping to any property of source (like Post.Categroy) that dose not contains in destination (like PostDto)
        //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value
        foreach (var property in entityType.GetProperties())
        {
            if (dtoType.GetProperty(property.Name) == null)
                mappingExpression.ForMember(property.Name, opt => opt.Ignore());
        }

        //Pass mapping expressin to customize mapping in concrete class
        CustomMappings(mappingExpression.ReverseMap());
    }

    //Concrete class can override this method to customize mapping
    public virtual void CustomMappings(IMappingExpression<TEntity, TDto> mapping)
    {
    }
}
  • کلاس جنریک BaseDto، متدCreateMappings اینترفیس IHaveCustomMapping را پیاده سازی می‌کند.
  • درون این متد، Mapping بین دو نوع TDto و TEntity، توسط ()<profile.CreateMap<TDto, TEntity کانفیگ می‌شود.
  • مانند مطلب قبل، خواصی را که نباید نگاشت شوند، توسط Reflection یافته و Ignore می‌کنیم.
  • سپس Mapping برعکس را توسط ReverseMap اعمال کرده و به متد زیرین آن که virtual نیز است، پاس می‌دهیم.
متد CustomMappings ای که به صورت virtual تعریف شده‌است، این امکان را به ما می‌دهد که در کلاس‌هایی که از BaseDto ارث بری می‌کنند، در صورت لزوم آن را بازنویسی (override) کرده و سفارشی سازی دلخواه‌مان را بر روی Mapping دریافتی اعمال کنیم.
مثال: کلاس PostDto زیر از BaseDto ارث بری کرده و چون سفارشی سازی‌ای لازم دارد، متد CustomMappings والد خود را override کرده است.
public class PostDto : BaseDto<PostDto, Post, long>
{
    public string Title { get; set; }
    public string Text { get; set; }
    public int CategoryId { get; set; }

    public string CategoryName { get; set; } //=> Category.Name
    public string FullTitle { get; set; } //=> custom mapping for "Title (Category.Name)"
        
    public override void CustomMappings(IMappingExpression<Post, PostDto> mapping)
    {
        mapping.ForMember(
                dest => dest.FullTitle,
                config => config.MapFrom(src => $"{src.Title} ({src.Category.Name})"));
    }
}
  • این کلاس، خاصیتی به نام FullTitle دارد که معادلی (خاصیت همنامی) در کلاس Post برای آن وجود ندارد و قرار است مقدار ترکیبی حاصل از Title و Category.Name را نمایش دهد. 
  • به همین جهت متد CustomMappings را باز نویسی کرده، شیء mapping را دریافت و سفارشی سازی لازم را روی آن انجام داده‌ایم.
  • توسط متد ForMember مشخص کرده‌ایم که مقدار خاصیت FullTitle باید حاصلی از ترکیب Title و Category.Name به نحو مشخص شده باشد ( توسط متد MapFrom).
پس در این روش علاوه بر امکانات BaseDto و Mapping خودکار، امکان سفارشی سازی دلخواه را نیز خواهیم داشت.
برای کوئری گرفتن از دیتابیس نیز و تبدیل آنها به لیستی از Dto‌ها می‌توان از متد ProjectTo بر روی IQueryable استفاده کرد و حتی شرط Where را بر روی کوئری Dto‌ها اعمال کرد مانند زیر:
List<PostDto> list =
    //ProjectTo method select only needed properties (of PostDto) not all properties
    //Also select only needed property of navigations (like Post.Category.Name) not all unlike Include
    //This ability called "Projection"
    await _applicationDbContext.Posts.ProjectTo<PostDto>()
    //We can also use Where on IQuerable<PostDto>
    .Where(p => p.Title.Contains("test") || p.CategoryName.Contains("test"))
    .ToListAsync();
  • متد ProjectTo کوئری post را به IQueryable ای از postDto تبدیل می‌کند (این قابلیت Projection نامیده می‌شود).
  • نگاشت خودکار خواص موجود در postDto توسط AutoMapper به صورت خودکار انجام می‌شود و فقط خواص لازم برای postDto واکشی می‌شوند (نه همه خواص در جدول post، که این به لحاظ کارآیی بهتر است).
  • همچنین اگر خواصی را داخل Navigation Property‌ها مانند CategoryName داشته باشیم، موقع کوئری گرفتن از دیتابیس، آنها نیز اعمال شده و فقط خواص لازم از Category واکشی می‌شوند (فقط خاصیت Name، بر خلاف Include که همه ستون‌ها را واکشی می‌کند).
  • همچنین می‌توان بر روی خواص Dto شرط Where را قرار داد مانند p.CategoryName.Contains("test") و تماما به کوئری SQL معادل آن ترجمه و اجرا می‌شوند.
مطالب دوره‌ها
تزریق وابستگی‌ها در فیلترهای ASP.NET MVC
فرض کنید فیلتر سفارشی لاگ کردن را که از سرویس ILogActionService استفاده می‌کند، به نحو ذیل تعریف کرده‌اید:
public interface ILogActionService
{
    void Log(string data);
}

public class LogAttribute : ActionFilterAttribute
{
    public ILogActionService LogActionService { get; set; }
 
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        LogActionService.Log("......data......");
        base.OnActionExecuted(filterContext);
    }
}
با استفاده‌ای مانند:
 [Log]
public ActionResult Index()
{}

روش متداول تنظیمات تزریق وابستگی‌ها در ASP.NET MVC، بیشتر به بحث کنترلرها مرتبط است و سایر قسمت‌ها را پوشش نمی‌دهد. برای این مورد خاص ابتدا نیاز است یک FilterProvider سفارشی را به نحو ذیل تدارک دید:
using StructureMap;
using System.Collections.Generic;
using System.Web.Mvc;
 
namespace DI06.CustomFilters
{
    public class StructureMapFilterProvider : FilterAttributeFilterProvider
    {
        private readonly IContainer _container;
        public StructureMapFilterProvider(IContainer container)
        {
            _container = container;
        }
 
        public override IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
        {
            var filters = base.GetFilters(controllerContext, actionDescriptor);
            foreach (var filter in filters)
            {
                _container.BuildUp(filter.Instance);
                yield return filter;
            }
        }
    }
}
نکته‌ی مهم آن، استفاده از متد BuildUp استراکچرمپ است. نمونه‌ی آن‌را در تنظیمات تزریق وابستگی‌ها در وب فرم‌ها پیشتر ملاحظه کرده‌اید. در این مثال کار آن وهله سازی وابستگی‌های فیلترهای تعریف شده در برنامه است.
پس از اینکه FilterProvider سفارشی مخصوص کار با استراکچرمپ را تهیه کردیم، اکنون نوبت به جایگزین کردن آن با FilterProvider پیش فرض ASP.NET MVC در فایل global.asax.cs به نحو ذیل است:
 //Using the custom StructureMapFilterProvider
var filterProvider = FilterProviders.Providers.Single(provider => provider is FilterAttributeFilterProvider);
FilterProviders.Providers.Remove(filterProvider);
FilterProviders.Providers.Add(SmObjectFactory.Container.GetInstance<StructureMapFilterProvider>());
استفاده از SmObjectFactory.Container.GetInstance سبب خواهد شد تا به صورت خودکار، وابستگی تزریق شده‌ی در سازنده‌ی کلاس StructureMapFilterProvider وهله سازی و تامین شود.
همچنین در این مثال چون تزریق وابستگی در کلاس LogAttribute از نوع setter injection است، نیاز است در تنظیمات ابتدایی Container مورد استفاده، Policies.SetAllProperties نیز قید شود:
namespace DI06.IocConfig
{
    public static class SmObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }
 
        private static Container defaultContainer()
        {
            return new Container(x =>
            {
                x.For<ILogActionService>().Use<LogActionService>();
 
                x.Policies.SetAllProperties(y =>
                {
                    y.OfType<ILogActionService>();
                });
            });
        }
    }
}


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
DI06
نظرات مطالب
رمزنگاری JWT و افزایش امنیت آن در ASP.NET Core
یک نکته‌ی تکمیلی: نکته امنیتی در هنگام استفاده از توکن ها

هنگامیکه کاربری اطلاعات خود را ویرایش میکند، معمولا یک ویوو مدل را از ورودی دریافت میکنیم و داده‌های آن کاربر را بر اساس آی‌دی که درون ویوو مدل ارسال شده‌است، ویرایش میکنیم. اما در این حالت کاربر میتواند با تغییر آیدی ارسالی در ویوو مدل، اطلاعات سایر کاربران را نیز تغییر دهد! برای جلوگیری از این کار میتوان به روش زیر عمل کرد. ابتدا در هنگام ساخت توکن، آیدی کاربر و یک امضا Signature (میتوان از یک GUID استفاده کرد) را در توکن نگهداری میکنیم و سپس توکن را به روش JWE رمزنگاری میکنیم تا اطلاعات توکن قابل مشاهده نباشد و در هنگام اعتبارسنجی توکن، امضای کاربر را با امضای درون توکن مقایسه میکنیم. اگر با هم تفاوت داشته باشند، به معنای آن است که توکن منقضی شده و قابل استفاده نیست. در هربار که کاربر درخواست توکنی را میدهد، باید امضای کاربر را تغییر داده و یک امضای جدید را برای او ثبت کنیم.
سپس در هنگام اجرای اکشن مورد نظر، آیدی درون توکن و آیدی ارسالی جهت ویرایش اطلاعات را بررسی خواهیم کرد. اگر این دو با هم همخوانی نداشته باشند، اجازه‌ی اجرای اکشن مورد نظر را به او نخواهیم داد و سپس امضای کاربر را تغییر میدهیم تا توکن منقضی شود و یک استثناء را صادر میکنیم.
برای پیاده سازی، ابتدا یک کلاس را برای بررسی مشخصات توکن و آیدی ارسالی میسازیم.
    public interface IJwtService
    {
        Task CheckId(int id, ClaimsPrincipal claimsPrincipal);
    }
    public class JwtService : IJwtService
    {
        private readonly IUserService _userService;

        public JwtService(IUserService userService)
        {
            _userService = userService;
        }

        public async Task CheckId(int id, ClaimsPrincipal claimsPrincipal)
        {
            var jwtId = Convert.ToInt32(claimsPrincipal.Identity.FindFirstValue(ClaimTypes.NameIdentifier));
            if (jwtId != id)
            {
                var user = _userService.GetById(jwtId);
                user.SecurityStamp = Guid.NewGuid();
                await _userService.UpdateAsync(user);
                throw new Exception("You are unauthorized to access this resource.");
            }
        }
    }
و نحوه‌ی استفاده‌ی از آن در کنترلر و اکشن مورد نظر:
private readonly IJwtService _jwtService;
private readonly IUserService _userService;
public UserController(IJwtService jwtService, IUserService userService)
        {
            _jwtService = jwtService;
            _userService = userService;
        }

        [HttpPut("Update")]
        public async Task<IActionResult> Update(UserEditViewModel editViewModel, CancellationToken cancellationToken)
        {
            _jwtService.CheckId(editViewModel.Id, HttpContext.User);
            await _userService.Update(editViewModel, cancellationToken);
        }
مطالب
Lambda Syntax و کارآیی
در این مطلب می‌خواهیم کارآیی event handlers پیاده سازی شده با روش‌های متفاوتی را مورد بررسی قراردهیم.
به مثال زیر توجه کنید:
class EventSource : System.Progress<int>
{
    public async System.Threading.Tasks.Task<int> PerformExpensiveCalculation()
    {
        var sum = 0;
        for (var i = 0; i < 100; i++)
        {
            await System.Threading.Tasks.Task.Delay(100);
            sum += i;
            this.OnReport(sum);
        }
        return sum;
    }
}

static class Program
{
    static void Main(string[] args)
    {
        var source = new EventSource();
        System.EventHandler<int> handler = (_, progress) => System.Console.WriteLine(progress);
        source.ProgressChanged += handler;
        System.Console.WriteLine(source.PerformExpensiveCalculation().Result);
        source.ProgressChanged -= handler;

        source.ProgressChanged += ProgressChangedMethod;
        System.Console.WriteLine(source.PerformExpensiveCalculation().Result);
        source.ProgressChanged -= ProgressChangedMethod;
    }

    private static void ProgressChangedMethod( object sender, int e )
    {
        System.Console.WriteLine(e);
    }
}
در مثال بالا دو نسخه‌ی مختلف از event handler را با دو روش، (روش اول) با استفاده از Lambda syntax و (روش دوم) با استفاده از یک متد، به صورت جدا تعریف شده، پیاده سازی کرده‌ایم.
خوب؛ برای اندازه گیری کارآیی این دو روش باید کمی فکر کنیم که چه چیزی کارآیی این دو روش را تغییر می‌دهد؟
آیا پردازش event با اضافه کردن و حذف کردن event handler؟ و یا پردازش درون event باعث تغییر در کارآیی می‌شود؟
این، سوال مهمی در تست کارآیی این دو روش مختلف است. اگر پردازش درون event باعث ایجاد تفاوت کارآیی می‌شود، با استفاده از این برنامه می‌توان آن را اندازه گیری کرد. با این حال اگر تفاوت کارآیی با اضافه کردن و حذف کردن event handler اتفاق می‌افتد، با این برنامه بعید است بتوان این روش را تست کرد چرا که فقط یکبار این عمل انجام می‌شود.
قبل از شروع به اندازه گیری کارآیی این دو روش‌، اجازه بدهید ابتدا به کد IL آن‌ها نگاهی کنیم. (روش اول با استفاده از Lambda syntax)
IL_0007:  ldsfld     class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__0_0'
IL_000c:  dup
IL_000d:  brtrue.s   IL_0026
IL_000f:  pop
IL_0010:  ldsfld     class LambdaPerformance.Program/'<>c' LambdaPerformance.Program/'<>c'::'<>9'
IL_0015:  ldftn      instance void LambdaPerformance.Program/'<>c'::'<Main>b__0_0'(object, int32)
IL_001b:  newobj     instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int)
IL_0020:  dup
IL_0021:  stsfld     class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__0_0'
IL_0026:  stloc.1
IL_0027:  ldloc.0
IL_0028:  ldloc.1
IL_0029:  callvirt   instance void class [mscorlib]System.Progress`1<int32>::add_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>)
IL_002e:  nop
IL_002f:  ldloc.0
IL_0030:  callvirt   instance class [mscorlib]System.Threading.Tasks.Task`1<int32> LambdaPerformance.EventSource::PerformExpensiveCalculation()
IL_0035:  callvirt   instance !0 class [mscorlib]System.Threading.Tasks.Task`1<int32>::get_Result()
IL_003a:  call       void [mscorlib]System.Console::WriteLine(int32)
IL_003f:  nop
IL_0040:  ldloc.0
IL_0041:  ldloc.1
IL_0042:  callvirt   instance void class [mscorlib]System.Progress`1<int32>::remove_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>)
در بالا 5 دستورالعمل برای اضافه کردن event handler وجود دارد (از IL_0010 تا IL_0029) و یک دستور برای حذف handler وجود دارد (IL_0042).
قبل از شروع مقایسه، کد IL روش دوم را نیز بررسی می‌کنیم:
IL_004a:  ldftn      void LambdaPerformance.Program::ProgressChangedMethod(object, int32)
IL_0050:  newobj     instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int)
IL_0055:  callvirt   instance void class [mscorlib]System.Progress`1<int32>::add_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>)
IL_005a:  nop
IL_005b:  ldloc.0
IL_005c:  callvirt   instance class [mscorlib]System.Threading.Tasks.Task`1<int32> LambdaPerformance.EventSource::PerformExpensiveCalculation()
IL_0061:  callvirt   instance !0 class [mscorlib]System.Threading.Tasks.Task`1<int32>::get_Result()
IL_0066:  call       void [mscorlib]System.Console::WriteLine(int32)
IL_006b:  nop
IL_006c:  ldloc.0
IL_006d:  ldnull
IL_006e:  ldftn      void LambdaPerformance.Program::ProgressChangedMethod(object, int32)
IL_0074:  newobj     instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int)
IL_0079:  callvirt   instance void class [mscorlib]System.Progress`1<int32>::remove_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>)
همانطور که مشاهده می‌کنید در روش دوم برای اضافه کردن event handler تنها 3 خط وجود دارند (IL_004a تا IL_0055) و برای حذف کردن آن نیز 3 خط وجود دارند (IL_006e تا IL_0079).

برای اندازه گیری دقیق، برنامه‌ی بالا را کمی تغییر می‌دهیم. ما میزان اضافه و حذف شدن event handler را می‌خواهیم اندازه‌گیری کنیم و کاری به زمان اجرای یک عملیات نداریم. بنابراین فراخوانی ()PerformExpensiveCalculation را comment کرده و به صورت خیلی ساده فقط handler را اضافه و حذف می‌کنیم.
static class Program
{
    static void Main(string[] args)
    {
        for (var repeats = 10; repeats <= 1000000; repeats *= 10)
        {
            VersionOne(repeats);
            VersionTwo(repeats);
        }
    }

    private static void VersionOne(int repeats)
    {
        var timer = new System.Diagnostics.Stopwatch();
        timer.Start();

        var source = new EventSource();
        for (var i = 0; i < repeats; i++)
        {
            System.EventHandler<int> handler = (_, progress) => System.Console.WriteLine(progress);
            source.ProgressChanged += handler;
            // Console.WriteLine(source.PerformExpensiveCalculation().Result);
            source.ProgressChanged -= handler;
        }

        timer.Stop();

        System.Console.WriteLine($"Version one: {repeats} add/remove takes {timer.ElapsedMilliseconds}ms");
    }

    private static void VersionTwo(int repeats)
    {
        var timer = new System.Diagnostics.Stopwatch();
        timer.Start();

        var source = new EventSource();
        for (var i = 0; i < repeats; i++)
        {
            source.ProgressChanged += ProgressChangedMethod;
            // Console.WriteLine(source.PerformExpensiveCalculation().Result);
            source.ProgressChanged -= ProgressChangedMethod;
        }

        timer.Stop();

        System.Console.WriteLine($"Version two: {repeats} add/remove takes {timer.ElapsedMilliseconds}ms");
    }

    private static void ProgressChangedMethod(object sender, int e)
    {
        System.Console.WriteLine(e);
    }
}
و چنین خروجی را تولید می‌کند (البته نسبت به سرعت CPU این زمان‌ها متفاوت خواهد بود)
Version one: 10 add/remove takes 0ms
Version two: 10 add/remove takes 0ms
Version one: 100 add/remove takes 0ms
Version two: 100 add/remove takes 0ms
Version one: 1000 add/remove takes 0ms
Version two: 1000 add/remove takes 0ms
Version one: 10000 add/remove takes 0ms
Version two: 10000 add/remove takes 1ms
Version one: 100000 add/remove takes 8ms
Version two: 100000 add/remove takes 13ms
Version one: 1000000 add/remove takes 93ms
Version two: 1000000 add/remove takes 121ms
خوب؛ اگر در یک اجرای برنامه، شما یک میلیون بار event handler را اضافه و حذف کنید، 28ms می‌توانید صرفه جویی کنید (در روش اول).

توجه:
اگر در برنامه‌ی شما یک میلیون بار event handler اضافه و حذف می‌شوند، نیاز به بازنگری مجدد در طراحی کلی برنامه تان دارد.

یک اشتباه بزرگ

با ایجاد یک تغییر در روش اول (Lambda syntax)، ممکن است تاثیر بسیار زیادی را در عملکرد برنامه مشاهده کنید:
private static void VersionOne(int repeats)
{
    var timer = new System.Diagnostics.Stopwatch();
    timer.Start();

    var source = new EventSource();
    for (var i = 0; i < repeats; i++)
    {
        // System.EventHandler<int> handler = (_, progress) => System.Console.WriteLine(progress);
        source.ProgressChanged += (_, progress) => System.Console.WriteLine(progress);
        // Console.WriteLine(source.PerformExpensiveCalculation().Result);
        source.ProgressChanged -= (_, progress) => System.Console.WriteLine(progress);
    }

    timer.Stop();

    System.Console.WriteLine($"Version one: {repeats} add/remove takes {timer.ElapsedMilliseconds}ms");
}
به جای تعریف یک متغیر محلی برای عبارت Lambda، دستور اضافه و حذف کردن event handler را به صورت inline استفاده کردیم. خروجی این روش به صورت زیر می‌شود:
Version one: 10 add/remove takes 0ms
Version two: 10 add/remove takes 0ms
Version one: 100 add/remove takes 1ms
Version two: 100 add/remove takes 0ms
Version one: 1000 add/remove takes 102ms
Version two: 1000 add/remove takes 0ms
Version one: 10000 add/remove takes 10509ms
Version two: 10000 add/remove takes 1ms
Version one: 100000 add/remove takes 1039014ms
Version two: 100000 add/remove takes 11ms
همانطور که مشاهده می‌کنید، روش اول خیلی خیلی آهسته است. توجه کنید من بعد از یکصد هزار بار اضافه و حذف کردن handler، به دلیل طولانی شدن، عملیات را قطع کردم. خب دلیل این همه کندی چیست؟ بیایید نگاهی به کد IL درون حلقه‌ی روش اول بیاندازیم.
  IL_0018:  nop
  IL_0019:  ldloc.1
  IL_001a:  ldsfld     class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__1_0'
  IL_001f:  dup
  IL_0020:  brtrue.s   IL_0039
  IL_0022:  pop
  IL_0023:  ldsfld     class LambdaPerformance.Program/'<>c' LambdaPerformance.Program/'<>c'::'<>9'
  IL_0028:  ldftn      instance void LambdaPerformance.Program/'<>c'::'<VersionOne>b__1_0'(object, int32)
  IL_002e:  newobj     instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int)
  IL_0033:  dup
  IL_0034:  stsfld     class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__1_0'
  IL_0039:  callvirt   instance void class [mscorlib]System.Progress`1<int32>::add_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>)
  IL_003e:  nop
  IL_003f:  ldloc.1
  IL_0040:  ldsfld     class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__1_1'
  IL_0045:  dup
  IL_0046:  brtrue.s   IL_005f
  IL_0048:  pop
  IL_0049:  ldsfld     class LambdaPerformance.Program/'<>c' LambdaPerformance.Program/'<>c'::'<>9'
  IL_004e:  ldftn      instance void LambdaPerformance.Program/'<>c'::'<VersionOne>b__1_1'(object, int32)
  IL_0054:  newobj     instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int)
  IL_0059:  dup
  IL_005a:  stsfld     class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__1_1'
  IL_005f:  callvirt   instance void class [mscorlib]System.Progress`1<int32>::remove_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>)
  IL_0064:  nop
  IL_0065:  nop
  IL_0066:  ldloc.2
  IL_0067:  stloc.3
  IL_0068:  ldloc.3
  IL_0069:  ldc.i4.1
  IL_006a:  add
  IL_006b:  stloc.2
  IL_006c:  ldloc.2
  IL_006d:  ldarg.0
  IL_006e:  clt
  IL_0070:  stloc.s    V_4
  IL_0072:  ldloc.s    V_4
  IL_0074:  brtrue.s   IL_0018
به خطهای ( IL_0028 و IL_0034 و IL_004e و IL_005a ) در کد بالا دقت کنید. توجه داشته باشید که event handler اضافه شده با event handler حذف شده، با هم متفاوت هستند. حذف کردن event handler ای که به جایی متصل نیست باعث ایجاد خطا نمی‌شود ولی کاری هم انجام نمی‌دهد. بنابراین اتفاقی که در روش اول درون حلقه می‌افتد این است که بیش از یک میلیون بار event handler به delegate اضافه می‌شود. همه‌ی آنها یکسان هستند؛ اما همچنان CPU و حافظه مصرف می‌کنند.

ممکن است شما به این نتیجه رسیده باشید که استفاده از Lambda syntax برای اضافه و حذف کردن event handler آهسته‌تر از، استفاده از متد جدا است، این یک اشتباه بزرگ است. در صورتی که شما اضافه و حذف کردن event handler را با استفاده از Lambda syntax به شکل صحیح انجام ندهید، به سرعت، در معیارهای کارآیی خود را نشان می‌دهد.

دانلود برنامه بالا
مطالب
امکان معرفی نوع‌های محدود به یک فایل در C# 11
در C# 11 ارائه‌ی شده‌ی به همراه NET 7.0.، واژه‌ی کلیدی جدید file، جهت تعریف نوع‌های محدود به یک فایل «File Scoped Types» ارائه شده‌است. این واژه‌ی کلیدی را می‌توان به تعریف هر نوع دلخواهی مانند class, interface, record, struct, enum, delegate اضافه کرد (منهای خواص، فیلدها و رخدادها؛ البته تا C# 11)، تا آن نوع، دیگر در سایر کلاس‌های فایل‌های برنامه، قابل دسترسی نباشد و سطح دید استفاده‌ی از آن، تنها محدود به فایل جاری محل قرار گیری آن شود. به این ترتیب می‌توان در یک فضای نام مشخص، چندین کلاس هم‌نام را تعریف کرد؛ کاری که در نگارش‌های پیشین #C، میسر نبود. بدیهی دیگر نمی‌توان یک چنین نوع‌هایی را با سطوح دسترسی متداول internal و یا  public، تعریف و ترکیب کرد.


یک مثال: نمونه‌ای از نحوه‌ی تعریف و استفاده‌ی از File Scoped Types

فرض کنید دو فایل جدید را به نام‌های File1.cs و File2.cs به پروژه‌ی جاری اضافه کرده‌ایم.
محتوای فایل File1.cs به صورت زیر است:
namespace CS11Tests;

file static class Post
{
    public static string GetTitle() => "Title from File1.cs";
}

internal static class InternalClassFromFile1
{
    public static string GetTitle() => Post.GetTitle();
}
و محتوای فایل File2.cs به نحو زیر تعریف شده‌است:
namespace CS11Tests;

file static class Post
{
    public static string GetTitle() => "Title from File2.cs";
}

internal static class InternalClassFromFile2
{
    public static string GetTitle() => Post.GetTitle();
}
اگر دقت کنید، ذیل فضای نام مشخص و ثابت CS11Tests، دو کلاس هم نام Post را داریم که اینبار با واژه‌ی کلیدی file، شروع شده‌اند و میدان دید دسترسی به آن‌ها، محدود به همان فایل دربرگیرنده‌ی آن‌ها است و در سایر قسمت‌های برنامه قابل دسترسی نیستند. اگر خواستیم به‌نحوی از آن‌ها در سایر قسمت‌های برنامه نیز استفاده کنیم، مانند فایل Program.cs، می‌توان یک تعریف متداول internal/public را مانند کلاس‌های internal تعریف شده، ایجاد کرد و سپس به صورت «غیرمستقیمی» به آن‌ها دسترسی یافت:
using System.Security.AccessControl;
using CS11Tests;
using static System.Console;

WriteLine(InternalClassFromFile1.GetTitle());
WriteLine(InternalClassFromFile2.GetTitle());

امکان partial تعریف کردن نوع‌های محدود به یک فایل در C# 11

در اینجا می‌توان نوع‌های محدود به یک فایل را partial نیز تعریف کرد؛ به شرطی که تمام تعاریف آن‌ها داخل همان فایل قرار گیرند:
namespace CS11Tests;

file static partial class Post
{
    internal static string GetFileScopeTitle() => "Title from File3.cs";
}

file static partial class Post
{
    internal static string AnotherGetFileScopeTitle() => "Another Title from File3.cs";
}

یک سؤال: اگر در یک فایل، file class Post و در فایلی دیگر، کلاس هم نام داخلی internal class Post را تعریف کردیم، آیا می‌توان از نمونه‌ی هم‌نام internal، در کلاس file دار استفاده کرد؟
پاسخ:
خیر!
فرض کنید در File4.cs چنین تعریفی را داریم:
namespace CS11Tests;

internal static class Post
{
    public static string GetTitle() => "Title from File4.cs";
}
در اینجا در فضای نام مشخصی، کلاس Post، به صورت internal تعریف شده‌است. اکنون در File3.cs، مجدد تعریف کلاس هم‌نام Post را اینبار به صورت file داریم:
namespace CS11Tests;

file static class Post
{
    internal static string GetFileScopeTitle() => CS11Tests.Post.GetTitle() + "Title from File3.cs";
}
این قطعه کد کامپایل نمی‌شود. چون Post ای که در اینجا قابل استفاده‌است، دقیقا همان کلاس Post جاری این فایل است و نه نمونه‌ی هم‌نام internal در فایلی دیگر.


خروجی کامپایلر C# 11 در مورد سطح دسترسی file

کامپایلر C# 11 جهت جلوگیری از تداخل نام‌های حاصل از تعریف کلاس‌های با سطح دسترسی file، از قالب زیر:
<SourceFileNameWithoutExtension>F$index$_TypeName
برای نامگذاری نهایی اینگونه نوع‌ها استفاده می‌کند؛ مانند مثال زیر که مرتبط با کلاس Post تعریف شده‌ی در فایل File1.cs است:
internal static class <File1>F3A5590C89B71B2DB20A548228781187A11D076C0CC91E851A4EE796FFE808F8F__Post
{
    public static string GetTitle()
    {
        return "Title from File1.cs";
    }
}
Index منحصربفرد استفاده شده، مشکل تداخل نام‌ها را برطرف می‌کند و به علت وجود <> در تعریف این نام‌های ویژه، امکان استفاده‌ی از آن‌ها در سایر قسمت‌ها و فایل‌های برنامه وجود ندارد.
تاکنون از این روش نامگذاری ویژه، در موارد دیگری مانند async/await , lambda, anonymous method, anonymous types نیز استفاده شده‌است.

چرا قابلیت «File Scoped Types» به زبان C# 11 اضافه شده‌است؟

- جهت کدهای تولیدی توسط ابزارها: گاهی از اوقات، تولید کننده‌های کد، از یک نام مشخص مانند DataSet، بارها و بارها استفاده می‌کنند. برای جلوگیری از تداخل این‌ها، عموما از تعریف تو در توی کلاس‌ها استفاده می‌شود و یا نام آن‌ها را با ایندکس‌هایی مانند DateSet1، DateSet2 و امثال آن‌ها مشخص می‌کنند. وجود واژه‌ی کلیدی file، کار ابزارهای تولید کننده‌ی کد را ساده‌تر می‌کند.
- برای ساده سازی تعریف متدهای الحاقی: با استفاده از سطح دسترسی فایل می‌توان از تداخل متدهای الحاقی هم نام و همچنین شلوغ شدن intellisense جلوگیری کرد. به این ترتیب می‌توان کلاس‌های حاوی Extension method مختص به یک فایل را ایجاد کرد که در سایر قسمت‌های برنامه قابل دسترسی نباشند.
- کاهش تعریف کلاس‌های تو در تو: همانطور که عنوان شد، یکی از روش‌های مقابله‌ی با مشکل تعریف کلاس‌های هم نام در یک فضای نام مشخص، تعریف nested classes است. با ارائه‌ی واژه‌ی کلیدی file، می‌توان یک سطح فرو رفتگی تعریف کلاس‌ها را کاهش داد و به کدهای تمیزتری رسید.
- امکان کپسوله سازی‌های بهتر: عموما کامپوننت‌ها و ماژول‌ها، از چند کلاس تشکیل می‌شوند. با وجود واژه‌ی کلیدی file، می‌توان به سطح بالاتری از خصوصی سازی نوع‌ها، بدون نیاز به تعریف نوع‌های private و یا nested private رسید.
- سهولت نوشتن کلاس‌های آزمون‌های واحد: عموما هر کلاس آزمون، از نوع‌ها و داده‌های خاص خودش استفاده می‌کنند و در اینجا می‌توان سطح دسترسی این تعاریف را بسیار محدود و مختص به همان فایل Test کرد.
مطالب
ارسال ایمیل در ASP.NET Core
فضای نام System.Net.Mail در NET Core 1.2. که پیاده سازی netstandard2.0 است، ارائه خواهد شد. بنابراین فعلا (در زمان NET Core 1.1.) راه حل توکار و رسمی برای ارسال ایمیل در برنامه‌های مبتنی بر NET Core. وجود ندارد. اما می‌توان کتابخانه‌ی ثالثی را به نام MailKit، به عنوان راه‌حلی که .NET 4.0, .NET 4.5, .NET Core, Xamarin.Android, و Xamarin.iOS را پشتیبانی می‌کند، درنظر گرفت و توانمندی‌ها و پروتکل‌های پشتیبانی شده‌ی توسط آن، از System.Net.Mail توکار نیز بسیار بیشتر است.


افزودن وابستگی‌های MailKit به برنامه

برای شروع به استفاده‌ی از MailKit، می‌توان بسته‌ی نیوگت آن‌را به فایل project.json برنامه معرفی کرد:
{
    "dependencies": {
        "MailKit": "1.10.0"
    }
}


استفاده از MailKit جهت تکمیل وابستگی‌های ASP.NET Core Identity

قسمتی از ASP.NET Core Identity، شامل ارسال ایمیل‌های «ایمیل خود را تائید کنید» است که آن‌را می‌توان توسط MailKit به نحو ذیل تکمیل کرد:
using System.Threading.Tasks;
using ASPNETCoreIdentitySample.Services.Contracts.Identity;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
 
namespace ASPNETCoreIdentitySample.Services.Identity
{
    public class AuthMessageSender : IEmailSender, ISmsSender
    {
        public async Task SendEmailAsync(string email, string subject, string message)
        {
            var emailMessage = new MimeMessage();
 
            emailMessage.From.Add(new MailboxAddress("DNT", "do-not-reply@dotnettips.info"));
            emailMessage.To.Add(new MailboxAddress("", email));
            emailMessage.Subject = subject;
            emailMessage.Body = new TextPart(TextFormat.Html)
            {
                Text = message
            };
 
            using (var client = new SmtpClient())
            {
                client.LocalDomain = "dotnettips.info";
                await client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false);
                await client.SendAsync(emailMessage).ConfigureAwait(false);
                await client.DisconnectAsync(true).ConfigureAwait(false);
            }
        }
 
        public Task SendSmsAsync(string number, string message)
        {
            // Plug in your SMS service here to send a text message.
            return Task.FromResult(0);
        }
    }
}
در اینجا MimeMessage بیانگر محتوا و تنظیمات ایمیلی است که قرار است ارسال شود. ابتدا قسمت‌های From و To آن تنظیم می‌شوند تا مشخص باشد که ایمیل ارسالی از کجا ارسال شده و قرار است به چه آدرسی ارسال شود. در ادامه موضوع و عنوان ایمیل تنظیم شده‌است. سپس متن ایمیل، به خاصیت Body شیء MimeMessage انتساب داده خواهد شد. فرمت ایمیل ارسالی را نیز می‌توان در اینجا تنظیم کرد. برای مثال TextFormat.Html جهت ارسال پیام‌هایی حاوی تگ‌های HTML مناسب است و اگر قرار است صرفا متن ارسال شود، می‌توان TextFormat.Plain را انتخاب کرد.
در آخر، این پیام به SmtpClient جهت ارسال نهایی، فرستاده می‌شود. این SmtpClient هرچند هم نام مشابه آن در System.Net.Mail است اما با آن یکی نیست و متعلق است به MailKit. در اینجا ابتدا LocalDomain تنظیم شده‌است. تنظیم این مورد اختیاری بوده و صرفا به SMTP سرور دریافت کننده‌ی ایمیل‌ها، مرتبط است که آیا قید آن‌را اجباری کرده‌است یا خیر. تنظیمات اصلی SMTP Server در متد ConnectAsync ذکر می‌شوند که شامل مقادیر host ،port و پروتکل ارسالی هستند.


ارسال ایمیل به SMTP pickup folder

روشی که تا به اینجا بررسی شد، جهت ارسال ایمیل‌ها به یک SMTP Server واقعی کاربرد دارد. اما در حین توسعه‌ی محلی برنامه می‌توان ایمیل‌ها را در داخل یک پوشه‌ی موقتی ذخیره و آن‌ها را توسط برنامه‌ی Outlook (و یا حتی مرورگر Firefox) بررسی و بازبینی کامل کرد.
در این حالت تنها کاری را که باید انجام داد، جایگزین کردن قسمت ارسال ایمیل واقعی توسط SmtpClient در کدهای فوق، با قطعه کد ذیل است:
using (var stream = new FileStream($@"c:\smtppickup\email-{Guid.NewGuid().ToString("N")}.eml", FileMode.CreateNew))
{
   emailMessage.WriteTo(stream);
}
تولید فایل‌های eml جهت «شبیه سازی ارسال ایمیل در ASP.Net» بسیار مفید هستند.


FAQ و منبع تکمیلی