web-dev-qa-db-fra.com

Expression Lambda dans le constructeur d'attributs

J'ai créé une classe Attribute appelée RelatedPropertyAttribute:

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

    public RelatedPropertyAttribute(string relatedProperty)
    {
        RelatedProperty = relatedProperty;
    }
}

J'utilise ceci pour indiquer les propriétés associées dans une classe. Exemple d'utilisation:

public class MyClass
{
    public int EmployeeID { get; set; }

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

J'aimerais utiliser des expressions lambda pour pouvoir passer un type fort au constructeur de mon attribut et non une "chaîne magique". De cette façon, je peux exploiter la vérification du type du compilateur. Par exemple:

public class MyClass
{
    public int EmployeeID { get; set; }

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

Je pensais pouvoir le faire avec ce qui suit, mais le compilateur ne le permet pas:

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

Erreur: 

Le type non générique 'RelatedPropertyAttribute' ne peut pas être utilisé avec arguments de type

Comment puis-je atteindre cet objectif?

36
davenewza

Vous ne pouvez pas

  • vous ne pouvez pas créer de types d'attributs génériques (ce n'est tout simplement pas autorisé); De même, aucune syntaxe n'est définie pour using generic ([Foo<SomeType>])
  • vous ne pouvez pas utiliser lambdas dans les initialiseurs d'attributs - les valeurs disponibles pour passer aux attributs sont très limitées et n'incluent tout simplement pas les expressions (qui sont très complexes et sont des objets d'exécution, pas des littéraux à la compilation)
32
Marc Gravell

Avoir un attribut générique n'est pas possible de manière conventionnelle. Cependant, C # et VB ne le supportent pas, mais le CLR le fait. Si vous voulez écrire du code IL, c'est possible.

Prenons votre code:

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

    public RelatedPropertyAttribute(string relatedProperty)
    {
       RelatedProperty = relatedProperty;
    }
}

Compilez le code, ouvrez l’Assembly avec ILSpy ou ILDasm , puis transférez le contenu dans un fichier texte L'IL de votre déclaration de classe d'attribut ressemblera à ceci:

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

Dans le fichier texte, vous pouvez ensuite rendre l'attribut générique. Il y a plusieurs choses à changer.

Cela peut être fait simplement en changeant le IL et le CLR ne se plaindra pas:

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

et maintenant vous pouvez changer le type de relatedProperty de chaîne en votre type générique. 

Par exemple:

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

changez le en:

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

Il y a beaucoup de cadres pour faire un travail "sale" comme celui-ci: Mono.Cecil ou CCI .

Comme je l'ai déjà dit, ce n'est pas une solution orientée objet propre, mais je voulais simplement indiquer un autre moyen de dépasser les limites de C # et de VB.

Il y a une lecture intéressante autour de ce sujet, jetez-y un œil ce livre.

J'espère que ça aide.

47
codingadventures

Si vous utilisez C # 6.0, vous pouvez utiliser nameof

Utilisé pour obtenir le nom de chaîne simple (non qualifié) d'une variable, type ou membre. Lorsque vous signalez des erreurs de code, connectez-vous liens modèle-vue-contrôleur (MVC), événements de modification de propriété d'activation, etc., vous souhaiterez souvent capturer le nom de chaîne d'une méthode. En utilisant nameof aide à garder votre code valide lorsque vous renommez des définitions. Avant vous deviez utiliser des littéraux de chaîne pour faire référence à des définitions, c'est-à-dire fragile lors du changement de nom d'éléments de code car les outils ne savent pas vérifier ces littéraux de chaîne.

avec cela, vous pouvez utiliser votre attribut comme ceci:

public class MyClass
{
    public int EmployeeID { get; set; }

    [RelatedProperty(nameof(EmployeeID))]
    public int EmployeeNumber { get; set; }
}
11
Ayman

Une des solutions de contournement possibles consiste à définir la classe de chaque relation de propriété et à la référencer en
opérateur typeof () dans le constructeur d'attributs.

Mis à jour:

Par exemple:

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

    public RelatedPropertyAttribute(Type relatedProperty)
    {
        RelatedProperty = relatedProperty;
    }
}

public class PropertyRelation<TOwner, TProperty>
{
    private readonly Func<TOwner, TProperty> _propGetter;

    public PropertyRelation(Func<TOwner, TProperty> 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<MyClass, int>
    {
        public EmployeeIdRelation()
            : base(@class => @class.EmployeeId)
        {

        }
    }
}
8

Tu ne peux pas. Les types d'attribut sont limités comme écrit ici . Ma suggestion, essayez d’évaluer votre expression lambda en externe, puis utilisez l’un des types suivants:

