ASP.NET Core with EF Core - DTO Collection mapping

前端 未结 3 2099
隐瞒了意图╮
隐瞒了意图╮ 2020-12-14 04:32

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.

<
相关标签:
3条回答
  • 2020-12-14 04:59

    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.

    0 讨论(0)
  • 2020-12-14 05:01

    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

    0 讨论(0)
  • 2020-12-14 05:04

    I was struggling with the very same issue for quite some time. After digging through many articles I've came up with my own implementation which I'm sharing with you.

    First of all I've created a custom IMemberValueResolver.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace AutoMapper
    {
        public class CollectionValueResolver<TDto, TItemDto, TModel, TItemModel> : IMemberValueResolver<TDto, TModel, IEnumerable<TItemDto>, IEnumerable<TItemModel>>
            where TDto : class
            where TModel : class
        {
            private readonly Func<TItemDto, TItemModel, bool> _keyMatch;
            private readonly Func<TItemDto, bool> _saveOnlyIf;
    
            public CollectionValueResolver(Func<TItemDto, TItemModel, bool> keyMatch, Func<TItemDto, bool> saveOnlyIf = null)
            {
                _keyMatch = keyMatch;
                _saveOnlyIf = saveOnlyIf;
            }
    
            public IEnumerable<TItemModel> Resolve(TDto sourceDto, TModel destinationModel, IEnumerable<TItemDto> sourceDtos, IEnumerable<TItemModel> destinationModels, ResolutionContext context)
            {
                var mapper = context.Mapper;
    
                var models = new List<TItemModel>();
                foreach (var dto in sourceDtos)
                {
                    if (_saveOnlyIf == null || _saveOnlyIf(dto))
                    {
                        var existingModel = destinationModels.SingleOrDefault(model => _keyMatch(dto, model));
                        if (EqualityComparer<TItemModel>.Default.Equals(existingModel, default(TItemModel)))
                        {
                            models.Add(mapper.Map<TItemModel>(dto));
                        }
                        else
                        {
                            mapper.Map(dto, existingModel);
                            models.Add(existingModel);
                        }
                    }
                }
    
                return models;
            }
        }
    }
    

    Then I configure AutoMapper and add my specific mapping:

    cfg.CreateMap<TDto, TModel>()
        .ForMember(dst => dst.DestinationCollection, opts =>
            opts.ResolveUsing(new CollectionValueResolver<TDto, TItemDto, TModel, TItemModel>((src, dst) => src.Id == dst.SomeOtherId, src => !string.IsNullOrEmpty(src.ThisValueShouldntBeEmpty)), src => src.SourceCollection));
    

    This implementation allows me to fully customize my object matching logic due to keyMatch function that is passed in constructor. You can also pass an additional saveOnlyIf function if you for some reason need to verify passed objects if they are suitable for mapping (in my case there were some objects that shouldn't be mapped and added to collection if they didn't pass an extra validation).

    Then e.g. in your controller if you want to update your disconnected graph you should do the following:

    var model = await Service.GetAsync(dto.Id); // obtain existing object from db
    Mapper.Map(dto, model);
    await Service.UpdateAsync(model);
    

    This works for me. It's up to you if this implementation suits you better than what author of this question proposed in his edited post:)

    0 讨论(0)
提交回复
热议问题