web-dev-qa-db-ja.com

ASP.NET CoreとEF Core-DTOコレクションのマッピング

データソースとしてEF Coreコンテキストを使用して、JavaScriptからASP.NET Core(Web API)への子オブジェクトのコレクションを持つDTOオブジェクトを(POST/PUT)使用しようとしています。

メインのDTOクラスは次のようなものです(もちろん簡略化されています):

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

私が実際に知らないのは、どの人物が追加/更新/削除されたかなどを見つけるためだけに、多くのコードを含まない方法でこれをCustomerエンティティークラスにマップする方法です。

AutoMapperで少し遊んでみましたが、このシナリオ(複雑なオブジェクト構造)とコレクションでは、EF CoreでNiceを実際に再生するようには見えません。

これに関するいくつかのアドバイスを探した後、私は良いアプローチがどうなるかについての良いリソースを見つけていません。私の質問は基本的に:「複雑な」DTOを使用しないようにJSクライアントを再設計する必要があるか、またはこれは私のDTOとエンティティモデル間のマッピングレイヤーによって「処理する必要がある」か、または他の適切な解決策がないかどうか知っていますか?

AutoMapperとオブジェクト間の手動マッピングの両方で解決できましたが、ソリューションはどれも正しく感じられず、多くのボイラープレートコードですぐにかなり複雑になります。

編集:

次の記事では、AutoMapperとEF Coreに関して私が何を参照しているかについて説明します。複雑なコードではありませんが、これを管理するための「最良の」方法であるかどうかを知りたいだけです。

(記事のコードは、上記のコード例に合わせて編集されています)

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;

上記のコードは、一般的な拡張メソッドとして記述されています。

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

編集:

私が本当に望んでいることの1つは、マッパー(またはマッピングコードの複雑化を必要とする他のソリューション)を使用するたびにMapCollection()拡張を使用する必要がない1か所(プロファイル)でAutoMapper構成を削除することです。

だから私はこのような拡張メソッドを作成しました

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

次に、マッピングを作成すると、次のようになります。

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

これにより、マッピング時に次のように使用できます。

_mapper.Map(customerDto, customer);

そして、リゾルバーがマッピングを処理します。

11
jmw

AutoMapperが最適なソリューションです。

あなたはこれをとても簡単に行うことができます:

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

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

注:AutoMapperList<Person>List<PersonDto>に自動的にマッピングします。これは、same nameがあり、PersonからPersonDtoへ。

ASP.netコアに挿入する方法を知る必要がある場合は、次の記事を参照する必要があります: AutoMapperとASP.NET Core DI

DTOとエンティティ間の自動マッピング

属性と拡張メソッドを使用したマッピング

6
Sampath

私はかなり長い間、同じ問題に苦労していました。多くの記事を調べた後、私はあなたと共有している独自の実装を思いつきました。

まず、カスタム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;
        }
    }
}

次に、AutoMapperを構成し、特定のマッピングを追加します。

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

この実装では、コンストラクターで渡されるkeyMatch関数により、オブジェクトマッチングロジックを完全にカスタマイズできます。渡されたオブジェクトがマッピングに適しているかどうかを何らかの理由で確認する必要がある場合は、追加のsaveOnlyIf関数を渡すこともできます(私の場合、マッピングされていないオブジェクトをコレクションに追加する必要がある場合があります)追加の検証に合格しない)。

その後、例えばコントローラーで、切断されたグラフを更新する場合は、以下を実行する必要があります。

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

これは私にとってはうまくいきます。この実装がこの質問の著者が編集した投稿で提案したものよりも適しているかどうかはあなた次第です:)

1
rosko

まず、更新に JsonPatchDocument を使用することをお勧めします。

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

そして、secoundは AutoMapper.Collections.EFCore を利用する必要があります。これは、拡張メソッドを使用して_Startup.cs_でAutoMapperを構成した方法です。これにより、構成コード全体なしでservices.AddAutoMapper()を呼び出すことができます。

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

YourProfileは次のようになります。

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

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

私は同様のオブジェクトグラフを持っていますが、これは私にとってはうまくいきます。

[〜#〜] edit [〜#〜]navigationProperties/Collectionsを明示的にロードする必要がない場合は、LazyLoadingを使用します。

0
Joshit