ایده شماره 2 ‫نگاشت خودکار اشیاء توسط AutoMapper و Reflection


پیش نیاز این مطلب، قسمت قبل آن است. در قسمت قبل، یک کلاس جنریک را به نام BaseDto ایجاد کردیم که با ارث بری Dto‌های پروژه از این کلاس، علاوه بر متد‌های ToEntity و FromEntity جهت ساده سازی عملیات نگاشت، Mapping‌های لازم بین Dto‌ها و Entity‌های مربوطه، توسط Reflection به صورت خودکار انجام می‌شد.  با ما همراه شوید. در این قسمت می‌خواهیم مکانیزم...

پیش نیاز این مطلب، قسمت قبل آن است. در قسمت قبل، یک کلاس جنریک را به نام 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().ReverseMap(); } }

اکنون لازم است تدبیری بیاندیشیم تا کلاس‌هایی را که از اینترفیس IHaveCustomMapping مشتق شده‌اند، به AutoMapper معرفی کنیم. در واقع باید کلاس‌های مذکور (مانند PostDtoMapping) را یافته، یک وهله از آنها را ایجاد کنیم، سپس متد CreateMappings آنها فراخوانی کرده و شیء ای از نوع Profile را به عنوان ورودی به آن پاس دهیم.

بدین منظور کلاسی را به نام CustomMappingProfile به نحو زیر تعریف می‌کنیم.
public class CustomMappingProfile : Profile { public CustomMappingProfile(IEnumerable 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 : IHaveCustomMapping where TEntity : BaseEntity { [Display(Name = "ردیف")] public TKey Id /// /// Maps this dto to a new entity object. /// public TEntity ToEntity() { return Mapper.Map(CastToDerivedClass(this)); } /// /// Maps this dto to an exist entity object. /// public TEntity ToEntity(TEntity entity) { return Mapper.Map(CastToDerivedClass(this), entity); } /// /// Maps the specified entity to a new dto object. /// public static TDto FromEntity(TEntity model) { return Mapper.Map(model); } protected TDto CastToDerivedClass(BaseDto baseInstance) { return Mapper.Map(baseInstance); } //Get automapper Profile then create mapping and ignore unmapped properties public void CreateMappings(Profile profile) { var mappingExpression = profile.CreateMap(); 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 mapping) { } }
کلاس جنریک BaseDto، متدCreateMappings اینترفیس IHaveCustomMapping را پیاده سازی می‌کند. درون این متد، Mapping بین دو نوع TDto و TEntity، توسط () custom mapping for "Title (Category.Name)" public override void CustomMappings(IMappingExpression mapping) { mapping.ForMember( dest => dest.FullTitle, config => config.MapFrom(src => $" ()")); } }
این کلاس، خاصیتی به نام FullTitle دارد که معادلی (خاصیت همنامی) در کلاس Post برای آن وجود ندارد و قرار است مقدار ترکیبی حاصل از Title و Category.Name را نمایش دهد. به همین جهت متد CustomMappings را باز نویسی کرده، شیء mapping را دریافت و سفارشی سازی لازم را روی آن انجام داده‌ایم. توسط متد ForMember مشخص کرده‌ایم که مقدار خاصیت FullTitle باید حاصلی از ترکیب Title و Category.Name به نحو مشخص شده باشد ( توسط متد MapFrom).
پس در این روش علاوه بر امکانات BaseDto و Mapping خودکار، امکان سفارشی سازی دلخواه را نیز خواهیم داشت.
برای کوئری گرفتن از دیتابیس نیز و تبدیل آنها به لیستی از Dto‌ها می‌توان از متد ProjectTo بر روی IQueryable استفاده کرد و حتی شرط Where را بر روی کوئری Dto‌ها اعمال کرد مانند زیر:
List 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() //We can also use Where on IQuerable .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 معادل آن ترجمه و اجرا می‌شوند.

dotnettips

ایده شماره 2 ‫نگاشت خودکار اشیاء توسط AutoMapper و Reflection

حتما بخوانید: سایر مطالب گروه آموزش

برای مشاهده فوری اخبار و مطالب در کانال تلگرام ما عضو شوید!


منتخب امروز

بیشترین بازدید یک ساعت گذشته


پرستار سنگدل پس از قتل فجیع پیرزن پسر معلولش را در اتاق زندانی کرد / کشف جسد در کمد دیو...