Wo soll AutoMapper.CreateMaps platziert werden?

216

Ich verwende AutoMapperin einer ASP.NET MVCAnwendung. Mir wurde gesagt, dass ich die AutoMapper.CreateMapwoanders hin ziehen sollte, da sie viel Overhead haben. Ich bin mir nicht sicher, wie ich meine Anwendung so gestalten soll, dass diese Anrufe nur an einer Stelle stattfinden.

Ich habe eine Webschicht, eine Serviceschicht und eine Datenschicht. Jedes ein eigenes Projekt. Ich benutze Ninjectalles. Ich werde AutoMappersowohl in Web- als auch in Service-Schichten verwenden.

Wie sind Ihre Einstellungen für AutoMapperCreateMap? Wo legst du es hin? Wie nennst du das?

Shawn Mclean
quelle

Antworten:

219

Egal, solange es sich um eine statische Klasse handelt. Es geht nur um Konventionen .

Unsere Konvention ist, dass jede "Schicht" (Web, Dienste, Daten) eine einzelne Datei hat, die aufgerufen wird AutoMapperXConfiguration.cs, mit einer einzigen Methode, die aufgerufen wird Configure(), wobei Xsich die Schicht befindet.

Die Configure()Methode ruft dann privateMethoden für jeden Bereich auf.

Hier ist ein Beispiel für unsere Web-Tier-Konfiguration:

public static class AutoMapperWebConfiguration
{
   public static void Configure()
   {
      ConfigureUserMapping();
      ConfigurePostMapping();
   }

   private static void ConfigureUserMapping()
   {
      Mapper.CreateMap<User,UserViewModel>();
   } 

   // ... etc
}

Wir erstellen eine Methode für jedes "Aggregat" (Benutzer, Beitrag), damit die Dinge gut getrennt sind.

Dann ist dein Global.asax:

AutoMapperWebConfiguration.Configure();
AutoMapperServicesConfiguration.Configure();
AutoMapperDomainConfiguration.Configure();
// etc

Es ist wie eine "Schnittstelle von Wörtern" - kann es nicht erzwingen, aber Sie erwarten es, sodass Sie bei Bedarf Code (und Refactor) erstellen können.

BEARBEITEN:

Ich dachte nur, ich würde erwähnen, dass ich jetzt AutoMapper- Profile verwende . Das obige Beispiel lautet also:

public static class AutoMapperWebConfiguration
{
   public static void Configure()
   {
      Mapper.Initialize(cfg =>
      {
        cfg.AddProfile(new UserProfile());
        cfg.AddProfile(new PostProfile());
      });
   }
}

public class UserProfile : Profile
{
    protected override void Configure()
    {
         Mapper.CreateMap<User,UserViewModel>();
    }
}

Viel sauberer / robuster.

RPM1984
quelle
2
@ AliRızaAdıyahşi Beide Projekte sollten eine Mapping-Datei haben. Der Core sollte über AutoMapperCoreConfiguration und die Benutzeroberfläche über AutoMapperWebConfiguration verfügen. Die Webkonfiguration sollte die Profile aus der Core-Konfiguration hinzufügen.
RPM1984
7
Mapper.InitializeÜberschreibt der Aufruf jeder Konfigurationsklasse die zuvor hinzugefügten Profile? Wenn ja, was sollte anstelle von Initialisieren verwendet werden?
Cody
4
Verfügt Ihr Web-API-Projekt nicht über einen Verweis auf Ihre Service- und Domain-Ebenen?
Chazt3n
3
Wenn ich Web -> Service -> BLL -> DAL habe. Meine Entitäten befinden sich in meinem DAL. Ich möchte weder aus dem Internet noch aus dem Dienst einen Verweis auf meine DAL geben. Wie initialisiere ich es?
Vyache
19
Ab AutoMapper ist 4.2 Mapper.CreateMap()nun obselet. 'Mapper.Map<TSource, TDestination>(TSource, TDestination)' is obsolete: 'The static API will be removed in version 5.0. Use a MapperConfiguration instance and store statically as needed. Use CreateMapper to create a mapper instance.'. Wie würden Sie Ihr Beispiel aktualisieren, um es an die neuen Anforderungen anzupassen?
15 ʙᴀᴋᴇʀ
34

