AutoMapperを自分用に適合させます

AutoMapperは、エンタープライズアプリケーションの開発に使用される主要なツールの1つであるため、エンティティマッピングを定義する際には、できるだけ少ないコードを記述したいと思います。


MapFromでの広い投影による複製は好きではありません。


CreateMap<Pupil, PupilDto>() .ForMember(x => x.Name, s => s.MapFrom(x => x.Identity.Passport.Name)) .ForMember(x => x.Surname, s => s.MapFrom(x => x.Identity.Passport.Surname)) .ForMember(x => x.Age, s => s.MapFrom(x => x.Identity.Passport.Age)) .ForMember(x => x.Number, s => s.MapFrom(x => x.Identity.Passport.Number)) 

このように書き直したいと思います。


 CreateMap<Pupil, PupilDto>() .From(x=>x.IdentityCard.Passport).To() 

Projectto


AutoMapperは、メモリ内でマッピングを構築し、SQLに変換し、Expressionを完成させ、プロファイルで説明したルールに従ってDTOに投影します。


 EntityQueryable.Select(dtoPupil => new PupilDto() { Name = dtoPupil.Identity.Passport, Surname = dtoPupil.Identity.Passport.Surname}) 

記述しなければならないマッピングの80%は、IQuerybleの式を完成させるマッピングです。


これは非常に便利です。


 public ActionResult<IEnumerable<PupilDto>> GetAdultPupils(){ var result = _context.Pupils .Where(x=>x.Identity.Passport.Age >= 18 && ...) .ProjectTo<PupilDto>().ToList(); return result; } 

宣言型スタイルで、Pupilsテーブルへのクエリを作成し、フィルタリングを追加し、目的のDTOに投影してクライアントに返しました。これにより、単純なCRUDインターフェイスのすべての読み取りメソッドを記述できます。これはすべてデータベースレベルで行われます。


確かに、深刻なアプリケーションでは、そのようなアクションは顧客を満足させることはまずありません。


短所AutoMapper'a


1)非常に冗長で、「ワイド」マッピングでは、1行のコードに収まらないルールを記述する必要があります。


プロファイルは成長し、一度書かれたコードのアーカイブに変わり、名前のリファクタリング時にのみ変更されます。


2)慣習に従ってマッピングを使用すると、名前の簡潔さは失われます
DTOのプロパティ:


 public class PupilDto { //  Pupil       IdentityCard // IdentityCard     Passport public string IdentityCardPassportName { get; set; } public string IdentityCardPassportSurname { get; set; } } 

3)型安全性の欠如


1と2は不快な瞬間ですが、我慢することはできますが、登録時の型安全性の欠如により、我慢するのはすでに難しくなっているため、これはコンパイルすべきではありません。


 // Name - string // Age - int ForMember(x => x.Age, s => s.MapFrom(x => x.Identity.Passport.Name) 

このようなエラーに関する情報は、実行時ではなくコンパイル段階で取得する必要があります。


拡張ラッパーを使用してこれらのポイントを排除します。


ラッパーを書く


なぜ登録はこのように書かれるべきなのですか


 CreateMap<Pupil, PupilDto>() .ForMember(x => x.Name, s => s.MapFrom(x => x.Identity.Passport.Name)) .ForMember(x => x.Surname, s => s.MapFrom(x => x.Identity.Passport.Surname)) .ForMember(x => x.Age, s => s.MapFrom(x => x.Identity.Passport.Age)) .ForMember(x => x.House, s => s.MapFrom(x => x.Address.House)) .ForMember(x => x.Street, s => s.MapFrom(x => x.Address.Street)) .ForMember(x => x.Country, s => s.MapFrom(x => x.Address.Country)) .ForMember(x => x.Surname, s => s.MapFrom(x => x.Identity.Passport.Age)) .ForMember(x => x.Group, s => s.MapFrom(x=>x.EducationCard.StudyGroup.Number)) 

はるかに簡潔:


 CreateMap<Pupil,PupilDto>() //    // PassportName = Passport.Name, PassportSurname = Passport.Surname .From(x => x.IdentityCard.Passport).To() // House,Street,Country -   .From(x => x.Address).To() //    -  DTO,  -  .From(x => x.EducationCard.Group).To((x => x.Group,x => x.Number)); 

マッピング規則を指定する必要がある場合、 Toメソッドはタプルを受け入れます。


IMapping <TSource、TDest>は、ForMember、ForAll()メソッドが定義されているオートマッパーのインターフェイスです...これらのすべてのメソッドはこれを返します(Fluent Api)。


FromメソッドからExpressionを記憶するラッパーを返します


 public static MapperExpressionWrapper<TSource, TDest, TProjection> From<TSource, TDest, TProjection> (this IMappingExpression<TSource, TDest> mapping, Expression<Func<TSource, TProjection>> expression) => new MapperExpressionWrapper<TSource, TDest, TProjection>(mapping, expression); 

これで、Fromメソッドを記述したプログラマーは、 Toメソッドのオーバーロードをすぐに見ることができます。それにより、APIに通知します。そのような場合、拡張メソッドのすべての魅力を実現でき、オートマッパーソースへの書き込みアクセスなしで動作を拡張します


類型化する


型付きToメソッドの実装はより複雑です。


このメソッドを設計してみましょう。可能な限り細分化して、他のメソッドのすべてのロジックを取り出す必要があります。 タプルパラメータの数を10に制限することにすぐに同意します。


私の練習で同様の問題が発生した場合、私はすぐにRoslynの方向に目を向けます。同じタイプのメソッドの多くを書きたくないので、コピーペーストを行います。


これは、generic'iに役立ちます。 ジェネリックとパラメーターの数が異なる10個のメソッドを生成する必要があります


発射体への最初のアプローチは少し異なっていました。私はラムダの戻り値の型(int、string、boolean、DateTime)を制限し、ユニバーサル型を使用したくないと考えました。


難点は、3つのパラメーターでさえ、64の異なるオーバーロードを生成する必要があることです。ジェネリックを使用する場合は、1つだけです。


 IMappingExpression<TSource, TDest> To<TSource, TDest, TProjection,T,T1, T2, T3>( this MapperExpressionWrapper<TSource,TDest,TProjection> mapperExpressionWrapper, (Expression<Func<TDest, T>>, Expression<Func<TProjection, T>>) arg0, (Expression<Func<TDest, T1>>, Expression<Func<TProjection, T1>>) arg1, (Expression<Func<TDest, T2>>, Expression<Func<TProjection, T2>>) arg2, (Expression<Func<TDest, T3>>, Expression<Func<TProjection, T3>>) arg3) { ... } 

しかし、これは主な問題ではありません。コードを生成します。時間がかかり、必要なメソッドのセット全体を取得します。


問題は異なります。ReSharperはそれほど多くのオーバーロードをピックアップせず、動作を拒否します。インテリジェンスを失い、IDEをロードします。