  • Types simples (bool, octet, char, short, int, long, float et double)
  • chaîne
  • Type de système 
  • enums 
  • object (L'argument d'un paramètre d'attribut de type object doit être une valeur constante de l'un des types ci-dessus.)
  • Tableaux unidimensionnels de l'un des types ci-dessus
5

Pour développer mon commentaire , c’est un moyen d’accomplir votre tâche avec une approche différente. Vous dites que vous voulez "indiquer les propriétés liées dans une classe" et que vous "souhaitez utiliser les expressions lambda pour que je puisse passer un type fort dans le constructeur de mon attribut, et non une" chaîne magique ". peut exploiter la vérification du type de compilateur ". 

Voici donc un moyen d'indiquer les propriétés associées qui est compile-time typed et qui n'a pas de chaîne magique:

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

C'est la classe à l'étude. Nous voulons indiquer que EmployeeId et EmployeeNumber sont liés. Pour un peu de concision de code, plaçons cet alias de type en haut du fichier de code. Ce n'est pas nécessaire du tout mais cela rend le code moins intimidant:

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

Cela fait de MyClassPropertyTuple un alias pour un Tuple de deux Expressions, chacun d'entre eux capturant la définition d'une fonction d'un MyClass à un objet. Par exemple, les getters de propriétés sur MyClass sont de telles fonctions.

Maintenant capturons la relation. Ici, j'ai créé une propriété statique sur MyClass, mais cette liste peut être définie n'importe où:

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

Le compilateur C # sait que nous construisons une Tuple de Expressions, nous n'avons donc pas besoin de transtypages explicites devant ces expressions lambda - elles sont automatiquement transformées en Expressions.

C’est essentiellement en termes de définition - ces mentions EmployeeId et EmployeeNumber sont fortement typées et appliquées au moment de la compilation, et les outils de refactoring qui permettent de renommer des propriétés devraient pouvoir retrouver ces utilisations lors d’un changement de nom (ReSharper le peut certainement). Il n'y a pas de cordes magiques ici.


Mais bien sûr, nous voulons aussi pouvoir interroger les relations au moment de l'exécution (je suppose!). Je ne sais pas exactement comment vous voulez faire cela, alors ce code est juste illustratif.

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

Le code pour propertyInfo1FromExpression et propertyInfo2FromExpression ici, j’ai travaillé avec une utilisation judicieuse de la fenêtre de surveillance pendant le débogage - c’est généralement ainsi que je détermine ce que contient réellement un arbre Expression.

Courir cela produira

EmployeeId
EmployeeNumber
True
True

montrant que nous pouvons extraire avec succès les détails des propriétés associées, et (de manière cruciale) elles sont identiques à la référence PropertyInfos obtenue par d'autres moyens. Espérons que vous pourrez l’utiliser conjointement avec l’approche que vous utilisez réellement pour spécifier les propriétés qui vous intéressent au moment de l’exécution.

4
AakashM

Pointe. Utilisez nameof . J'ai un DateRangeAttribute qui valide deux propriétés et s'assure qu'elles sont un DateRange valide. 

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
 public class DateRangeAttribute : ValidationAttribute
 {
      private readonly string _endDateProperty;
      private readonly string _startDateProperty;

      public DateRangeAttribute(string startDateProperty, string endDateProperty) : base()
      {
            _startDateProperty = startDateProperty;
            _endDateProperty = endDateProperty;
      }

      protected override ValidationResult IsValid(object value, ValidationContext validationContext)
      {
            var stP = validationContext.ObjectType.GetProperty(_startDateProperty);
            var enP = validationContext.ObjectType.GetProperty(_endDateProperty);
            if (stP == null || enP == null || stP.GetType() != typeof(DateTime) || enP.GetType() != typeof(DateTime))
            {
                 return new ValidationResult($"startDateProperty and endDateProperty must be valid DateTime properties of {nameof(value)}.");
            }
            DateTime start = (DateTime)stP.GetValue(validationContext.ObjectInstance, null);
            DateTime end = (DateTime)enP.GetValue(validationContext.ObjectInstance, null);

            if (start <= end)
            {
                 return ValidationResult.Success;
            }
            else
            {
                 return new ValidationResult($"{_endDateProperty} must be equal to or after {_startDateProperty}.");
            }
      }
 }


class Tester
{
    public DateTime ReportEndDate { get; set; }
    [DateRange(nameof(ReportStartDate), nameof(ReportEndDate))]
    public DateTime ReportStartDate { get; set; }
}
0
vbjay