Sie können es wirklich überall platzieren, solange Ihr Webprojekt auf die Assembly verweist, in der es sich befindet. In Ihrer Situation würde ich es in die Service-Schicht einfügen, da die Web-Schicht und die Service-Schicht darauf zugreifen können und später, wenn Sie dies wünschen Wenn Sie eine Konsolen-App ausführen oder ein Unit-Test-Projekt durchführen, ist die Zuordnungskonfiguration auch für diese Projekte verfügbar.

In Ihrer Global.asax rufen Sie dann die Methode auf, mit der alle Ihre Karten festgelegt werden. Siehe unten:

Datei AutoMapperBootStrapper.cs

public static class AutoMapperBootStrapper
{
     public static void BootStrap()
     {  
         AutoMapper.CreateMap<Object1, Object2>();
         // So on...


     }
}

Global.asax beim Anwendungsstart

Ruf einfach an

AutoMapperBootStrapper.BootStrap();

Jetzt werden einige Leute gegen diese Methode argumentieren und gegen einige SOLID-Prinzipien verstoßen, für die sie gültige Argumente haben. Hier sind sie zum Lesen.

Das Konfigurieren von Automapper in Bootstrapper verstößt gegen das Open-Closed-Prinzip?

Brett Allred
quelle
13
Dies. Jeder Schritt in Richtung einer richtigen "Hardcore" -Architektur scheint exponentiell mehr Code zu beinhalten. Das ist einfach; es wird für 99,9% der Codierer da draußen ausreichen; und Ihre Mitarbeiter werden die Einfachheit zu schätzen wissen. Ja, jeder sollte das Thema bezüglich des Open-Closed-Prinzips lesen, aber jeder sollte auch über den Kompromiss nachdenken.
Anon
Wo haben Sie die AutoMapperBootStrapper-Klasse erstellt?
user6395764
16

Update: Der hier veröffentlichte Ansatz ist nicht mehr gültig, da SelfProfilerer ab AutoMapper v2 entfernt wurde.

Ich würde einen ähnlichen Ansatz wie Thoai verfolgen. Aber ich würde die eingebaute SelfProfiler<>Klasse verwenden, um die Karten zu behandeln, und dann die Mapper.SelfConfigureFunktion zum Initialisieren verwenden.

Verwenden dieses Objekts als Quelle:

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
    public string GetFullName()
    {
        return string.Format("{0} {1}", FirstName, LastName);
    }
}

Und diese als Ziel:

public class UserViewModel
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class UserWithAgeViewModel
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public int Age { get; set; }
}

Sie können diese Profile erstellen:

public class UserViewModelProfile : SelfProfiler<User,UserViewModel>
{
    protected override void DescribeConfiguration(IMappingExpression<User, UserViewModel> map)
    {
    //This maps by convention, so no configuration needed
    }
}

public class UserWithAgeViewModelProfile : SelfProfiler<User, UserWithAgeViewModel>
{
    protected override void DescribeConfiguration(IMappingExpression<User, UserWithAgeViewModel> map)
    {
    //This map needs a little configuration
        map.ForMember(d => d.Age, o => o.MapFrom(s => DateTime.Now.Year - s.BirthDate.Year));
    }
}

Erstellen Sie diese Klasse, um sie in Ihrer Anwendung zu initialisieren

 public class AutoMapperConfiguration
 {
      public static void Initialize()
      {
          Mapper.Initialize(x=>
          {
              x.SelfConfigure(typeof (UserViewModel).Assembly);
              // add assemblies as necessary
          });
      }
 }

Fügen Sie diese Zeile Ihrer Datei global.asax.cs hinzu: AutoMapperConfiguration.Initialize()

Jetzt können Sie Ihre Mapping-Klassen dort platzieren, wo sie für Sie sinnvoll sind, und sich nicht um eine monolithische Mapping-Klasse kümmern.

Codeprogression
quelle
3
Nur zu Ihrer Information, die SelfProfiler-Klasse ist seit Automapper v2 weg.
Matt Honeycutt
15

Für diejenigen unter Ihnen, die sich an Folgendes halten:

  1. mit einem ioc container
  2. mag es nicht, dafür aufzubrechen
  3. mag keine monolithische Konfigurationsdatei

Ich habe eine Kombination zwischen Profilen und der Nutzung meines ioc-Containers erstellt:

IoC-Konfiguration:

public class Automapper : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        container.Register(Classes.FromThisAssembly().BasedOn<Profile>().WithServiceBase());

        container.Register(Component.For<IMappingEngine>().UsingFactoryMethod(k =>
        {
            Profile[] profiles = k.ResolveAll<Profile>();

            Mapper.Initialize(cfg =>
            {
                foreach (var profile in profiles)
                {
                    cfg.AddProfile(profile);
                }
            });

            profiles.ForEach(k.ReleaseComponent);

            return Mapper.Engine;
        }));
    }
}

Konfigurationsbeispiel:

public class TagStatusViewModelMappings : Profile
{
    protected override void Configure()
    {
        Mapper.CreateMap<Service.Contracts.TagStatusViewModel, TagStatusViewModel>();
    }
}

Anwendungsbeispiel:

public class TagStatusController : ApiController
{
    private readonly IFooService _service;
    private readonly IMappingEngine _mapper;

    public TagStatusController(IFooService service, IMappingEngine mapper)
    {
        _service = service;
        _mapper = mapper;
    }

    [Route("")]
    public HttpResponseMessage Get()
    {
        var response = _service.GetTagStatus();

        return Request.CreateResponse(HttpStatusCode.Accepted, _mapper.Map<List<ViewModels.TagStatusViewModel>>(response)); 
    }
}

Der Nachteil ist, dass Sie den Mapper über die IMappingEngine-Schnittstelle anstelle des statischen Mappers referenzieren müssen, aber das ist eine Konvention, mit der ich leben kann.

Marius
quelle
14

Alle oben genannten Lösungen bieten eine statische Methode zum Aufrufen (von app_start oder einem beliebigen Ort), mit der andere Methoden zum Konfigurieren von Teilen der Mapping-Konfiguration aufgerufen werden sollen. Wenn Sie jedoch eine modulare Anwendung haben, die Module jederzeit ein- und ausschalten können, funktionieren diese Lösungen nicht. Ich schlage vor, eine WebActivatorBibliothek zu verwenden, die einige Methoden registrieren kann, auf denen ausgeführt werden soll, app_pre_startund app_post_startalle, wo:

// in MyModule1.dll
public class InitMapInModule1 {
    static void Init() {
        Mapper.CreateMap<User, UserViewModel>();
        // other stuffs
    }
}
[assembly: PreApplicationStartMethod(typeof(InitMapInModule1), "Init")]

// in MyModule2.dll
public class InitMapInModule2 {
    static void Init() {
        Mapper.CreateMap<Blog, BlogViewModel>();
        // other stuffs
    }
}
[assembly: PreApplicationStartMethod(typeof(InitMapInModule2), "Init")]

// in MyModule3.dll
public class InitMapInModule3 {
    static void Init() {
        Mapper.CreateMap<Comment, CommentViewModel>();
        // other stuffs
    }
}
[assembly: PreApplicationStartMethod(typeof(InitMapInModule2), "Init")]

// and in other libraries...

Sie können WebActivatorüber NuGet installieren .

ravy amiry
quelle
2
Ich bin kürzlich zu dem gleichen Schluss gekommen. Es hält Ihren Kartenerstellungscode nahe an dem Code, der ihn verbraucht. Diese Methode macht einen MVC-Controller wesentlich wartbarer.
mfras3r
Wie fange ich irgendwo an? Können Sie ein Beispiel geben? Ihre Blog-Links funktionieren nicht ...
Vyache
1
@Vyache es ist ziemlich klar! MyModule1Erstellen Sie im Projekt (oder wie auch immer der Name Ihres Projekts lautet) einfach eine Klasse mit dem Namen InitMapInModule1und fügen Sie den Code in die Datei ein. Machen Sie dasselbe für andere Module.
Ravy Amyy
Gotcha, ich habe es gerade versucht. Ich habe WebActivator von Nuget zu meiner Klassenbibliothek (DAL) hinzugefügt und dort eine statische AutoMapperDalConfiguration-Klasse erstellt. Ich habe eine @ RPM1984-Implementierung erstellt, um die Maps zu konfigurieren und zu initialisieren. Ich benutze kein Profil durch. Danke dir.
Vyache
10

