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'は、型引数と一緒に使用できません
どうすればこれを達成できますか?
できません
[Foo<SomeType>]
)が定義されています一般的な属性を持つことは、従来の方法では不可能です。ただし、C#および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 [〜#〜] 。
既に述べたように、これはクリーンなオブジェクト指向ソリューションではありませんが、C#とVBの限界を打ち破る別の方法を指摘したかっただけです。
このトピックに関する興味深い読み物があります チェックアウト この本。
それが役に立てば幸い。
C#6.0を使用している場合は、 nameof を使用できます
変数、型、またはメンバーの単純な(修飾されていない)文字列名を取得するために使用されます。コードのエラーを報告するとき、モデルビューコントローラー(MVC)リンクを接続するとき、プロパティ変更イベントを起動するときなどに、メソッドの文字列名をキャプチャしたいことがよくあります。 nameofを使用すると、定義の名前を変更するときにコードを有効に保つことができます。文字列リテラルを使用して定義を参照する必要があった前は、コード要素の名前を変更する場合、ツールはこれらの文字列リテラルをチェックすることを知らないため、脆弱です。
これを使用すると、次のように属性を使用できます。
public class MyClass
{
public int EmployeeID { get; set; }
[RelatedProperty(nameof(EmployeeID))]
public int EmployeeNumber { get; set; }
}
可能な回避策の1つは、各プロパティリレーションシップのクラスを定義し、それを参照することです。
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)
{
}
}
}
できません。属性タイプは、書かれているように制限されます here 。私の提案では、ラムダ式を外部で評価してから、次のいずれかのタイプを使用してください。
my comment を展開すると、これは異なるアプローチでタスクを達成する方法です。 「クラスに関連するプロパティを示す」こと、「ラムダ式を使用して、属性のコンストラクタではなく強い型を渡すことができるようにしたい」と言います。魔法の文字列」。この方法でコンパイラの型チェックを活用できます "。
次に、compile-time typedであり、マジックストリングを持たない関連プロパティを示す方法を示します。
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
が2つのTuple
のExpression
のエイリアスになり、それぞれがMyClass
からオブジェクトへの関数の定義をキャプチャします。たとえば、MyClass
のプロパティゲッターはそのような関数です。
次に、関係をキャプチャしましょう。ここではMyClass
に静的プロパティを作成しましたが、このリストはどこでも定義できます。
public class MyClass
{
public static List<MyClassPropertyTuple> Relationships
= new List<MyClassPropertyTuple>
{
new MyClassPropertyTuple(c => c.EmployeeId, c => c.EmployeeNumber)
};
}
C#コンパイラは、Tuple
sのExpression
を構築していることを知っているので、これらのラムダ式の前に明示的なキャストは必要ありません-それらは自動的に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
sと参照同一です。実行時に目的のプロパティを指定するために実際に使用しているアプローチと組み合わせてこれを使用できることを願っています。
ヒント。 nameof を使用します。 2つのプロパティを検証し、それらが有効なDateRangeであることを確認するDateRangeAttributeがあります。
[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; }
}