Expressão lambda no construtor de atributo

Eu criei uma class Attribute chamada RelatedPropertyAttribute :

 [AttributeUsage(AttributeTargets.Property)] public class RelatedPropertyAttribute: Attribute { public string RelatedProperty { get; private set; } public RelatedPropertyAttribute(string relatedProperty) { RelatedProperty = relatedProperty; } } 

Eu uso isso para indicar propriedades relacionadas em uma class. Exemplo de como eu usaria:

 public class MyClass { public int EmployeeID { get; set; } [RelatedProperty("EmployeeID")] public int EmployeeNumber { get; set; } } 

Eu gostaria de usar expressões lambda para que eu possa passar um tipo forte para o construtor do meu atributo, e não uma “string mágica”. Desta forma eu posso explorar a verificação do tipo de compilador. Por exemplo:

 public class MyClass { public int EmployeeID { get; set; } [RelatedProperty(x => x.EmployeeID)] public int EmployeeNumber { get; set; } } 

Eu pensei que poderia fazer isso com o seguinte, mas não é permitido pelo compilador:

 public RelatedPropertyAttribute(Expression<Func> propertyExpression) { ... } 

Erro:

O tipo não genérico ‘RelatedPropertyAttribute’ não pode ser usado com argumentos de tipo

Como posso conseguir isso?

Você não pode

  • você não pode criar tipos de atributos genéricos (simplesmente não é permitido); da mesma forma, nenhuma syntax para usar atributos genéricos ( [Foo] ) é definida
  • você não pode usar lambdas em inicializadores de atributo – os valores disponíveis para passar para atributos são muito limitados e simplesmente não incluem expressões (que são muito complexas e são objects de tempo de execução, não literais de tempo de compilation)

Ter um atributo genérico não é possível de maneira convencional. No entanto C # e VB não suportam, mas o CLR faz. Se você quiser escrever algum código IL, é possível.

Vamos pegar seu código:

 [AttributeUsage(AttributeTargets.Property)] public class RelatedPropertyAttribute: Attribute { public string RelatedProperty { get; private set; } public RelatedPropertyAttribute(string relatedProperty) { RelatedProperty = relatedProperty; } } 

Compile o código, abra o assembly com ILSpy ou ILDasm e, em seguida, despeje o conteúdo em um arquivo de texto. A IL da sua declaração de class de atributo ficará assim:

 .class public auto ansi beforefieldinit RelatedPropertyAttribute extends [mscorlib]System.Attribute 

No arquivo de texto, você pode tornar o atributo genérico. Existem várias coisas que precisam ser alteradas.

