خودکارسازی فرآیند نگاشت اشیاء در AutoMapper
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سه دقیقه

قرار دادن تمامی تنظیمات نگاشت‌ها درون کلاس‌‌های پروفایل تا حدودی حجم کدهای ما را در آینده زیاد خواهد کرد.
public class TestProfile1 : Profile
{
    protected override void Configure()
    {
        // این تنظیم سراسری هست و به تمام خواص زمانی اعمال می‌شود
        this.CreateMap<DateTime, string>().ConvertUsing(new DateTimeToPersianDateTimeConverter()); 
        this.CreateMap<User, UserViewModel>();
       // Other mappings
     }
  
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
در ادامه می‌خواهیم به روشی جهت سازماندهی بهتر این نوع کلاس‌ها بپردازیم. به طوری‌که تعاریف مربوط به نگاشت‌ها در کنار View Modelهای برنامه قرار گیرند. برای اینکار ابتدا اینترفیس‌های زیر را ایجاد خواهیم کرد:
public interface IMapFrom<T>
{

}
public interface IHaveCustomMappings
{
      void CreateMappings(IConfiguration configuration);
}
خوب، همانطور که مشاهده می‌کنید، در اینترفیس IMapFrom امضای هیچ متدی تعریف نشده است. در واقع View Model‌های ما از این اینترفیس جهت تشخیص اینکه به چه مدلی قرار است نگاشت شوند، استفاده خواهند کرد. اما در حالتی‌که نیاز به نگاشت صریح پراپرتی‌های یک View Model داشتیم می‌توانیم اینترفیس IHaveCustomMappings را پیاده‌سازی کرده و جزئیات نگاشت را درون متد CreateMappings تعیین کنیم.
به عنوان مثال View Model زیر را در نظر بگیرید:
public class PersonViewModel : IMapFrom<Person>
{
       public string Name { get; set; }
       public string LastName { get; set; }
}
خوب، در اینجا با پیاده‌سازی اینترفیس IMapFrom نوع مبدا را برای ویومدل فوق مشخص کرده‌ایم. در این‌حالت هدف ما نگاشت تمامی خواص کلاس Person به تمامی خواص کلاس PersonViewModel خواهد بود. برای حالت‌های خاص نیز که نیاز به نگاشت دقیق خواص باشد به اینصورت عمل خواهیم کرد:
public class PersonViewModel : IHaveCustomMapping
{
      public string Name { get; set; }
      // دیگر پراپرتی‌ها
     
      public void CreateMappings(IConfiguration configuration)
      {
             configuration.CreateMap<ApplicationUser, PersonViewModel>()
                   .ForMember(m => m.Name, opt => 
                         opt.MapFrom(u => u.ApplicationUser.UserName));
             // دیگر نگاشت‌ها
      }
}
خوب، در نهایت با استفاده از امکانات LINQ و Reflection کار پردازش تنظیمات نگاشت‌های هر View Model و خودکارسازی فرآیند نگاشت را انجام خواهیم داد. اینکار را می‌توانیم درون یک کلاس با نام AutoMapperConfig و با پیاده‌سازی اینترفیس IRunInit انجام دهیم:
public void Execute() 
{
      var types = Assembly.GetExecutingAssembly().GetExportedTypes();

      LoadStandardMappings(types);

      LoadCustomMappings(types);
}
در داخل متد Execute دو متد به نام‌های LoadStandardMappings و LoadCustomMapping را فراخوانی کرده‌ایم. متد اول برای پردازش حالتی است که اینترفیس IMapFrom را پیاده‌سازی کرده باشیم و متد دوم نیز برای حالتی است که اینترفیس IHaveCustomMappings را پیاده‌سازی کرده باشیم.

متد LoadStandardMappings
:
private static void LoadStandardMappings(IEnumerable <Type> types) 
{
     var maps = (from t in types
                      from i in t.GetInterfaces()
                      where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom< >)  && !t.IsAbstract && !t.IsInterface
                      select new {
                               Source = i.GetGenericArguments()[0],
                               Destination = t
                      }).ToArray();

      foreach(var map in maps) 
      {
               Mapper.CreateMap(map.Source, map.Destination);
      }
}
توضیح کدهای فوق:
  1. ابتدا تمامی typeهای تعریف شده در پروژه به متد فوق پاس داده خواهند شد. 
  2. برای هر type تمامی اینترفیس‌هایی که توسط این type پیاده‌سازی شده باشند را دریافت خواهیم کرد.
  3. سپس هر type که اینترفیس IMapFrom را پیاده‌سازی کرده باشد را پردازش می‌کنیم.
  4. سپس از نوع‌های Abstract و Interface صرفنظر خواهیم کرد.
  5. انواع مبدا و مقصد را برای AutoMapper فراهم خواهیم کرد.
  6. در نهایت AutoMapper براساس آنها نگاشت را ایجاد خواهد کرد. 

 متد LoadCustomMapping:
