I am trying to use (POST/PUT) a DTO object with a collection of child objects from JavaScript to an ASP.NET Core (Web API) with an EF Core context as my data source.
The main DTO class is something like this (simplified of course):
public class CustomerDto {
public int Id { get;set }
...
public IList<PersonDto> SomePersons { get; set; }
...
}
What I don't really know is how to map this to the Customer entity class in a way that does not include a lot of code just for finding out which Persons had been added/updated/removed etc.
I have played around a bit with AutoMapper but it does not really seem to play nice with EF Core in this scenario (complex object structure) and collections.
After googling for some advice around this I haven't found any good resources around what a good approach would be. My questions is basically: should I redesign the JS-client to not use "complex" DTOs or is this something that "should" be handled by a mapping layer between my DTOs and Entity model or are there any other good solution that I am not aware of?
I have been able to solve it with both AutoMapper and and by manually mapping between the objects but none of the solutions feels right and quickly become pretty complex with much boilerplate code.
The following article describes what I am referring to regarding AutoMapper and EF Core. Its not complicated code but I just want to know if it's the "best" way to manage this.
(Code from the article is edited to fit the code example above)
http://cpratt.co/using-automapper-mapping-instances/
var updatedPersons = new List<Person>();
foreach (var personDto in customerDto.SomePersons)
{
var existingPerson = customer.SomePersons.SingleOrDefault(m => m.Id == pet.Id);
// No existing person with this id, so add a new one
if (existingPerson == null)
{
updatedPersons.Add(AutoMapper.Mapper.Map<Person>(personDto));
}
// Existing person found, so map to existing instance
else
{
AutoMapper.Mapper.Map(personDto, existingPerson);
updatedPersons.Add(existingPerson);
}
}
// Set SomePersons to updated list (any removed items drop out naturally)
customer.SomePersons = updatedPersons;
Code above written as a generic extension method.
public static void MapCollection<TSourceType, TTargetType>(this IMapper mapper, Func<ICollection<TSourceType>> getSourceCollection, Func<TSourceType, TTargetType> getFromTargetCollection, Action<List<TTargetType>> setTargetCollection)
{
var updatedTargetObjects = new List<TTargetType>();
foreach (var sourceObject in getSourceCollection())
{
TTargetType existingTargetObject = getFromTargetCollection(sourceObject);
updatedTargetObjects.Add(existingTargetObject == null
? mapper.Map<TTargetType>(sourceObject)
: mapper.Map(sourceObject, existingTargetObject));
}
setTargetCollection(updatedTargetObjects);
}
.....
_mapper.MapCollection(
() => customerDto.SomePersons,
dto => customer.SomePersons.SingleOrDefault(e => e.Id == dto.Id),
targetCollection => customer.SomePersons = targetCollection as IList<Person>);
One thing I really want is to delcare the AutoMapper configuration in one place (Profile) not have to use the MapCollection() extension every time I use the mapper (or any other solution that requires complicating the mapping code).
So I created an extension method like this
public static class AutoMapperExtensions
{
public static ICollection<TTargetType> ResolveCollection<TSourceType, TTargetType>(this IMapper mapper,
ICollection<TSourceType> sourceCollection,
ICollection<TTargetType> targetCollection,
Func<ICollection<TTargetType>, TSourceType, TTargetType> getMappingTargetFromTargetCollectionOrNull)
{
var existing = targetCollection.ToList();
targetCollection.Clear();
return ResolveCollection(mapper, sourceCollection, s => getMappingTargetFromTargetCollectionOrNull(existing, s), t => t);
}
private static ICollection<TTargetType> ResolveCollection<TSourceType, TTargetType>(
IMapper mapper,
ICollection<TSourceType> sourceCollection,
Func<TSourceType, TTargetType> getMappingTargetFromTargetCollectionOrNull,
Func<IList<TTargetType>, ICollection<TTargetType>> updateTargetCollection)
{
var updatedTargetObjects = new List<TTargetType>();
foreach (var sourceObject in sourceCollection ?? Enumerable.Empty<TSourceType>())
{
TTargetType existingTargetObject = getMappingTargetFromTargetCollectionOrNull(sourceObject);
updatedTargetObjects.Add(existingTargetObject == null
? mapper.Map<TTargetType>(sourceObject)
: mapper.Map(sourceObject, existingTargetObject));
}
return updateTargetCollection(updatedTargetObjects);
}
}
Then when I create the mappings I us it like this:
CreateMap<CustomerDto, Customer>()
.ForMember(m => m.SomePersons, o =>
{
o.ResolveUsing((source, target, member, ctx) =>
{
return ctx.Mapper.ResolveCollection(
source.SomePersons,
target.SomePersons,
(targetCollection, sourceObject) => targetCollection.SingleOrDefault(t => t.Id == sourceObject.Id));
});
});
Which allow me to use it like this when mapping:
_mapper.Map(customerDto, customer);
And the resolver takes care of the mapping.
About EF Core NuGet packages. To add EF Core support to a project, install the database provider that you want to target. This tutorial uses SQL Server, and the provider package is Microsoft.EntityFrameworkCore.SqlServer. This package is included in the Microsoft.AspNetCore.App metapackage, so you don't need to reference the package.
One way to solve this is by using data transfer objects. A data transfer object (in English: data transfer object, DTO) is an object used to transport data between processes. We will use these DTOs to represent the data we want the clients of our Web API to receive. Another name that the DTOs receive is View Model.
Always use the AutoMapper.Extensions.Microsoft.DependencyInjection package in ASP.NET Core with services.AddAutoMapper (assembly []). This package will perform all the scanning and dependency injection registration. We only need to declare the Profile configurations. Always organize configuration into Profiles.
ASP.NET Core dependency injection takes care of passing an instance of SchoolContext into the controller. You configured that in the Startup class. The controller contains an Index action method, which displays all students in the database.
First I would recommend using JsonPatchDocument for your update:
[HttpPatch("{id}")]
public IActionResult Patch(int id, [FromBody] JsonPatchDocument<CustomerDTO> patchDocument)
{
var customer = context.EntityWithRelationships.SingleOrDefault(e => e.Id == id);
var dto = mapper.Map<CustomerDTO>(customer);
patchDocument.ApplyTo(dto);
var updated = mapper.Map(dto, customer);
context.Entry(entity).CurrentValues.SetValues(updated);
context.SaveChanges();
return NoContent();
}
And secound you should take advantage of AutoMapper.Collections.EFCore. This is how I configured AutoMapper in Startup.cs
with an extension method, so that I´m able to call services.AddAutoMapper()
without the whole configuration-code:
public static IServiceCollection AddAutoMapper(this IServiceCollection services)
{
var config = new MapperConfiguration(cfg =>
{
cfg.AddCollectionMappers();
cfg.UseEntityFrameworkCoreModel<MyContext>(services);
cfg.AddProfile(new YourProfile()); // <- you can do this however you like
});
IMapper mapper = config.CreateMapper();
return services.AddSingleton(mapper);
}
This is what YourProfile
should look like:
public YourProfile()
{
CreateMap<Person, PersonDTO>(MemberList.Destination)
.EqualityComparison((p, dto) => p.Id == dto.Id)
.ReverseMap();
CreateMap<Customer, CustomerDTO>(MemberList.Destination)
.ReverseMap();
}
I have a similar object-graph an this works fine for me.
EDIT I use LazyLoading, if you don´t you have to explicitly load navigationProperties/Collections.
AutoMapper
is the best solution.
You can do it very easily like this :
Mapper.CreateMap<Customer, CustomerDto>();
Mapper.CreateMap<CustomerDto, Customer>();
Mapper.CreateMap<Person, PersonDto>();
Mapper.CreateMap<PersonDto, Person>();
Note : Because AutoMapper
will automatically map the List<Person>
to List<PersonDto>
.since they have same name
, and there is already a mapping from Person
to PersonDto
.
If you need to know how to inject it to ASP.net core,you have to see this article : Integrating AutoMapper with ASP.NET Core DI
Auto mapping between DTOs and entities
Mapping using attributes and extension methods
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With