简体   繁体   中英

How to “inflate” entity with AutoMapper

I'm receiving an DTO in my web api project and I'd like to use AutoMapper to automagically convert my DTO to the entity which I'm inserting into the database.

Here is a simplification of the DTO and the entities:

class RegistrationDTO
{
    string name;
    ICollection<int> Departments;
}

class Registration
{
    int id;
    DateTime CreatedAt;
    string name;
    virtual ICollection<Department> Departments;
}

class Department
{
    int id;
    string name;
    virtual ICollection<Registration> Registrations;
}

The problem is that RegistrationDTO only has the ids of the departments and I can't find a way to get AutoMapper to get the departments from the database (using Entity Framework 5).

Using a custom ValueResolver I can convert a list of ints to a list of Departments, but I'd like to get the Departments from the database, not create new ones.

This is the solution I came up with, but I'm pretty sure that there's a better way to do it:

var reg= Mapper.Map<Registration>(dto);

reg.Departments = new List<int>(dto.Departments).ConvertAll(input => Context.Departments.Find(input));

if(reg.Departments.Contains(null)) //a department provided does not exist in the database
    return Request.CreateResponse(HttpStatusCode.BadRequest, "invalid department");

...

Anybody can help me out with this?

It is generally a bad idea to use Automapper for inflating entities from DTO data. It's great for going in the opposite direction -- passing data from entities to viewmodels, webapimodels, or DTO's in general. But especially with EntityFramework, using it in the Client-to-Domain direction can get messy.

For example, you have 2 properties on your entity that are not on your viewmodel: id and CreatedAt (why the inconsistent casing btw?). In order for AutoMapper.Mapper.AssertConfigurationIsValid() to not throw an exception, this means you need to ignore or use custom resolvers for these 2 properties in your CreateMap call, in addition to an ignore or custom resolver for the Departments property. In the end, the only thing being automapped is name , which kind of defeats the purpose for using automapper in the first place.

Your code for converting the DTO to an entity is actually pretty concise. Honestly, the major thing I would change about it is the removal of automapper -- in this case, it's really not necessary.

var reg = new Registration { name = dto.name }; // less code than with automapper

reg.Departments = new List<int>(dto.Departments)
    .ConvertAll(input => Context.Departments.Find(input));

if(reg.Departments.Contains(null)) //a department provided does not exist in the database
    return Request.CreateResponse(HttpStatusCode.BadRequest, "invalid department");

You might be tempted to try somthing like this:

Mapper.CreateMap<RegistrationDTO, Registration>()
   .ForMember(d => d.id, o => o.Ignore())
   .ForMember(d => d.CreatedAt, o => o.UseValue(DateTime.Now))
   .ForMember(d => d.Departments, o => o.MapFrom(s => 
   {
       var dbContext = new MyDbContext();
       var departments = new List<int>(s.Departments)
           .ConvertAll(input => dbContext.Departments.Find(input));
       return departments;
   }))
;

This won't work because the DbContext in the delegate block is not the same DbContext you will use to add the Registration entity to ( dbContext.Registrations.Add(reg) ) and invoke SaveChanges on. When you have entities that are attached to different contexts, you will end up with duplicate Department entities in your database (or possibly SQL exceptions due to duplicate primary keys).

Update

I went for AutoMapper because both my entity and DTO have 15+ fields, the only difference between the two being the database specific stuff my entity has, like id, creation date, last modification date, etc. Would you maintain your advice of not using AutoMapper in this case considering that my entities are a lot bigger than the simplification I posted here?

That depends. For your 15+ other properties, are they all scalars? Are any of them foreign key properties (exposed to manage non-collection navigation properties)? How many of them would call for custom resolvers?

I would definitely not use automapper to DTO collection navigation properties ( public virtual ICollection<SomeOtherEntity> OtherEntities { get; set; } ). I would also not try to use automapper to DTO non-collection navigation properties that do not expose a foreign key ( public virtual SomeOtherEntity OtherEntity { get; set; } ).

The code smell here is that for every DTO-to-entity CreateMap call, you will have at least a couple of Ignore s (ignoring creation date, last modification date, etc). Also, if your non-collection nav properties do expose foreign key properties, you can automap the fk property and it will work, but you end up having another ignore for the actual ( virtual ) nav property.

Also, when it comes to domain code, that is your system of record, and it helps to have everything out in the open when reading it, rather than having some details hidden behind AutoMapper. Consider the following -- It is much more explicit, and though it is somewhat verbose, I don't think that's necessarily a bad thing, since it shows all of the domain transfer code in a single source file:

var reg = new Registration
{
    name = dto.name,
    prop1 = dto.prop1,
    prop2 = dto.prop2,
    ...
    propN = dto.propN
};

Compare how many extra lines you would have here with all of the extra lines (ignores, custom resolvers, etc) you would need in a CreateMap bootstrapper. In the end it's your call, hopefully this helps.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM