Adaptamos o AutoMapper para nós mesmos

O AutoMapper é uma das principais ferramentas usadas no desenvolvimento de aplicativos corporativos, por isso quero escrever o mínimo de código possível ao definir o mapeamento de entidades.


Não gosto de duplicação no MapFrom com projeções amplas.


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

Gostaria de reescrevê-lo assim:


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

Projectto


O AutoMapper pode construir o mapeamento na memória e convertê-lo em SQL; ele completa a Expressão, projetando no DTO de acordo com as regras que você descreveu nos perfis.


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

80% do mapeamento que tenho que escrever é o mapeamento que completa a Expressão do IQueryble.


Isso é muito conveniente:


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

Em um estilo declarativo, formamos uma consulta na tabela Pupils, adicionamos filtragem, projetamos no DTO desejado e devolvemos ao cliente, para que você possa escrever todos os métodos de leitura de uma interface CRUD simples e tudo isso será feito no nível do banco de dados.


É verdade que em aplicativos sérios essas ações dificilmente satisfarão os clientes.


Contras AutoMapper'a


1) É muito detalhado, com o mapeamento "amplo" você precisa escrever regras que não cabem em uma linha de código.


Os perfis crescem e se transformam em arquivos de código que são gravados uma vez e mudam apenas ao refatorar nomes.


2) Se você usar o mapeamento de acordo com a convenção, o nome será perdido
propriedades no DTO:


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

3) Falta de segurança do tipo


1 e 2 são momentos desagradáveis, mas você pode atendê-los, mas com a falta de segurança do tipo ao se registrar, já é mais difícil atendê-los, isso não deve ser compilado:


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

Queremos receber informações sobre esses erros no estágio de compilação, e não no tempo de execução.


Usando wrappers de extensão para eliminar esses pontos.


Escrevendo um wrapper


Por que o registro deve ser escrito dessa maneira?


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

Muito mais conciso:


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

O método To aceitará tuplas se você precisar especificar regras de mapeamento


IMapping <TSource, TDest> é a interface do automaper na qual os métodos ForMember, ForAll () são definidos ... todos esses métodos retornam isso (Fluent Api).


Retornaremos o wrapper para lembrar Expressão do método From


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

Agora, o programador, depois de escrever o método From, verá imediatamente a sobrecarga do método To ; assim, diremos a ele a API; nesses casos, podemos perceber todos os encantos dos métodos de extensão; expandimos o comportamento sem ter acesso de gravação às fontes do automapper


Nós tipificamos


Implementar um método To digitado é mais complicado.


Vamos tentar projetar esse método, precisamos dividi-lo o máximo possível e remover toda a lógica de outros métodos. Concordo imediatamente que limitaremos o número de parâmetros da tupla a dez.


Quando ocorre um problema semelhante em minha prática, olho imediatamente na direção de Roslyn, não tenho vontade de escrever muitos dos mesmos tipos de métodos e de copiar e colar, é mais fácil gerá-los.


Neste genérico vai nos ajudar. É necessário gerar 10 métodos com um número diferente de genéricos e parâmetros


A primeira abordagem para o projétil foi um pouco diferente, eu queria limitar os tipos de retorno de lambdas (int, string, booleano, DateTime) e não usar tipos universais.


A dificuldade é que, mesmo para três parâmetros, teremos de gerar 64 sobrecargas diferentes e, ao usar apenas genéricos 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) { ... } 

Mas esse não é o principal problema, geramos o código, levará algum tempo e obteremos todo o conjunto de métodos necessários.


O problema é diferente, o ReSharper não capta tantas sobrecargas e apenas se recusa a trabalhar, você perde o Intellisience e carrega o IDE.


Implementamos um método que leva uma tupla:


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

Primeiro, vamos verificar quais mapeamentos de propriedades podem ser encontrados por convenção; este é um método bastante simples; para cada propriedade no DTO, procuramos o caminho na entidade original. Os métodos terão que ser chamados reflexivamente, porque você precisa obter um lambda digitado, e seu tipo depende de prop.


É impossível registrar um lambda do tipo Expression <Func <TSource, object >>, e o AutoMapper mapeará todas as propriedades do DTO para digitar o objeto


 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 recebe uma tupla que define as regras de mapeamento, ele precisa estar "conectado" nele
FromExpression e expressão transmitidas para a tupla.


Invoque, o EF Core 2.0 não suporta, versões posteriores começaram a suportar. Isso permitirá que você faça uma "composição lambd":


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

Método 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)); } 

O método To foi projetado para ser fácil de estender ao adicionar parâmetros de tupla. Ao adicionar outra tupla aos parâmetros, você precisa adicionar outro parâmetro genérico e chamar o método RegisterRule para o novo parâmetro.


Um exemplo para dois parâmetros:


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

Usamos CSharpSyntaxRewriter , este é um visitante que percorre os nós da árvore de sintaxe. Tomamos como base um método com To com um argumento e adicionamos um parâmetro genérico e chamamos 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); } 

O ReWriteMethodInfo contém os nós da árvore de sintaxe gerados que você precisa adicionar. Depois disso, obtemos uma lista de 10 objetos do tipo MethodDeclarationSyntax (uma árvore de sintaxe que representa um método).


Na próxima etapa, pegamos a classe na qual o método do modelo está e escrevemos todos os novos métodos usando outro Visitor, no qual redefinimos o VisitClassDeclatation.


O método Update permite editar um nó de árvore existente, sob o capô itera todos os argumentos passados ​​e, se pelo menos um difere do original, cria um novo nó.


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

No final, obtemos o SyntaxNode - uma classe com métodos adicionados, gravamos o nó em um novo arquivo.Agora sobrecarregamos o método To , que leva de 1 a 10 tuplas e um mapeamento muito mais conciso.


Ponto de expansão


Vejamos o AutoMapper como algo mais. O provedor consultável não pode analisar muitas consultas, e uma certa parte dessas consultas pode ser reescrita de maneira diferente. É aqui que o AutoMapper entra em ação. A extensão é um ponto de extensão em que podemos adicionar nossas próprias regras.


Usaremos o visitante do artigo anterior que substitui a interpolação de seqüência de caracteres por concatenação no método RegusterRule.Como resultado, todas as expressões que definem o mapeamento da entidade passarão por esse visitante, eliminando assim a necessidade de chamar ReWrite a cada vez. Isso não é uma panacéia. gerenciar é uma projeção, mas ainda facilita a vida.


Também podemos adicionar algumas extensões convenientes, por exemplo, para mapeamento por condição:


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

O principal é não brincar com isso e não começar a transferir lógica complexa para o nível de exibição
Github

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


All Articles