Isso pode ser feito simplesmente mudando o IL e o CLR não reclamará:

 .class public abstract auto ansi beforefieldinit RelatedPropertyAttribute`1 extends [mscorlib]System.Attribute 

e agora você pode alterar o tipo de relatedProperty da string para o tipo genérico.

Por exemplo:

 .method public hidebysig specialname rtspecialname instance void .ctor ( string relatedProperty ) cil managed 

mude para:

 .method public hidebysig specialname rtspecialname instance void .ctor ( !T relatedProperty ) cil managed 

Há muitos frameworks para fazer um trabalho “sujo” como esse: Mono.Cecil ou CCI .

Como eu já disse, não é uma solução limpa orientada a objects, mas apenas queria apontar uma outra maneira de quebrar o limite de C # e VB.

Há uma leitura interessante em torno deste tópico, confira este livro.

Espero que ajude.

Se você estiver usando o C # 6.0, você pode usar o nome

Usado para obter o nome da cadeia simples (não qualificada) de uma variável, tipo ou membro. Ao relatar erros no código, conectando links MVC (controlador de exibição de modelo), events alterados de propriedades de acionamento, etc., você geralmente deseja capturar o nome da cadeia de um método. Usar nameof ajuda a manter seu código válido ao renomear definições. Antes você tinha que usar literais de string para se referir a definições, o que é frágil ao renomear elementos de código porque as ferramentas não sabem verificar estes literais de string.

Com ele você pode usar seu atributo assim:

 public class MyClass { public int EmployeeID { get; set; } [RelatedProperty(nameof(EmployeeID))] public int EmployeeNumber { get; set; } } 

Uma das soluções possíveis é definir a class para cada relação de propriedade e referenciá-la por
operador typeof () no construtor de atributo.

Atualizada:

Por exemplo:

 [AttributeUsage(AttributeTargets.Property)] public class RelatedPropertyAttribute : Attribute { public Type RelatedProperty { get; private set; } public RelatedPropertyAttribute(Type relatedProperty) { RelatedProperty = relatedProperty; } } public class PropertyRelation { private readonly Func _propGetter; public PropertyRelation(Func propGetter) { _propGetter = propGetter; } public TProperty GetProperty(TOwner owner) { return _propGetter(owner); } } public class MyClass { public int EmployeeId { get; set; } [RelatedProperty(typeof(EmployeeIdRelation))] public int EmployeeNumber { get; set; } public class EmployeeIdRelation : PropertyRelation { public EmployeeIdRelation() : base(@class => @class.EmployeeId) { } } } 

Você não pode. Os tipos de atributos são limitados, conforme escrito aqui . Minha sugestão, tente avaliar sua expressão lambda externamente e use um dos seguintes tipos:

  • Tipos simples (bool, byte, char, short, int, long, float e double)
  • corda
  • Tipo de sistema
  • enums
  • object (O argumento para um parâmetro de atributo do tipo object deve ser um valor constante de um dos tipos acima).
  • Matrizes unidimensionais de qualquer um dos tipos acima

Para expandir meu comentário , essa é uma maneira de realizar sua tarefa com uma abordagem diferente. Você diz que quer “indicar propriedades relacionadas em uma class” e que “gostaria de usar expressões lambda para que eu possa passar um tipo forte para o construtor do meu atributo, e não uma” string mágica “. Dessa forma, eu posso explorar verificação do tipo de compilador “.

Aqui, então, é uma maneira de indicar propriedades relacionadas que são compiladas em tempo de digitação e não possuem nenhuma sequência mágica:

 public class MyClass { public int EmployeeId { get; set; } public int EmployeeNumber { get; set; } } 

Esta é a class em consideração. Queremos indicar que EmployeeId e EmployeeNumber estão relacionados. Para um pouco de concisão de código, vamos colocar esse tipo de alias no topo do arquivo de código. Não é necessário, mas torna o código menos intimidante:

 using MyClassPropertyTuple = System.Tuple< System.Linq.Expressions.Expression>, System.Linq.Expressions.Expression> >; 

Isso torna MyClassPropertyTuple um alias para um Tuple de duas Expression , cada uma delas captura a definição de uma function de um MyClass para um object. Por exemplo, os getters de propriedade no MyClass são essas funções.

Agora vamos capturar o relacionamento. Aqui eu fiz uma propriedade estática no MyClass , mas essa lista pode ser definida em qualquer lugar:

 public class MyClass { public static List Relationships = new List { new MyClassPropertyTuple(c => c.EmployeeId, c => c.EmployeeNumber) }; } 

O compilador C # sabe que estamos construindo um Tuple de Expression s, portanto, não precisamos de nenhuma Tuple explícita na frente dessas expressões lambda – elas são automaticamente transformadas em Expression s.

É basicamente isso em termos de definição – as menções EmployeeNumber e EmployeeNumber são fortemente tipadas e aplicadas em tempo de compilation, e as ferramentas de refatoração que fazem renomeação de propriedade devem ser capazes de encontrar esses usos durante uma renomeação (o ReSharper definitivamente pode). Não há cordas mágicas aqui.


Mas é claro que também queremos ser capazes de interrogar relacionamentos em tempo de execução (suponho!). Eu não sei exatamente como você quer fazer isso, então esse código é apenas ilustrativo.

 class Program { static void Main(string[] args) { var propertyInfo1FromReflection = typeof(MyClass).GetProperty("EmployeeId"); var propertyInfo2FromReflection = typeof(MyClass).GetProperty("EmployeeNumber"); var e1 = MyClass.Relationships[0].Item1; foreach (var relationship in MyClass.Relationships) { var body1 = (UnaryExpression)relationship.Item1.Body; var operand1 = (MemberExpression)body1.Operand; var propertyInfo1FromExpression = operand1.Member; var body2 = (UnaryExpression)relationship.Item2.Body; var operand2 = (MemberExpression)body2.Operand; var propertyInfo2FromExpression = operand2.Member; Console.WriteLine(propertyInfo1FromExpression.Name); Console.WriteLine(propertyInfo2FromExpression.Name); Console.WriteLine(propertyInfo1FromExpression == propertyInfo1FromReflection); Console.WriteLine(propertyInfo2FromExpression == propertyInfo2FromReflection); } } } 

O código para propertyInfo1FromExpression e propertyInfo2FromExpression aqui eu trabalhei com o uso criterioso da janela Watch durante a debugging – geralmente é assim que eu trabalho o que uma tree Expression realmente contém.

Correndo isso irá produzir

 EmployeeId EmployeeNumber True True 

mostrando que podemos extrair com sucesso os detalhes das propriedades relacionadas e (crucialmente) eles são referência-idênticos aos PropertyInfo obtidos por outros meios . Espero que você possa usar isso em conjunto com qualquer abordagem que esteja realmente usando para especificar propriedades de interesse em tempo de execução.