Neben der besten Antwort ist die Verwendung von Autofac IoC liberary eine gute Möglichkeit , um eine gewisse Automatisierung hinzuzufügen. Damit definieren Sie einfach Ihre Profile unabhängig von Initiationen.

   public static class MapperConfig
    {
        internal static void Configure()
        {

            var myAssembly = Assembly.GetExecutingAssembly();

            var builder = new ContainerBuilder();

            builder.RegisterAssemblyTypes(myAssembly)
                .Where(t => t.IsSubclassOf(typeof(Profile))).As<Profile>();

            var container = builder.Build();

            using (var scope = container.BeginLifetimeScope())
            {
                var profiles = container.Resolve<IEnumerable<Profile>>();

                foreach (var profile in profiles)
                {
                    Mapper.Initialize(cfg =>
                    {
                        cfg.AddProfile(profile);
                    });                    
                }

            }

        }
    }

und Aufrufen dieser Zeile in Application_StartMethode:

MapperConfig.Configure();

Der obige Code findet alle Profilunterklassen und initiiert sie automatisch.

Mahmoud Moravej
quelle
7

Es ist für mich keine gute Praxis, die gesamte Zuordnungslogik an einem Ort zu platzieren. Weil die Mapping-Klasse extrem groß und sehr schwer zu pflegen ist.

Ich empfehle, das Mapping-Material zusammen mit der ViewModel-Klasse in derselben cs-Datei zusammenzustellen. Sie können gemäß dieser Konvention einfach zu der gewünschten Zuordnungsdefinition navigieren. Darüber hinaus können Sie beim Erstellen der Zuordnungsklasse schneller auf die ViewModel-Eigenschaften verweisen, da sie sich in derselben Datei befinden.

Ihre Ansichtsmodellklasse sieht also folgendermaßen aus:

public class UserViewModel
{
    public ObjectId Id { get; set; }

    public string Firstname { get; set; }

    public string Lastname { get; set; }

    public string Email { get; set; }

    public string Password { get; set; }
}

public class UserViewModelMapping : IBootStrapper // Whatever
{
    public void Start()
    {
        Mapper.CreateMap<User, UserViewModel>();
    }
}
Van Thoai Nguyen
quelle
9
Wie nennst du das?
Shawn Mclean
1
Ich würde einer Klasse pro Dateiregel
Umair
Eine ähnliche Lösung wird in Velirs Blog Organizing AutoMappers Kartenkonfigurationen in MVC
xmedeko
5

Ab der neuen Version von AutoMapper mit der statischen Methode ist Mapper.Map () veraltet. Sie können also MapperConfiguration als statische Eigenschaft zu MvcApplication (Global.asax.cs) hinzufügen und damit eine Instanz von Mapper erstellen.

App_Start

public class MapperConfig
{
    public static MapperConfiguration MapperConfiguration()
    {
        return new MapperConfiguration(_ =>
        {
            _.AddProfile(new FileProfile());
            _.AddProfile(new ChartProfile());
        });
    }
}

Global.asax.cs

public class MvcApplication : System.Web.HttpApplication
{
    internal static MapperConfiguration MapperConfiguration { get; private set; }

    protected void Application_Start()
    {
        MapperConfiguration = MapperConfig.MapperConfiguration();
        ...
    }
}

BaseController.cs

    public class BaseController : Controller
    {
        //
        // GET: /Base/
        private IMapper _mapper = null;
        protected IMapper Mapper
        {
            get
            {
                if (_mapper == null) _mapper = MvcApplication.MapperConfiguration.CreateMapper();
                return _mapper;
            }
        }
    }

https://github.com/AutoMapper/AutoMapper/wiki/Migrating-from-static-API

Andrey Burykin
quelle
3

Für diejenigen, die (verloren) sind mit:

  • WebAPI 2
  • SimpleInjector 3.1
  • AutoMapper 4.2.1 (mit Profilen)

So habe ich es geschafft, AutoMapper auf " neue Weise " zu integrieren. Auch ein großes Dankeschön an diese Antwort (und Frage)

1 - Im WebAPI-Projekt wurde ein Ordner mit dem Namen "ProfileMappers" erstellt. In diesem Ordner platziere ich alle meine Profilklassen, die meine Zuordnungen erstellen:

public class EntityToViewModelProfile : Profile
{
    protected override void Configure()
    {
        CreateMap<User, UserViewModel>();
    }