1つのタプルを取るメソッドを実装します。


 public static IMappingExpression<TSource, TDest> To <TSource, TDest, TProjection, T>(this MapperExpressionWrapper<TSource,TDest,TProjection> mapperExpressionWrapper, (Expression<Func<TDest, T>>, Expression<Func<TProjection, T>>) arg0) { //    RegisterByConvention(mapperExpressionWrapper); //    expreession RegisterRule(mapperExpressionWrapper, arg0); //  IMappingExpression,     //   extension  return mapperExpressionWrapper.MappingExpression; } 

最初に、慣例によりどのプロパティマッピングが見つかるかを確認します。これはかなり単純な方法で、DTOの各プロパティに対して元のエンティティのパスを探します。 型付きラムダを取得する必要があり、その型はpropに依存するため、メソッドは再帰的に呼び出す必要があります。


Expression <Func <TSource、object >>型のラムダを登録することはできません。AutoMapperはすべてのDTOプロパティをtypeオブジェクトにマップします


 private static void RegisterByConvention<TSource, TDest, TProjection>( MapperExpressionWrapper<TSource, TDest, TProjection> mapperExpressionWrapper) { var properties = typeof(TDest).GetProperties().ToList(); properties.ForEach(prop => { // mapperExpressionWrapper.FromExpression = x=>x.Identity.Passport // prop.Name = Name // ruleByConvention Expression<Func<Pupil,string>> x=>x.Identity.Passport.Name var ruleByConvention = _cachedMethodInfo .GetMethod(nameof(HelpersMethod.GetRuleByConvention)) .MakeGenericMethod(typeof(TSource), typeof(TProjection), prop.PropertyType) .Invoke(null, new object[] {prop, mapperExpressionWrapper.FromExpression}); if (ruleByConvention == null) return; // mapperExpressionWrapper.MappingExpression.ForMember(prop.Name, s => s.MapFrom((dynamic) ruleByConvention)); }); } 

RegisterRuleは、マッピングルールを定義するタプルを受け取ります。タプルは、その中に「接続」する必要があります
タプルに渡されるFromExpressionと式。


これはExpression.Invokeに役立ちます。EFCore 2.0ではサポートされていませんでしたが、以降のバージョンではサポートが開始されました。 これにより、「子羊の作曲」を作成できます。


 Expression<Func<Pupil,StudyGroup>> from = x=>x.EducationCard.StudyGroup; Expression<Func<StudyGroup,int>> @for = x=>x.Number; //invoke = x=>x.EducationCard.StudyGroup.Number; var composition = Expression.Lambda<Func<Pupil, string>>( Expression.Invoke(@for,from.Body),from.Parameters.First()) 

RegisterRuleメソッド:


 private static void RegisterRule<TSource, TDest, TProjection, T (MapperExpressionWrapper<TSource,TDest,TProjection> mapperExpressionWrapper, (Expression<Func<TDest, T>>, Expression<Func<TProjection, T>>) rule) { //rule = (x=>x.Group,x=>x.Number) var (from, @for) = rule; //      @for = (Expression<Func<TProjection, T>>) _interpolationReplacer.Visit(@for); //mapperExpressionWrapper.FromExpression = (x=>x.EducationCard.StudyGroup) var result = Expression.Lambda<Func<TSource, T>>( Expression.Invoke(@for, mapperExpressionWrapper.FromExpression.Body), mapperExpressionWrapper.FromExpression.Parameters.First()); var destPropertyName = from.PropertiesStr().First(); // result = x => Invoke(x => x.Number, x.EducationCard.StudyGroup) //  ,  result = x=>x.EducationCard.StudyCard.Number mapperExpressionWrapper.MappingExpression .ForMember(destPropertyName, s => s.MapFrom(result)); } 

Toメソッドは、タプルパラメータを追加するときに簡単に拡張できるように設計されています。 別のタプルをパラメーターに追加する場合、別のジェネリックパラメーターを追加し、新しいパラメーターに対してRegisterRuleメソッドを呼び出す必要があります。


2つのパラメーターの例:


 IMappingExpression<TSource, TDest> To<TSource, TDest, TProjection, T, T1> (this MapperExpressionWrapper<TSource,TDest,TProjection>mapperExpressionWrapper, (Expression<Func<TDest, T>>, Expression<Func<TProjection, T>>) arg0, (Expression<Func<TDest, T1>>, Expression<Func<TProjection, T1>>) arg1) { RegisterByConvention(mapperExpressionWrapper); RegisterRule(mapperExpressionWrapper, arg0); RegisterRule(mapperExpressionWrapper, arg1); return mapperExpressionWrapper.MappingExpression; } 

CSharpSyntaxRewriterを使用します 。これは、構文ツリーのノードをウォークスルーするビジターです。 基本として、1つの引数を持つToを持つメソッドを取得し、ジェネリックパラメーターを追加してRegisterRuleを呼び出します


 public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) { //     To if (node.Identifier.Value.ToString() != "To") return base.VisitMethodDeclaration(node); // returnStatement = return mapperExpressionWrapper.MappingExpression; var returnStatement = node.Body.Statements.Last(); //beforeReturnStatements: //[RegisterByConvention(mapperExpressionWrapper), // RegisterRule(mapperExpressionWrapper, arg0)] var beforeReturnStatements = node.Body.Statements.SkipLast(1); //   RegisterRule  returStatement var newBody = SyntaxFactory.Block( beforeReturnStatements.Concat(ReWriteMethodInfo.Block.Statements) .Concat(new[] {returnStatement})); //     return node.Update( node.AttributeLists, node.Modifiers, node.ReturnType, node.ExplicitInterfaceSpecifier, node.Identifier, node.TypeParameterList.AddParameters (ReWriteMethodInfo.Generics.Parameters.ToArray()), node.ParameterList.AddParameters (ReWriteMethodInfo.AddedParameters.Parameters.ToArray()), node.ConstraintClauses, newBody, node.SemicolonToken); } 

ReWriteMethodInfoには、追加する必要がある生成された構文ツリーノードが含まれています。 その後、MethodDeclarationSyntax型(メソッドを表す構文ツリー)の10個のオブジェクトのリストを取得します。


次のステップでは、テンプレートメソッドToが含まれるクラスを取得し、VisitClassDeclatationを再定義する別の訪問者を使用して、すべての新しいメソッドをそのクラスに書き込みます。


Updateメソッドを使用すると、既存のツリーノードを編集できます。フードの下で、渡されたすべての引数を反復処理し、少なくとも1つが元の引数と異なる場合、新しいノードを作成します。


 public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) { //todo refactoring it return node.Update( node.AttributeLists, node.Modifiers, node.Keyword, node.Identifier, node.TypeParameterList, node.BaseList, node.ConstraintClauses, node.OpenBraceToken, new SyntaxList<MemberDeclarationSyntax>(ReWriteMethods), node.CloseBraceToken, node.SemicolonToken); } 

最後に、SyntaxNode(メソッドを追加したクラス)を取得し、ノードを新しいファイルに書き込みます。1〜10のタプルとより簡潔なマッピングを取るToメソッドのオーバーロードがあります。


拡張ポイント


AutoMapperをもっと見てみましょう。 クエリ可能なプロバイダーは非常に多くのクエリを解析できず、これらのクエリの特定の部分を異なる方法で書き換えることができます。 これがAutoMapperの出番です。拡張機能は、独自のルールを追加できる拡張ポイントです。


RegusterRuleメソッドで文字列補間を連結に置き換える前の記事のビジターを使用するため、エンティティからのマッピングを定義するすべての式はこのビジターを通過するため、毎回ReWriteを呼び出す必要はありません。管理することは予測ですが、それでも人生を楽にします。


また、条件によるマッピングなど、便利な拡張を追加することもできます。


 CreateMap<Passport,PassportDto>() .ToIf(x => x.Age, x => x < 18, x => $"{x.Age}", x => "Adult") 

主なことは、これで遊ぶことではなく、複雑なロジックを表示レベルに転送し始めることではない
Github



Source: https://habr.com/ru/post/J444934/


All Articles