private static void LoadCustomMappings(IEnumerable <Type> types) 
{
     var maps = (from t in types
                      from i in t.GetInterfaces()
                      where typeof(IHaveCustomMappings).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface
                      select(IHaveCustomMappings) Activator.CreateInstance(t)).ToArray();

     foreach(var map in maps) 
     {
               map.CreateMappings(Mapper.Configuration);
     }
}

توضیح کدهای فوق:
این متد نیز همانند متد قبلی، تمامی typeها را پردازش خواهد کرد. با این تفاوت که مواردی که اینترفیس IHaveCustomMappings را پیاده‌سازی کرده باشند، دریافت کرده و در نهایت متد CreateMappings آنها را فراخوانی خواهیم کرد.
اکنون کدهای نگاشت برنامه از اصول  Open and Closed  پیروی می‌کنند. در نتیجه می‌توانیم نگاشت‌های جدید را به سادگی و با ایجاد View Model ها تعریف کنیم.
  • #
    ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۹ شهریور ۱۳۹۵، ساعت ۱۵:۳۹
    با سلام؛ من از استراکچرمپ 3 و اتومپر 5 استفاده میکنم، وقتی میخوام از امکان ProjectTo اتومپر استفاده کنم پیغام خطای زیر رو دریافت میکنم
     _contacts.ProjectTo<ContactViewModel>(_mapper.ConfigurationProvider).ToList();

     Mapper not initialized. Call Initialize with appropriate configuration
    ولی اگر به صورت عادی مثلا به این صورت مپ کنم خطایی رخ نمیده
     _mapper.Map<ContactViewModel>(contact);
    اگه تنظیم مپ رو به صورت دستی مپ کنم درست کار میکنه
     cfg.CreateMap<Contact, ContactViewModel>();
    شاید به جای استفاده از دستور
     Mapper.CreateMap(map.Source, map.Destination);
    از متد جنریکش استفاده کنیم درست بشه
     Mapper.CreateMap<map.Source, map.Destination>();
    • #
      ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۹ شهریور ۱۳۹۵، ساعت ۱۵:۵۷
      نباید static mapper را با اینترفیس IMapper یکی درنظر گرفت. شما در یک برنامه می‌توانید چندین mapper داشته باشید.
      اگر می‌نویسید Mapper.CreateMap یعنی در حال استفاده‌ی از static mapper آن هستید. اگر می‌نویسید:
      For<IMapper>().Use(ctx => ctx.GetInstance<MapperConfiguration>().CreateMapper(ctx.GetInstance));
      در حال استفاده‌ی از IMapper  سفارشی خودتان هستید. به همین جهت پیام خطای «Mapper not initialized. Call Initialize with appropriate configuration» را دریافت می‌کنید؛ چون تنظیمات را در وهله‌ی دیگری ثبت کرده‌اید و اکنون از وهله‌‌ای متفاوت در حال استفاده‌اید.
      برای رفع مشکل فقط از یک وهله از «
      IMapper» استفاده کنید.
      • #
        ‫۸ سال و ۱ ماه قبل، سه‌شنبه ۹ شهریور ۱۳۹۵، ساعت ۲۱:۴۹
        ممنون از پاسختون؛ تنظیمات رجیستری مربوط به اتومپر من به این صورته: 
        public class AutomapperRegistry : Registry
            {
                public AutomapperRegistry()
                {
                    For<MapperConfiguration>().Use("", ctx =>
                    {
                        var config = new MapperConfiguration(cfg =>
                        {
                            AutomapperConfig.setup(cfg, ctx);
                        });
        
                        config.AssertConfigurationIsValid();
                        
                        return config;
        
                    }).Singleton();
        
                    For<IMapper>()
                        .HttpContextScoped()
                        .Use(ctx => 
                            ctx.GetInstance<MapperConfiguration>().CreateMapper(ctx.GetInstance));
                }
                
            }
        همانطور که ملاحظه میکنید تنها یک وهله از کلاس MapperConfiguration  به صورت  Singleton تهیه  و از آن برای تهیه وهله از  IMapper 
        استفاده میشه و برای تنظیمات مپ از متدهای استاتیک آن استفاده نمیکنم و به این صورته:
        public static class AutomapperConfig
            {
                public static void setup(IMapperConfigurationExpression mappconfig, IContext ctx)
                {
                    configureAutoMapper(mappconfig, ctx);
                }
        
                private static void configureAutoMapper(IMapperConfigurationExpression mappconfig, IContext ctx)
                {
                    var profiles = ctx.GetAllInstances<AutoMapper.Profile>().ToList();
                    foreach (var profile in profiles)
                    {
                        mappconfig.AddProfile(profile);
                    }
        
                    var types = Assembly.GetExecutingAssembly().GetExportedTypes();
        
                    LoadStandardMappings(types, mappconfig);
        
                    LoadCustomMappings(types, mappconfig);
                }
        
                private static void LoadStandardMappings(IEnumerable<Type> types, IMapperConfigurationExpression mapper)
                {
                    var maps = (from t in types
                                from i in t.GetInterfaces()
                                where i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>) && !t.IsAbstract && !t.IsInterface
                                select new
                                {
                                    Source = i.GetGenericArguments()[0],
                                    Destination = t
                                }).ToArray();
        
                    foreach (var map in maps)
                    {
                        mapper.CreateMap(map.Source, map.Destination);
                    }
                }
        
                private static void LoadCustomMappings(IEnumerable<Type> types, IMapperConfigurationExpression mapper)
                {
                    var maps = (from t in types
                                from i in t.GetInterfaces()
                                where typeof(IHaveCustomMappings).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface
                                select (IHaveCustomMappings)Activator.CreateInstance(t)).ToArray();
        
                    foreach (var map in maps)
                    {
                        map.CreateMappings(mapper);
                    }
                }
            }
        اگه جایی اشتباه کردم ممنون میشم راهنمایی کنید.
        • #
          ‫۸ سال و ۱ ماه قبل، چهارشنبه ۱۰ شهریور ۱۳۹۵، ساعت ۰۰:۵۷
          نحوه‌ی یافتن اسمبلی حاوی ViewModelها را باید مشخص کنید (اگر ExecutingAssembly حاوی آن‌ها نیست):
          //var types = Assembly.GetExecutingAssembly().GetExportedTypes();
          var types = typeof(PersonViewModel).Assembly.GetExportedTypes();
          • #
            ‫۸ سال و ۱ ماه قبل، پنجشنبه ۱۱ شهریور ۱۳۹۵، ساعت ۰۰:۳۷
            ممنون از پاسختون؛
            اتفاق عجیبی رخ میده که متوجه نمیشم ؛ کلاسهای ViewModel در پروژه سرویس تعریف شدن  و از داخل سرویس خروجی را  مپ میکنم و بازگشت میدم.
            به این صورتی که فرمودید عمل کردم؛ حالا عمل نگاشت و مپ در سرویس به درستی عمل میکنه ولی عمل نگاشت و مپ داخل کنترلها که داخل پروژه وب هستند دیگر کار نمیکند قبل از اینکه این کارو بکنم دقیقا عکس  این اتفاق رخ میداد یعنی عمل مپ در پروژه وب و کنترلها به درستی انجام میشد ولی در داخل سرویس با خطا مواجه میشد. به نظر میرسه فقط عملیات مپ اسمبلی که معرفی شده به درستی کار میکند!
            تنظیمات تزریق وابستگی اتومپر داخل پروژه MVC انجام میشه و در پروژه سرویس و کنترل های MVC  مپر تزریق میشه. با سپاس.
            • #
              ‫۸ سال و ۱ ماه قبل، پنجشنبه ۱۱ شهریور ۱۳۹۵، ساعت ۰۱:۰۳
              محل دقیق اسکن استراکچرمپ را به این صورت مشخص کنید:
              cfg.Scan(scan =>
              {
                 scan.AssemblyContainingType<SomeType1>();
                 scan.AssemblyContainingType<SomeType2>();
              });
  • #
    ‫۷ سال و ۱۱ ماه قبل، دوشنبه ۱۲ مهر ۱۳۹۵، ساعت ۱۴:۰۶
    سلام؛ با چه  روشی می‌توان وقتی که اطلاعات را از دیتابیس فراخوانی می‌کنم، AddressViewModel داخل SupplierViewModel از نگاشت  <CreateMap<Address, AddressViewModel استفاده کند.
    public class SupplierViewModel
    {  
       public string name { get; set; }   
       public AddressViewModel AddressViewModel { get; set; }=new AddressViewModel();    }
    
    public class AddressViewModel
    {
       public string phone_mobile { get; set; }
       public string address1 { get; set; }
    }
    
    public class Supplier 
    {      
       public int Id { get; set; }   
       public string name { get; set; } 
       public virtual Address Address { get; set; } 
    } 
    
    CreateMap<Supplier, SupplierViewModel>().IgnoreAllUnmapped()  });
    CreateMap<Address, AddressViewModel>()
    var viewModel =
             await  _suppliers.AsNoTracking()
                       .ProjectTo<SupplierViewModel>(parameters:null,configuration: _mappingEngine.ConfigurationProvider)
                       .FirstOrDefaultAsync(a => a.Id == id);
    • #
      ‫۷ سال و ۱۱ ماه قبل، دوشنبه ۱۲ مهر ۱۳۹۵، ساعت ۱۴:۵۴
      - سؤال شما مرتبط به بحث جاری نیست.
      - این موارد مانند استفاده از متد ForMember برای نگاشت‌های چندسطحی در دوره‌ی AutoMapper بحث شدند.
  • #
    ‫۷ سال و ۱۱ ماه قبل، دوشنبه ۲۶ مهر ۱۳۹۵، ساعت ۱۶:۲۵
    یک نکته‌ی تکمیلی
    در نگارش‌های جدید AutoMapper می‌توان متد LoadStandardMappings را حذف و با خاصیت CreateMissingTypeMaps جایگزین کرد (نیازی هم به اینترفیس خالی IMapFrom نیست):
    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMissingTypeMaps = true;
  • #
    ‫۷ سال و ۱۰ ماه قبل، دوشنبه ۲۴ آبان ۱۳۹۵، ساعت ۱۸:۳۴
    با سلام
    من از AutoMapper 5 استفاده می‌کنم.میخواستم ببینم متد های  
     LoadStandardMappings();
    LoadCustomMappings();
    رو چطور باید ویرایش کنم.

    همان طور که در عکس مشاهده میکنید خطا وجود دارد.
    با تشکر.
      • #
        ‫۷ سال و ۱۰ ماه قبل، سه‌شنبه ۲۵ آبان ۱۳۹۵، ساعت ۱۵:۱۹
        ممنونم.
        پس دیگه به کلاس AutoMapperConfig  احتیاجی نیست؟
        و وقتی که از
        اینترفیس IHaveCustomMappings استفاده کنیم مشکلی به وجود نمی‌آید؟

          • #
            ‫۶ سال و ۵ ماه قبل، دوشنبه ۶ فروردین ۱۳۹۷، ساعت ۲۲:۲۸
            با سلام
            وقتی از 
             private void addAllIHaveCustomMappings(IContext ctx, IMapperConfigurationExpression cfg)
                    {
                        var profiles = ctx.GetAllInstances<IHaveCustomMappings>().ToList();
                        foreach (var profile in profiles)
                        {
                         profile.CreateMappings(cfg);
                    }
                  }
            در یک پروژه MVC استفاده میکنم مپ صورت نمی‌گیره و خطایی هم صادر نمیشود ,وقتی برکپوینت هم قرار میدهم profiles  بصورت نال هست 
            در ضمن ویومدل‌ها در لایه سرویس و پروژه دیگری میباشد و ورژن Automapper من 6.2.2 هست
            آیا تنظیمات خاص دیگری در Application_Start نیاز هست یا کلا تنظیمات در MVC متفاوت هست
            با تشکر
            • #
              ‫۶ سال و ۵ ماه قبل، دوشنبه ۶ فروردین ۱۳۹۷، ساعت ۲۲:۳۷
              در ضمن ویومدل‌ها در لایه سرویس و پروژه دیگری میباشد.

              این مورد را باید اضافه کنید تا آن اسمبلی و یا اسمبلی‌های دیگر را هم اسکن کند؛ وگرنه میدان دید scan.TheCallingAssembly فقط محدود به اسمبلی جاری هست:
              scan.AssemblyContainingType<SomeTypeInThatAssembly>();
              • #
                ‫۶ سال و ۵ ماه قبل، سه‌شنبه ۷ فروردین ۱۳۹۷، ساعت ۱۷:۱۳
                ممنونم. مشکل قبل حل شد. 
                 اگر ویومدل من بدینصورت باشه
                public class MyViewModel : IHaveCustomMappings
                    {
                       
                        public int SchedulerId { get; set; }
                        public bool IsApplied { get; set; }
                        public int Qty { get; set; }
                        
                        public void CreateMappings(IMapperConfigurationExpression configuration)
                        {
                            configuration.CreateMap<DomainModels.Models.Scheduler, MyViewModel>()
                                .ForMember(dest => dest.SchedulerId, opt => opt.MapFrom(src => src.Id));
                                    }
                    }
                خطایی صادر میکندبا این عنوان: 
                Unmapped members were found. Review the types and members below.
                Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
                For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
                اگر دو فیلد IsApplied  ,    Qty   هم در متد  CreateMappings بصورت دستی map کنم خطایی صادر نمیشود. در صورتیکه قبلا با اتوماتیک مپ میشدند. آیا تغییری در AutoMapperRegistry  نیاز هست؟