Lambda expression in attribute constructor

非 Y 不嫁゛ 提交于 2019-11-27 05:32:04

问题


I have created an Attribute class called RelatedPropertyAttribute:

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

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

I use this to indicate related properties in a class. Example of how I would use it:

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

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

I would like to use lambda expressions so that I can pass a strong type into my attribute's constructor, and not a "magic string". This way I can exploit compiler type checking. For example:

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

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

I thought I could do it with the following, but it isn't allowed by the compiler:

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

Error:

The non-generic type 'RelatedPropertyAttribute' cannot be used with type arguments

How can I achieve this?


回答1:


You cannot

  • you cannot create generic attribute types (it simply isn't allowed); equally, no syntax for using generic attributes ([Foo<SomeType>]) is defined
  • you cannot use lambdas in attribute initializers - the values available to pass to attributes is very limited, and simply does not include expressions (which are very complex, and are runtime objects, not compile-time literals)



回答2:


Having a generic attribute is not possible in a conventional way. However C# and VB don't support it but the CLR does. If you want to write some IL code it's possible.

Let's take your code:

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

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

Compile the code, open up the assembly with ILSpy or ILDasm and then dump the content to a text file. The IL of you attribute class declaration will look like this:

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

In the text file, you can then make the attribute generic. There are several things that need to be changed.

This can simply be done by changing the IL and the CLR won't complain:

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

and now you can change the type of relatedProperty from string to your generic type.

For Example:

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

change it to:

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

There are lot of frameworks to do a "dirty" job like that: Mono.Cecil or CCI.

As I have already said it's not a clean object oriented solution but just wanted to point out another way to break the limit of C# and VB.

There's an interesting reading around this topic, check it out this book.

Hope it helps.




回答3:


If you are using C# 6.0, you can use nameof

Used to obtain the simple (unqualified) string name of a variable, type, or member. When reporting errors in code, hooking up model-view-controller (MVC) links, firing property changed events, etc., you often want to capture the string name of a method. Using nameof helps keep your code valid when renaming definitions. Before you had to use string literals to refer to definitions, which is brittle when renaming code elements because tools do not know to check these string literals.

with it you can use your attribute like this:

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

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



回答4:


One of possible workarounds is to define class for each property relationship and to reference it by
typeof() operator in attribute constructor.

Updated:

For example:

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

        }
    }
}



回答5:


You can't. Attribute types are limited as written here. My suggestion, try to evaluate your lambda expression externally, then use one of the following types:

  • Simple types (bool, byte, char, short, int, long, float, and double)
  • string
  • System.Type
  • enums
  • object (The argument to an attribute parameter of type object must be a constant value of one of the above types.)
  • One-dimensional arrays of any of the above types



回答6:


To expand on my comment, this is a way to achieve your task with a different approach. You say you want to "indicate related properties in a class", and that you "would like to use lambda expressions so that I can pass a strong type into my attribute's constructor, and not a "magic string". This way I can exploit compiler type checking".

Here then is a way of indicating related properties that is compile-time typed and doesn't have any magic strings:

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

This is the class under consideration. We want to indicate that EmployeeId and EmployeeNumber are related. For a bit of code conciseness, let's put this type alias up at the top of the code file. It's not necessary at all but it does make the code less intimidating:

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

This makes MyClassPropertyTuple an alias for a Tuple of two Expressions, each of which captures the definition of a function from a MyClass to an object. For example, property getters on MyClass are such functions.

Now let's capture the relationship. Here I've made a static propery on MyClass, but this list could be defined anywhere:

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

The C# compiler knows we're constructing a Tuple of Expressions, so we don't need any explicit casts in front of those lambda expressions - they are automatically made into Expressions.

That's basically it in terms of definition - those EmployeeId and EmployeeNumber mentions are strongly typed and enforced at compile time, and refactoring tools that do property renames should be able to find those usages during a rename (ReSharper definitely can). There are no magic strings here.


But of course we also want to be able to interrogate relationships at runtime (I assume!). I don't know exactly how you want to be doing this so this code is just illustrative.

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

The code for propertyInfo1FromExpression and propertyInfo2FromExpression here I worked out with judicious use of the Watch window while debugging - this is usually how I work out what an Expression tree actually contains.

Running this will produce

EmployeeId
EmployeeNumber
True
True

showing that we can successfully extract the details of the related properties, and (crucially) they are reference-identical to the PropertyInfos obtained by other means. Hopefully you can use this in conjunction with whatever approach you are actually using to specify properties of interest at runtime.




回答7:


Tip. Use nameof. I have a DateRangeAttribute that validates two properties and makes sure they are a valid DateRange.

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


来源:https://stackoverflow.com/questions/16809294/lambda-expression-in-attribute-constructor

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!