    public override string ProfileName
    {
        get
        {
            return this.GetType().Name;
        }
    }
}

2 - In meinem App_Start habe ich einen SimpleInjectorApiInitializer, der meinen SimpleInjector-Container konfiguriert:

public static Container Initialize(HttpConfiguration httpConfig)
{
    var container = new Container();

    container.Options.DefaultScopedLifestyle = new WebApiRequestLifestyle();

    //Register Installers
    Register(container);

    container.RegisterWebApiControllers(GlobalConfiguration.Configuration);

    //Verify container
    container.Verify();

    //Set SimpleInjector as the Dependency Resolver for the API
    GlobalConfiguration.Configuration.DependencyResolver =
       new SimpleInjectorWebApiDependencyResolver(container);

    httpConfig.DependencyResolver = new SimpleInjectorWebApiDependencyResolver(container);

    return container;
}

private static void Register(Container container)
{
     container.Register<ISingleton, Singleton>(Lifestyle.Singleton);

    //Get all my Profiles from the assembly (in my case was the webapi)
    var profiles =  from t in typeof(SimpleInjectorApiInitializer).Assembly.GetTypes()
                    where typeof(Profile).IsAssignableFrom(t)
                    select (Profile)Activator.CreateInstance(t);

    //add all profiles found to the MapperConfiguration
    var config = new MapperConfiguration(cfg =>
    {
        foreach (var profile in profiles)
        {
            cfg.AddProfile(profile);
        }
    });

    //Register IMapper instance in the container.
    container.Register<IMapper>(() => config.CreateMapper(container.GetInstance));

    //If you need the config for LinqProjections, inject also the config
    //container.RegisterSingleton<MapperConfiguration>(config);
}

3 - Startup.cs

//Just call the Initialize method on the SimpleInjector class above
var container = SimpleInjectorApiInitializer.Initialize(configuration);

4 - Dann injizieren Sie in Ihren Controller wie gewohnt eine IMapper-Schnittstelle:

private readonly IMapper mapper;

public AccountController( IMapper mapper)
{
    this.mapper = mapper;
}

//Using..
var userEntity = mapper.Map<UserViewModel, User>(entity);
jpgrassi
quelle
Mit ein wenig Anpassung an einige Besonderheiten funktioniert dieser Ansatz auch hervorragend mit MVC - danke, Mann!
Nick Coad
Bitte fügen Sie ein Demo-Beispiel in Github
Mohammad Daliri
3

Für vb.net-Programmierer, die die neue Version (5.x) von AutoMapper verwenden.

Global.asax.vb:

Public Class MvcApplication
    Inherits System.Web.HttpApplication

    Protected Sub Application_Start()
        AutoMapperConfiguration.Configure()
    End Sub
End Class

AutoMapperConfiguration:

Imports AutoMapper

Module AutoMapperConfiguration
    Public MapperConfiguration As IMapper
    Public Sub Configure()
        Dim config = New MapperConfiguration(
            Sub(cfg)
                cfg.AddProfile(New UserProfile())
                cfg.AddProfile(New PostProfile())
            End Sub)
        MapperConfiguration = config.CreateMapper()
    End Sub
End Module

Profile:

Public Class UserProfile
    Inherits AutoMapper.Profile
    Protected Overrides Sub Configure()
        Me.CreateMap(Of User, UserViewModel)()
    End Sub
End Class

Kartierung:

Dim ViewUser = MapperConfiguration.Map(Of UserViewModel)(User)
Roland
quelle
Ich habe Ihre Antwort ausprobiert, aber in dieser Zeile wird ein Fehler angezeigt: Dim config = New MapperConfiguration (// Überlastungsauflösung fehlgeschlagen, da mit den folgenden Argumenten kein zugängliches 'New' aufgerufen werden kann: 'Public Overloads Sub New (configurationExpression As MapperConfigurationExpression) Can Bitte helfen Sie mir dabei.
Barsan
@barsan: Haben Sie alle Profilklassen korrekt konfiguriert (UserProfile und PostProfile)? Für mich funktioniert es mit Automapper Version 5.2.0.
Roland
Die neue Version 6.0 wird veröffentlicht. Das Protected Overrides Sub Configure()ist also veraltet. Alles bleibt gleich, aber diese Zeile sollte sein:Public Sub New()
Roland