属性构造函数中的Lambda表达式

时间:2013-05-29 08:46:00

标签: c# .net reflection attributes custom-attributes

我创建了一个名为Attribute的{​​{1}}类:

RelatedPropertyAttribute

我用它来表示类中的相关属性。我将如何使用它的示例:

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

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

我想使用lambda表达式,以便我可以将强类型传递给我的属性的构造函数,而不是“魔术字符串”。这样我可以利用编译器类型检查。例如:

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

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

我认为我可以使用以下内容执行此操作,但编译器不允许这样做:

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

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

错误:

  

非泛型类型'RelatedPropertyAttribute'不能与之一起使用   类型参数

我怎样才能做到这一点?

7 个答案:

答案 0 :(得分:49)

以传统方式不可能具有通用属性。但是C#和VB不支持它,但CLR支持它。如果你想写一些IL代码,那就可以了。

我们来看看你的代码:

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

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

编译代码,使用ILSpyILDasm打开程序集,然后将内容转储到 文本文件。属性类声明的IL将如下所示:

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

在文本文件中,您可以使该属性具有通用性。有几件事需要改变。

这可以通过更改IL来完成,而CLR不会抱怨:

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

现在您可以将 relatedProperty 的类型从字符串更改为您的泛型类型。

例如:

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

将其更改为:

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

有许多框架可以执行这样的“脏”工作:Mono.CecilCCI

正如我已经说过,它不是一个干净的面向对象的解决方案,但只是想指出另一种方法来突破C#和VB的限制。

围绕这个主题有一个有趣的解读,check it out这本书。

希望它有所帮助。

答案 1 :(得分:32)

你不能

  • 你不能创建通用的属性类型(它是不允许的);同样,没有定义使用通用属性([Foo<SomeType>])的语法
  • 你不能在属性初始化器中使用lambdas - 可用于传递给属性的值非常有限,并且根本不包含表达式(它们非常复杂,并且是运行时对象,而不是编译时文字)

答案 2 :(得分:12)

如果您使用的是C#6.0,则可以使用nameof

  

用于获取变量的简单(非限定)字符串名称,   类型或成员。在代码中报告错误时,请挂钩   模型 - 视图 - 控制器(MVC)链接,触发属性更改事件,   等等,您经常要捕获方法的字符串名称。运用   nameof有助于在重命名定义时保持代码有效。之前   你必须使用字符串文字来引用定义,这是   重命名代码元素时脆弱,因为工具不知道要检查   这些字符串文字。

用它可以使用你的属性:

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

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

答案 3 :(得分:8)

可能的解决方法之一是为每个属性关系定义类并通过
引用它 属性构造函数中的typeof()运算符。

<强>更新

例如:

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

        }
    }
}

答案 4 :(得分:5)

你做不到。属性类型受限于书面here。我的建议是,尝试从外部评估你的lambda表达式,然后使用以下类型之一:

  • 简单类型(bool,byte,char,short,int,long,float和double)
  • 字符串
  • System.Type
  • enums
  • object(对象类型的属性参数的参数必须是上述类型之一的常量值。)
  • 任何上述类型的一维阵列

答案 5 :(得分:4)

要展开my comment,这是一种通过不同方法实现任务的方法。你说你想“指出一个类中的相关属性”,并且你“想要使用lambda表达式,这样我就可以将强类型传递给我的属性的构造函数,而不是”魔术字符串“。这样我可以利用编译器类型检查“。

这是一种指示编译时键入的相关属性并且没有任何魔术字符串的方法:

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

这是正在考虑的课程。我们希望指出EmployeeIdEmployeeNumber是相关的。为了简化一些代码,让我们把这个类型的别名放在代码文件的顶部。它根本没有必要,但它确实使代码不那么令人生畏:

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

这使MyClassPropertyTuple成为两个Tuple的{​​{1}}的别名,每个Expression都会捕获从MyClass到对象的函数定义。例如,MyClass上的属性getter就是这样的函数。

现在让我们捕捉这段关系。在这里,我在MyClass上制作了一个静态属性,但这个列表可以在任何地方定义:

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

C#编译器知道我们正在构造Tuple Expression,因此我们不需要在这些lambda表达式前面进行任何显式转换 - 它们会自动转换为{{1} }第

这基本上就定义而言 - 那些ExpressionEmployeeId提及在编译时是强类型和强制执行的,重构属性重命名的工具应该能够找到这些用法在重命名期间(ReSharper绝对可以)。这里没有神奇的字符串。


但当然我们也希望能够在运行时查询关系(我假设!)。我不知道你想怎么做这个,所以这段代码只是说明性的。

EmployeeNumber

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); } } } propertyInfo1FromExpression的代码我在调试时明智地使用了Watch窗口,这通常是我如何计算propertyInfo2FromExpression树实际包含的内容。< / p>

运行它将产生

Expression

表明我们可以成功提取相关属性的详细信息,并且(关键地)它们与通过其他方式获得的EmployeeId EmployeeNumber True True 的引用相同。希望您可以将它与您实际使用的任何方法结合使用,以在运行时指定感兴趣的属性。

答案 6 :(得分:0)

提示。使用nameof。我有一个DateRangeAttribute,它可以验证两个属性并确保它们是有效的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; }
}