Подтвердить что ты не робот

Выражение Lambda в конструкторе атрибутов

Я создал класс Attribute под названием RelatedPropertyAttribute:

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

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

Я использую это, чтобы указать связанные свойства в классе. Пример того, как я буду использовать его:

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

Я думал, что могу сделать это со следующим, но компилятором это не разрешено:

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

Ошибка:

Не-общий тип 'RelatedPropertyAttribute' не может использоваться с аргументы типа

Как я могу это достичь?

4b9b3361

Ответ 1

Вы не можете

  • вы не можете создавать общие типы атрибутов (это просто не допускается); одинаково не используется синтаксис для использования общих атрибутов ([Foo<SomeType>])
  • Вы не можете использовать lambdas в инициализаторах атрибутов - значения, доступные для передачи атрибутам, очень ограничены и просто не содержат выражений (которые являются очень сложными и являются объектами времени выполнения, а не компиляционными литералами времени).

Ответ 2

Наличие универсального атрибута невозможно обычным способом. Однако С# и VB не поддерживают его, но CLR. Если вы хотите написать IL-код, это возможно.

Возьмите код:

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

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

Скомпилируйте код, откройте сборку ILSpy или ILDasm, а затем выгрузите содержимое к текстовый файл. 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.Cecil или CCI.

Как я уже сказал, это не чисто объектно-ориентированное решение, а просто хотел указать другой способ разбить предел С# и VB.

Здесь интересно прочитать эту тему, проверить ее в этой книге.

Надеюсь, что это поможет.

Ответ 3

Если вы используете С# 6.0, вы можете использовать nameof

Используется для получения простого (неквалифицированного) имени строки переменной, типа или члена. Когда вы сообщаете об ошибках в коде, подключайте ссылки на модель-просмотр-контроллер (MVC), изменение свойств измененных событий, и т.д., вы часто хотите захватить имя строки метода. С помощью nameof помогает сохранить ваш код действительным при переименовании определений. До вам пришлось использовать строковые литералы для обозначения определений, которые хрупкий при переименовании элементов кода, потому что инструменты не знают, чтобы проверить эти строковые литералы.

с этим вы можете использовать свой атрибут следующим образом:

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

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

Ответ 4

Одним из возможных обходных решений является определение класса для каждого отношения свойств и ссылки на него с помощью 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)
        {

        }
    }
}

Ответ 5

Вы не можете. Типы атрибутов ограничены в виде здесь. Мое предложение, попробуйте оценить внешнее выражение лямбды, затем используйте один из следующих типов:

  • Простые типы (bool, byte, char, short, int, long, float и double)
  • строка
  • System.Type
  • enums
  • object (Аргумент параметра атрибута объекта типа должен быть постоянным значением одного из указанных выше типов.)
  • Одномерные массивы любого из указанных типов

Ответ 6

Чтобы расширить мой комментарий, это способ достижения вашей задачи с помощью другого подхода. Вы говорите, что хотите "указать связанные свойства в классе" и что вы "хотели бы использовать лямбда-выражения , чтобы я мог передать сильный тип в свой конструктор атрибутов, а не" магическую строку ". Я могу использовать проверку типа компилятора.

В этом случае можно указать связанные свойства, которые время компиляции и не имеют никаких магических строк:

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

Это рассматриваемый класс. Мы хотим указать, что EmployeeId и EmployeeNumber связаны. Для немного краткости кода, пусть этот псевдоним типа вверх находится в верхней части файла кода. Это совсем не обязательно, но это делает код менее пугающим:

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

Это делает MyClassPropertyTuple псевдоним для Tuple двух Expression s, каждый из которых фиксирует определение функции из MyClass для объекта. Например, свойство getters на MyClass являются такими функциями.

Теперь давайте зафиксировать связь. Здесь я сделал статический вклад в MyClass, но этот список можно было определить где угодно:

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

Компилятор С# знает, что мы создаем Tuple из Expression s, поэтому нам не нужны какие-либо явные приемы перед этими лямбда-выражениями - они автоматически превращаются в Expression s.

В основном это с точки зрения определения - те упоминания EmployeeId и EmployeeNumber строго типизированы и принудительно применяются во время компиляции, а инструменты рефакторинга, которые переименовывают свойства, должны иметь возможность находить эти способы использования при переименовании ( ReSharper определенно может). Здесь нет магических струн.


Но, конечно, мы также хотим иметь возможность опросить отношения во время выполнения (я полагаю!). Я не знаю точно, как вы хотите это делать, чтобы этот код был просто иллюстративным.

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 и propertyInfo2FromExpression здесь я разработал с разумным использованием окна "Часы" во время отладки - это обычно то, как я разрабатываю то, что фактически содержит дерево Expression.

Запуск этого будет производить

EmployeeId
EmployeeNumber
True
True

показывающий, что мы можем успешно извлечь детали связанных свойств, и (в решающей степени) они являются ссылочными, идентичными PropertyInfo, полученным другими способами. Надеюсь, вы можете использовать это в сочетании с любым подходом, который вы фактически используете, чтобы указать свойства, представляющие интерес, во время выполнения.