it-swarm.com.de

Lambda-Ausdruck im Attributkonstruktor

Ich habe eine Attribute-Klasse mit dem Namen RelatedPropertyAttribute erstellt:

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

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

Ich benutze dies, um verwandte Eigenschaften in einer Klasse anzuzeigen. Beispiel, wie ich es verwenden würde:

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

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

Ich möchte Lambda-Ausdrücke verwenden, damit ich einen starken Typ an den Konstruktor meines Attributs übergeben kann und keine "magische Zeichenfolge". Auf diese Weise kann ich die Überprüfung des Compilertyps nutzen. Zum Beispiel:

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

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

Ich dachte, ich könnte das mit folgendem machen, aber der Compiler erlaubt es nicht:

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

Error: 

Der nicht generische Typ 'RelatedPropertyAttribute' kann nicht mit .__ verwendet werden. Typ Argumente

Wie kann ich das erreichen?

36
davenewza

Du kannst nicht

  • sie können keine generischen Attributtypen erstellen (dies ist einfach nicht zulässig). Ebenso ist keine Syntax für using generische Attribute ([Foo<SomeType>]) definiert
  • sie können keine Lambdas in Attributinitialisierern verwenden. Die Werte, die an Attribute übergeben werden können, sind sehr begrenzt und enthalten einfach keine Ausdrücke (die sehr komplex sind und Laufzeitobjekte sind, keine Kompilierzeitliterale).
32
Marc Gravell

Ein generisches Attribut ist auf herkömmliche Weise nicht möglich. C # und VB unterstützen es jedoch nicht, die CLR jedoch. Wenn Sie IL-Code schreiben möchten, ist dies möglich.

Nehmen wir Ihren Code:

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

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

Kompilieren Sie den Code, öffnen Sie die Assembly mit ILSpy oder ILDasm und geben Sie den Inhalt in eine Textdatei aus. Die Klassendeklaration der Attributklasse für Sie wird folgendermaßen aussehen:

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

In der Textdatei können Sie das Attribut dann generisch machen. Es gibt einige Dinge, die geändert werden müssen.

Dies kann einfach durch Ändern der IL getan werden und die CLR wird sich nicht beschweren:

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

nun können Sie den Typ von relatedProperty von String in Ihren generischen Typ ändern. 

Zum Beispiel:

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

Ändern Sie es in:

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

Es gibt viele Frameworks, um einen "schmutzigen" Job wie folgt auszuführen: Mono.Cecil oder CCI .

Wie ich bereits sagte, handelt es sich nicht um eine saubere objektorientierte Lösung, sondern wollte nur einen anderen Weg aufzeigen, um das Limit von C # und VB zu durchbrechen.

Zu diesem Thema gibt es eine interessante Lektüre: check it out dieses Buch.

Ich hoffe es hilft.

47

Wenn Sie C # 6.0 verwenden, können Sie nameof verwenden.

Wird verwendet, um den einfachen (nicht qualifizierten) Zeichenfolgennamen einer Variablen zu erhalten Typ oder Mitglied. Wenn Sie Fehler im Code melden, verbinden Sie MVC-Links (Model-View-Controller), geänderte Ereignisse der Auslöseeigenschaften, usw. möchten Sie häufig den String-Namen einer Methode erfassen. Verwenden von nameof hilft, Ihren Code beim Umbenennen von Definitionen gültig zu halten. Vor Sie mussten String-Literale verwenden, um auf Definitionen zu verweisen, nämlich beim Umbenennen von Codeelementen brüchig, da Tools nicht wissen, ob diese String-Literale.

damit können Sie Ihr Attribut folgendermaßen verwenden:

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

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

Eine mögliche Problemumgehung besteht darin, die Klasse für jede Eigenschaftsbeziehung zu definieren und darauf zu verweisen
typeof () - Operator im Attributkonstruktor.

Aktualisierte:

Zum Beispiel:

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

        }
    }
}

Du kannst nicht Attributtypen sind auf hier begrenzt. Mein Vorschlag, versuchen Sie, Ihren Lambda-Ausdruck extern auszuwerten. Verwenden Sie dann einen der folgenden Typen:

  • Einfache Typen (bool, byte, char, short, int, long, float und double)
  • schnur
  • Systemtyp 
  • enums 
  • object (Das Argument für einen Attributparameter des Typs object muss ein konstanter Wert eines der oben genannten Typen sein.)
  • Eindimensionale Arrays eines der oben genannten Typen
5

Um auf mein Kommentar zu erweitern, können Sie Ihre Aufgabe mit einem anderen Ansatz erreichen. Sie sagen, Sie möchten "verwandte Eigenschaften in einer Klasse anzeigen" und "möchten die Lambda-Ausdrücke verwenden, damit ich einen starken Typ in den Konstruktor meines Attributs übergeben kann und keine" magische Zeichenfolge ". Auf diese Weise kann Compilertypprüfung ausnutzen ". 

Hier ist eine Möglichkeit, verwandte Eigenschaften anzuzeigen, die compile-time sind und keine magischen Zeichenfolgen enthalten:

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

Dies ist die betrachtete Klasse. Wir möchten darauf hinweisen, dass EmployeeId und EmployeeNumber verwandt sind. Für etwas Code-Prägnanz sollten Sie diesen Typ-Alias ​​oben in die Codedatei einfügen. Es ist überhaupt nicht notwendig, macht aber den Code weniger einschüchternd:

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

Dies macht MyClassPropertyTuple zu einem Alias ​​für ein Tuple von zwei Expressions, von denen jeder die Definition einer Funktion von einem MyClass zu einem Objekt erfasst. Eigenschafts-Getter auf MyClass sind beispielsweise solche Funktionen.

Lassen Sie uns jetzt die Beziehung erfassen. Hier habe ich eine statische Eigenschaft in MyClass gemacht, aber diese Liste könnte irgendwo definiert werden:

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

Der C # -Compiler weiß, dass wir ein Tuple von Expressions erstellen. Daher brauchen wir keine expliziten Casts vor diesen Lambda-Ausdrücken - sie werden automatisch in Expressions umgewandelt.

Das ist es im Grunde in Bezug auf die Definition - die EmployeeId- und EmployeeNumber-Erwähnungen werden während der Kompilierzeit stark typisiert und durchgesetzt, und Refactoring-Tools, die das Umbenennen von Eigenschaften durchführen, sollten diese Verwendungen während eines Umbenennens finden können (ReSharper kann das definitiv. Hier gibt es keine magischen Zeichenketten.


Natürlich wollen wir aber auch zur Laufzeit Beziehungen abfragen (nehme ich an!). Ich weiß nicht genau, wie Sie das machen wollen, daher ist dieser Code nur illustrativ.

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

Den Code für propertyInfo1FromExpression und propertyInfo2FromExpression habe ich hier beim Debuggen mit vernünftiger Verwendung des Watch-Fensters erarbeitet - so finde ich normalerweise heraus, was ein Expression-Baum tatsächlich enthält.

Wenn Sie dies ausführen, wird es produziert

EmployeeId
EmployeeNumber
True
True

was zeigt, dass wir die Details der verwandten Eigenschaften erfolgreich extrahieren können, und (entscheidend) sie sind mit den mit anderen Mitteln erhaltenen PropertyInfos identisch. Hoffentlich können Sie dies in Verbindung mit dem jeweils verwendeten Ansatz verwenden, um die Eigenschaften von Interesse zur Laufzeit anzugeben.

4
AakashM

Spitze. Verwenden Sie nameof . Ich habe ein DateRangeAttribute, das zwei Eigenschaften überprüft und sicherstellt, dass sie ein gültiges DateRange sind. 

[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