Gere e compile o nome para indexar a tradução/mapeamento para uma reutilização mais rápida
-
11-12-2019 - |
Pergunta
Suponha que eu obtenha dados de um serviço (que não posso controlar) como:
public class Data
{
// an array of column names
public string[] ColumnNames { get; set; }
// an array of rows that contain arrays of strings as column values
public string[][] Rows { get; get; }
}
e na camada intermediária eu gostaria de mapear/traduzir isso para um IEnumerable<Entity>
onde os nomes das colunas em Data
talvez representados como propriedades em meu Entity
aula.Eu disse poderia porque posso não precisar de todos os dados retornados pelo serviço, mas apenas de alguns deles.
Transformação
Esta é uma abstração de um algoritmo que faria a tradução:
- criar um
IDictionary<string, int>
deColumnNames
para que eu possa mapear facilmente nomes de colunas individuais para índices de array em linhas individuais. - usar a reflexão para examinar meu
Entity
nomes de propriedades para que eu possa combiná-los com nomes de colunas - iterar através
Data.Rows
e criar o meuEntity
objetos e preencher propriedades de acordo com o mapeamento feito em #1.Provavelmente usando reflexão eSetValue
nas propriedades para defini-las.
Otimização
É claro que o algoritmo superior funcionaria, mas acho que, por usar reflexão, ele deveria fazer algum armazenamento em cache e possivelmente alguma compilação em tempo real, o que poderia acelerar consideravelmente as coisas.
Quando as etapas 1 e 2 forem concluídas, poderíamos gerar um método que pega um array de strings e instancia minhas entidades usando índices diretamente e compila-o e armazená-lo em cache para reutilização futura.
Normalmente recebo uma página de resultados, portanto, as solicitações subsequentes reutilizariam o mesmo método compilado.
Fato adicional
Isso não é obrigatório para a pergunta (e respostas), mas também criei dois atributos que ajudam no mapeamento de coluna para propriedade quando estes não correspondem nos nomes.Eu criei o mais óbvio MapNameAttribute
(que usa uma string e, opcionalmente, também habilita a diferenciação de maiúsculas e minúsculas) e IgnoreMappingAttribute
para propriedades no meu Entity
isso não deve ser mapeado para nenhum dado.Mas esses atributos são lidos na etapa 2 do algoritmo superior para que os nomes das propriedades sejam coletados e renomeados de acordo com esses metadados declarativos para que correspondam aos nomes das colunas.
Pergunta
Qual é a melhor e mais fácil maneira de gerar e compilar tal método?Expressões lambda? CSharpCodeProvider
aula?
Você talvez tenha um exemplo de código gerado e compilado que faça algo semelhante?Acho que os mapeamentos são um cenário bastante comum.
Observação:Enquanto isso, examinarei o PetaPoco (e talvez também o Massive) porque ambos fazem compilação e armazenamento em cache dinamicamente exatamente para fins de mapeamento.
Solução
Sugestão: obter FastMember do NuGet
Depois é só usar:
var accessor = TypeAccessor.Create(typeof(Entity));
Então, apenas no seu loop, quando você encontrar o memberName
e newValue
para a iteração atual:
accessor[obj, memberName] = newValue;
Isso foi projetado para fazer o que você está pedindo;internamente, ele mantém um conjunto de tipos como já visto antes.Quando um novo tipo é visto, ele cria uma nova subclasse de TypeAccessor
em tempo real (através TypeBuilder
) e armazena-o em cache.Cada único TypeAccessor
está ciente das propriedades desse tipo e basicamente apenas age como um:
switch(memberName) {
case "Foo": obj.Foo = (int)newValue;
case "Bar": obj.Bar = (string)newValue;
// etc
}
Como isso é armazenado em cache, você paga apenas qualquer custo (e não realmente um grande custo) na primeira vez que vê seu tipo;no resto do tempo, é grátis.Porque ele usa ILGenerator
diretamente, também evita qualquer abstração desnecessária, por exemplo, via Expression
ou CodeDom, então é o mais rápido possível.
(Devo também esclarecer que para dynamic
tipos, ou seja,tipos que implementam IDynamicMetaObjectProvider
, ele pode usar uma única instância para suportar todos os objetos).
Adicional:
O que você poderia fazer é:pegue o existente FastMember
código e edite-o para processar MapNameAttribute
e IgnoreMappingAttribute
durante WriteGetter
e WriteSetter
;então todo o vodu acontece no seu dados nomes, em vez de membro nomes.
Isso significaria mudar as linhas:
il.Emit(OpCodes.Ldstr, prop.Name);
e
il.Emit(OpCodes.Ldstr, field.Name);
em ambos WriteGetter
e WriteSetter
, e fazendo um continue
no início do foreach
loops se deve ser ignorado.