问题
In a few places, I've noticed expression trees passed as arguments to methods to allow compiler checking of property names. For example, Caliburn Micro has the following method signature in its PropertyChangedBase class:
public virtual void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property);
I have a custom attribute which I would like to have the same type of compiler checking of property names in the constructor, to enable me to type:
[MyCustomAttribute(() => PropertyName)]
Instead of:
[MyCustomAttribute("PropertyName")]
Using a constructor definition along the lines of:
public MyCustomAttribute(params Expression<Func<object>>[] properties)
However, due to the restriction on Attribute parameters being constant expressions, this appears not to be possible.
Can anyone recommend a different approach where I can get the compiler to check property names in my attribute parameters rather than leaving this potential bug where only strings are used?
Edit: Thanks to Marc's answer, I have implemented this for now:
#if DEBUG
foreach (var propertyInfo in
GetType().GetProperties().Where(propertyInfo => Attribute.IsDefined(propertyInfo, typeof (MyCustomAttribute))))
{
foreach (var propertyName in propertyInfo.GetAttributes<MyCustomAttribute>(true)
.SelectMany(attribute => attribute.PropertyNames))
Debug.Assert(
GetType().GetProperty(propertyName) != null,
"Property not found",
"Property {0} declared on attributed property {1} was not found on type {2}.",
propertyName, propertyInfo.Name, GetType().Name
);
}
#endif
回答1:
This is simply not possible. Attributes are limited to very basic types that don't include what you would need for this. One possible static-safe way of doing it would be to subclass the attribute per property but that is an insane amount of work.
Personally, I'd just write a unit test that finds all occurrences of the attribute and checks they are sensible via reflection. You could also do this in the main code inside a #if DEBUG
block (or similar).
回答2:
There are several solutions using PostSharp (disclaimer: I am the man), some of which with the free edition.
Solution 1
You could use a PostSharp aspect and use CompileTimeInitialize to read the property name.
For instance:
[Serializable]
class MyCustomAttribute : LocationLevelAspect
{
string propertyName;
public override void CompileTimeInitialize( LocationInfo targetLocation,
AspectInfo aspectInfo )
{
this.propertyName = targetLocation.PropertyName;
}
}
This feature is present in the free PostSharp Community Edition.
The catch is that a custom attribute built this way is not visible using System.Reflection.
Solution 2
You can also use an aspect that adds a custom attribute. The aspect should then implement IAspectProvider and return instances of CustomAttributeIntroductionAspect. There's an example you can get inspired from on this page. This feature is available on PostSharp Professional Edition ($).
Solution 3
You can also make your custom attribute class (any class, not specifically an aspect) implement the interface IValidableAnnotation:
public class MyAttribute : Attribute, IValidableAnnotation
{
private string propertyName;
public MyAttribute(string propertyName)
{
this.propertyName = propertyName;
}
public bool CompileTimeValidate( object target )
{
PropertyInfo targetProperty = (PropertyInfo) target;
if ( targetProperty.Name != propertyName )
{
Message.Write( Severity.Error, "MY001",
"The custom attribute argument does not match the property name.");
return false;
}
}
}
This is possible using the free edition of PostSharp and you can easily include it in an #if/#endif block to make your code fully independent from PostSharp if you wish.
来源:https://stackoverflow.com/questions/4518291/how-to-pass-compiler-checked-property-names-expression-tree-to-a-custom-attrib