اشتراکها
پیش نیاز این مطلب، قسمت قبل آن است. در قسمت قبل، یک کلاس جنریک را به نام BaseDto ایجاد کردیم که با ارث بری Dtoهای پروژه از این کلاس، علاوه بر متدهای ToEntity و FromEntity جهت ساده سازی عملیات نگاشت، Mappingهای لازم بین Dtoها و Entityهای مربوطه، توسط Reflection به صورت خودکار انجام میشد.
در این قسمت میخواهیم مکانیزم Mapping خودکار را کمی تغییر داده و قابلیت سفارشی سازی Mappingها را فراهم کنیم. سورس کامل مثال را میتوانید در این ریپازیتوری مشاهده کنید.
ابتدا یک اینترفیس را به نام IHaveCustomMapping به نحو زیر ایجاد میکنیم.
public interface IHaveCustomMapping { void CreateMappings(AutoMapper.Profile profile); }
به عنوان مثال کلاس زیر، Mapping لازم برای PostDto و Post را درون متد CreateMappings خود اعمال میکند.
public class PostDtoMapping : IHaveCustomMapping { public void CreateMappings(Profile profile) { profile.CreateMap<PostDto, Post>().ReverseMap(); } }
بدین منظور کلاسی را به نام 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 استفاده میکند، به نحو ذیل تعریف کردهاید:
با استفادهای مانند:
روش متداول تنظیمات تزریق وابستگیها در ASP.NET MVC، بیشتر به بحث کنترلرها مرتبط است و سایر قسمتها را پوشش نمیدهد. برای این مورد خاص ابتدا نیاز است یک FilterProvider سفارشی را به نحو ذیل تدارک دید:
نکتهی مهم آن، استفاده از متد BuildUp استراکچرمپ است. نمونهی آنرا در تنظیمات تزریق وابستگیها در وب فرمها پیشتر ملاحظه کردهاید. در این مثال کار آن وهله سازی وابستگیهای فیلترهای تعریف شده در برنامه است.
پس از اینکه FilterProvider سفارشی مخصوص کار با استراکچرمپ را تهیه کردیم، اکنون نوبت به جایگزین کردن آن با FilterProvider پیش فرض ASP.NET MVC در فایل global.asax.cs به نحو ذیل است:
استفاده از SmObjectFactory.Container.GetInstance سبب خواهد شد تا به صورت خودکار، وابستگی تزریق شدهی در سازندهی کلاس StructureMapFilterProvider وهله سازی و تامین شود.
همچنین در این مثال چون تزریق وابستگی در کلاس LogAttribute از نوع setter injection است، نیاز است در تنظیمات ابتدایی Container مورد استفاده، Policies.SetAllProperties نیز قید شود:
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
DI06
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; } } } }
پس از اینکه 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>());
همچنین در این مثال چون تزریق وابستگی در کلاس 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
یک نکتهی تکمیلی: نکته امنیتی در هنگام استفاده از توکن ها هنگامیکه کاربری اطلاعات خود را ویرایش میکند، معمولا یک ویوو مدل را از ورودی دریافت میکنیم و دادههای آن کاربر را بر اساس آیدی که درون ویوو مدل ارسال شدهاست، ویرایش میکنیم. اما در این حالت کاربر میتواند با تغییر آیدی ارسالی در ویوو مدل، اطلاعات سایر کاربران را نیز تغییر دهد! برای جلوگیری از این کار میتوان به روش زیر عمل کرد. ابتدا در هنگام ساخت توکن، آیدی کاربر و یک امضا 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); }
در این مطلب میخواهیم کارآیی event handlers پیاده سازی شده با روشهای متفاوتی را مورد بررسی قراردهیم.
به مثال زیر توجه کنید:
در مثال بالا دو نسخهی مختلف از event handler را با دو روش، (روش اول) با استفاده از Lambda syntax و (روش دوم) با استفاده از یک متد، به صورت جدا تعریف شده، پیاده سازی کردهایم.
خوب؛ برای اندازه گیری کارآیی این دو روش باید کمی فکر کنیم که چه چیزی کارآیی این دو روش را تغییر میدهد؟
آیا پردازش event با اضافه کردن و حذف کردن event handler؟ و یا پردازش درون event باعث تغییر در کارآیی میشود؟
این، سوال مهمی در تست کارآیی این دو روش مختلف است. اگر پردازش درون event باعث ایجاد تفاوت کارآیی میشود، با استفاده از این برنامه میتوان آن را اندازه گیری کرد. با این حال اگر تفاوت کارآیی با اضافه کردن و حذف کردن event handler اتفاق میافتد، با این برنامه بعید است بتوان این روش را تست کرد چرا که فقط یکبار این عمل انجام میشود.
قبل از شروع به اندازه گیری کارآیی این دو روش، اجازه بدهید ابتدا به کد IL آنها نگاهی کنیم. (روش اول با استفاده از Lambda syntax)
در بالا 5 دستورالعمل برای اضافه کردن event handler وجود دارد (از IL_0010 تا IL_0029) و یک دستور برای حذف handler وجود دارد (IL_0042).
قبل از شروع مقایسه، کد IL روش دوم را نیز بررسی میکنیم:
همانطور که مشاهده میکنید در روش دوم برای اضافه کردن event handler تنها 3 خط وجود دارند (IL_004a تا IL_0055) و برای حذف کردن آن نیز 3 خط وجود دارند (IL_006e تا IL_0079).
برای اندازه گیری دقیق، برنامهی بالا را کمی تغییر میدهیم. ما میزان اضافه و حذف شدن event handler را میخواهیم اندازهگیری کنیم و کاری به زمان اجرای یک عملیات نداریم. بنابراین فراخوانی ()PerformExpensiveCalculation را comment کرده و به صورت خیلی ساده فقط handler را اضافه و حذف میکنیم.
و چنین خروجی را تولید میکند (البته نسبت به سرعت CPU این زمانها متفاوت خواهد بود)
خوب؛ اگر در یک اجرای برنامه، شما یک میلیون بار event handler را اضافه و حذف کنید، 28ms میتوانید صرفه جویی کنید (در روش اول).
توجه: اگر در برنامهی شما یک میلیون بار event handler اضافه و حذف میشوند، نیاز به بازنگری مجدد در طراحی کلی برنامه تان دارد.
به جای تعریف یک متغیر محلی برای عبارت Lambda، دستور اضافه و حذف کردن event handler را به صورت inline استفاده کردیم. خروجی این روش به صورت زیر میشود:
همانطور که مشاهده میکنید، روش اول خیلی خیلی آهسته است. توجه کنید من بعد از یکصد هزار بار اضافه و حذف کردن handler، به دلیل طولانی شدن، عملیات را قطع کردم. خب دلیل این همه کندی چیست؟ بیایید نگاهی به کد IL درون حلقهی روش اول بیاندازیم.
به خطهای ( 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 به شکل صحیح انجام ندهید، به سرعت، در معیارهای کارآیی خود را نشان میدهد.
دانلود برنامه بالا
به مثال زیر توجه کنید:
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 با اضافه کردن و حذف کردن 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>)
قبل از شروع مقایسه، کد 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 را میخواهیم اندازهگیری کنیم و کاری به زمان اجرای یک عملیات نداریم. بنابراین فراخوانی ()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); } }
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 اضافه و حذف میشوند، نیاز به بازنگری مجدد در طراحی کلی برنامه تان دارد.
یک اشتباه بزرگ
با ایجاد یک تغییر در روش اول (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"); }
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
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
ممکن است شما به این نتیجه رسیده باشید که استفاده از Lambda syntax برای اضافه و حذف کردن event handler آهستهتر از، استفاده از متد جدا است، این یک اشتباه بزرگ است. در صورتی که شما اضافه و حذف کردن event handler را با استفاده از Lambda syntax به شکل صحیح انجام ندهید، به سرعت، در معیارهای کارآیی خود را نشان میدهد.
دانلود برنامه بالا
در 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 به صورت زیر است:
و محتوای فایل File2.cs به نحو زیر تعریف شدهاست:
اگر دقت کنید، ذیل فضای نام مشخص و ثابت CS11Tests، دو کلاس هم نام Post را داریم که اینبار با واژهی کلیدی file، شروع شدهاند و میدان دید دسترسی به آنها، محدود به همان فایل دربرگیرندهی آنها است و در سایر قسمتهای برنامه قابل دسترسی نیستند. اگر خواستیم بهنحوی از آنها در سایر قسمتهای برنامه نیز استفاده کنیم، مانند فایل Program.cs، میتوان یک تعریف متداول internal/public را مانند کلاسهای internal تعریف شده، ایجاد کرد و سپس به صورت «غیرمستقیمی» به آنها دسترسی یافت:
امکان partial تعریف کردن نوعهای محدود به یک فایل در C# 11
در اینجا میتوان نوعهای محدود به یک فایل را partial نیز تعریف کرد؛ به شرطی که تمام تعاریف آنها داخل همان فایل قرار گیرند:
یک سؤال: اگر در یک فایل، file class Post و در فایلی دیگر، کلاس هم نام داخلی internal class Post را تعریف کردیم، آیا میتوان از نمونهی همنام internal، در کلاس file دار استفاده کرد؟
پاسخ: خیر!
فرض کنید در File4.cs چنین تعریفی را داریم:
در اینجا در فضای نام مشخصی، کلاس Post، به صورت internal تعریف شدهاست. اکنون در File3.cs، مجدد تعریف کلاس همنام Post را اینبار به صورت file داریم:
این قطعه کد کامپایل نمیشود. چون Post ای که در اینجا قابل استفادهاست، دقیقا همان کلاس Post جاری این فایل است و نه نمونهی همنام internal در فایلی دیگر.
خروجی کامپایلر C# 11 در مورد سطح دسترسی file
برای نامگذاری نهایی اینگونه نوعها استفاده میکند؛ مانند مثال زیر که مرتبط با کلاس Post تعریف شدهی در فایل File1.cs است:
Index منحصربفرد استفاده شده، مشکل تداخل نامها را برطرف میکند و به علت وجود <> در تعریف این نامهای ویژه، امکان استفادهی از آنها در سایر قسمتها و فایلهای برنامه وجود ندارد.
یک مثال: نمونهای از نحوهی تعریف و استفادهی از 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(); }
namespace CS11Tests; file static class Post { public static string GetTitle() => "Title from File2.cs"; } internal static class InternalClassFromFile2 { public static string GetTitle() => Post.GetTitle(); }
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"; }
namespace CS11Tests; file static class Post { internal static string GetFileScopeTitle() => CS11Tests.Post.GetTitle() + "Title from File3.cs"; }
خروجی کامپایلر C# 11 در مورد سطح دسترسی file
کامپایلر C# 11 جهت جلوگیری از تداخل نامهای حاصل از تعریف کلاسهای با سطح دسترسی file، از قالب زیر:
<SourceFileNameWithoutExtension>F$index$_TypeName
internal static class <File1>F3A5590C89B71B2DB20A548228781187A11D076C0CC91E851A4EE796FFE808F8F__Post { public static string GetTitle() { return "Title from File1.cs"; } }
تاکنون از این روش نامگذاری ویژه، در موارد دیگری مانند 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 کرد.
- جهت کدهای تولیدی توسط ابزارها: گاهی از اوقات، تولید کنندههای کد، از یک نام مشخص مانند DataSet، بارها و بارها استفاده میکنند. برای جلوگیری از تداخل اینها، عموما از تعریف تو در توی کلاسها استفاده میشود و یا نام آنها را با ایندکسهایی مانند DateSet1، DateSet2 و امثال آنها مشخص میکنند. وجود واژهی کلیدی file، کار ابزارهای تولید کنندهی کد را سادهتر میکند.
- برای ساده سازی تعریف متدهای الحاقی: با استفاده از سطح دسترسی فایل میتوان از تداخل متدهای الحاقی هم نام و همچنین شلوغ شدن intellisense جلوگیری کرد. به این ترتیب میتوان کلاسهای حاوی Extension method مختص به یک فایل را ایجاد کرد که در سایر قسمتهای برنامه قابل دسترسی نباشند.
- کاهش تعریف کلاسهای تو در تو: همانطور که عنوان شد، یکی از روشهای مقابلهی با مشکل تعریف کلاسهای هم نام در یک فضای نام مشخص، تعریف nested classes است. با ارائهی واژهی کلیدی file، میتوان یک سطح فرو رفتگی تعریف کلاسها را کاهش داد و به کدهای تمیزتری رسید.
- امکان کپسوله سازیهای بهتر: عموما کامپوننتها و ماژولها، از چند کلاس تشکیل میشوند. با وجود واژهی کلیدی file، میتوان به سطح بالاتری از خصوصی سازی نوعها، بدون نیاز به تعریف نوعهای private و یا nested private رسید.
- سهولت نوشتن کلاسهای آزمونهای واحد: عموما هر کلاس آزمون، از نوعها و دادههای خاص خودش استفاده میکنند و در اینجا میتوان سطح دسترسی این تعاریف را بسیار محدود و مختص به همان فایل Test کرد.
public class Token { [JsonPropertyName("refreshToken")] public string RefreshToken { get; set; } } public async Task<IActionResult> RefreshToken([FromBody]Token model)
فضای نام 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 برنامه معرفی کرد:
استفاده از MailKit جهت تکمیل وابستگیهای ASP.NET Core Identity
قسمتی از ASP.NET Core Identity، شامل ارسال ایمیلهای «ایمیل خود را تائید کنید» است که آنرا میتوان توسط MailKit به نحو ذیل تکمیل کرد:
در اینجا 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 در کدهای فوق، با قطعه کد ذیل است:
تولید فایلهای eml جهت «شبیه سازی ارسال ایمیل در ASP.Net» بسیار مفید هستند.
FAQ و منبع تکمیلی
افزودن وابستگیهای 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); } } }
در آخر، این پیام به 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); }
FAQ و منبع تکمیلی