web-dev-qa-db-fra.com

ASP.NET Core avec EF Core - Mappage de la collection DTO

J'essaie d'utiliser (POST/PUT) un objet DTO avec une collection d'objets enfants de JavaScript vers un ASP.NET Core (API Web) avec un contexte EF Core comme source de données.

La classe DTO principale est quelque chose comme ça ( simplifié bien sûr):

public class CustomerDto {
    public int Id { get;set }
    ...
    public IList<PersonDto> SomePersons { get; set; }
    ...
}

Ce que je ne sais pas vraiment, c'est comment mapper cela à la classe d'entité Client d'une manière qui n'inclut pas beaucoup de code juste pour savoir quelles personnes ont été ajoutées/mises à jour/supprimées, etc.

J'ai un peu joué avec AutoMapper mais il ne semble pas vraiment jouer Nice avec EF Core dans ce scénario (structure d'objet complexe) et collections.

Après avoir cherché sur Google des conseils à ce sujet, je n'ai pas trouvé de bonnes ressources sur ce que serait une bonne approche. Ma question est essentiellement: dois-je repenser le client JS pour ne pas utiliser de DTO "complexes" ou est-ce quelque chose qui "devrait" être géré par une couche de mappage entre mes DTO et le modèle d'entité ou existe-t-il une autre bonne solution que je ne suis pas conscient de?

J'ai pu le résoudre à la fois avec AutoMapper et et en mappant manuellement entre les objets mais aucune des solutions ne semble bonne et devient rapidement assez complexe avec beaucoup de code standard.

MODIFIER:

L'article suivant décrit ce à quoi je fais référence concernant AutoMapper et EF Core. Ce n'est pas du code compliqué mais je veux juste savoir si c'est la "meilleure" façon de gérer cela.

(Le code de l'article est modifié pour s'adapter à l'exemple de code ci-dessus)

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;

Le code ci-dessus est écrit comme une méthode d'extension générique.

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>);

Modifier:

Une chose que je veux vraiment est de supprimer la configuration d'AutoMapper en un seul endroit (Profile) sans avoir à utiliser l'extension MapCollection () chaque fois que j'utilise le mappeur (ou toute autre solution qui nécessite de compliquer le code de mappage).

J'ai donc créé une méthode d'extension comme celle-ci

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);
    }
}

Ensuite, quand je crée les mappages, je le fais comme ceci:

    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));
            });
        });

Ce qui me permet de l'utiliser comme ceci lors du mappage:

_mapper.Map(customerDto, customer);

Et le résolveur s'occupe du mappage.

11
jmw

AutoMapper est la meilleure solution.

Vous pouvez le faire très facilement comme ceci:

    Mapper.CreateMap<Customer, CustomerDto>();
    Mapper.CreateMap<CustomerDto, Customer>();

    Mapper.CreateMap<Person, PersonDto>();
    Mapper.CreateMap<PersonDto, Person>();

Remarque: Parce que AutoMapper mappera automatiquement le List<Person> à List<PersonDto>. puisqu'ils ont same name, et il existe déjà un mappage de Person vers PersonDto.

Si vous devez savoir comment l'injecter dans le noyau ASP.net, vous devez consulter cet article: Intégration d'AutoMapper avec ASP.NET Core DI

Mappage automatique entre les DTO et les entités

Mappage à l'aide d'attributs et de méthodes d'extension

6
Sampath

Je luttais avec le même problème depuis un certain temps. Après avoir fouillé dans de nombreux articles, j'ai trouvé ma propre implémentation que je partage avec vous.

Tout d'abord, j'ai créé un IMemberValueResolver personnalisé.

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;
        }
    }
}

Ensuite, je configure AutoMapper et ajoute mon mappage spécifique:

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));

Cette implémentation me permet de personnaliser entièrement ma logique de correspondance d'objet grâce à la fonction keyMatch qui est passée dans le constructeur. Vous pouvez également transmettre une fonction saveOnlyIf supplémentaire si, pour une raison quelconque, vous devez vérifier les objets passés s'ils conviennent au mappage (dans mon cas, certains objets ne devraient pas être mappés et ajoutés à la collection s'ils ne le faisaient pas). ne passe pas une validation supplémentaire).

Ensuite, par ex. dans votre contrôleur si vous souhaitez mettre à jour votre graphique déconnecté, vous devez procéder comme suit:

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

Cela fonctionne pour moi. A vous de voir si cette implémentation vous convient mieux que l'auteur de cette question proposé dans son billet édité :)

1
rosko

Tout d'abord, je recommanderais d'utiliser JsonPatchDocument pour votre mise à jour:

    [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();
    }

Et vous devriez profiter de AutoMapper.Collections.EFCore . Voici comment j'ai configuré AutoMapper dans Startup.cs Avec une méthode d'extension, pour que je puisse appeler services.AddAutoMapper() sans le code de configuration complet:

    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);
    }

Voici à quoi devrait ressembler YourProfile:

    public YourProfile()
    {
        CreateMap<Person, PersonDTO>(MemberList.Destination)
            .EqualityComparison((p, dto) => p.Id == dto.Id)
            .ReverseMap();

        CreateMap<Customer, CustomerDTO>(MemberList.Destination)
            .ReverseMap();
    }

J'ai un graphique d'objet similaire et cela fonctionne bien pour moi.

[~ # ~] modifier [~ # ~] J'utilise LazyLoading, si vous n'avez pas besoin de charger explicitement navigationProperties/Collections.

0
Joshit