Applying custom attributes to class members in C# is a powerful way to add metadata about those members at compile time.
PostSharp provides the ability to create a custom attribute class that, when applied to another class, can iterate through those class members and automatically decorate them with custom attributes. This can be useful, for example, to automatically apply custom attributes or groups of custom attributes when new class members are added, without having to remember to do it manually each time.
Introducing new custom attributes
In the following example, we create an attribute decorator class that applies the .NET DataContractAttribute to a class and DataMemberAttribute to members of a class at build time.
Start by creating a class called
AutoDataContractAttributethat derives from TypeLevelAspect. TypeLevelAspect transforms the class into an attribute that can be applied to other classes. Also implement IAspectProvider, which exposes the ProvideAspects(object) method for iterating on class members. ProvideAspects(object) is called once for each class to which the aspect is applied. Like all aspects, the class must be annotated with PSerializableAttribute.Implement the ProvideAspects(object) method to cast the
targetElementparameter to aTypeobject. Note that this method will be called at build time. TheTypeobject can be used for inspecting each member and making decisions about when and how to apply custom attributes. In the following snippet, the implementation returns a new AspectInstance for theTypecontaining a new DataContractAttribute and then iterates through each property of theTypereturning a new AspectInstance with the DataMemberAttribute for each. Note that both the DataContractAttribute and DataMemberAttribute are both wrapped in CustomAttributeIntroductionAspect objects:[PSerializable] public sealed class AutoDataContractAttribute : TypeLevelAspect, IAspectProvider { // This method is called at build time and should just provide other aspects. public IEnumerable<AspectInstance> ProvideAspects( object targetElement ) { Type targetType = (Type) targetElement; CustomAttributeIntroductionAspect introduceDataContractAspect = new CustomAttributeIntroductionAspect( new ObjectConstruction( typeof(DataContractAttribute).GetConstructor( Type.EmptyTypes ) ) ); CustomAttributeIntroductionAspect introduceDataMemberAspect = new CustomAttributeIntroductionAspect( new ObjectConstruction( typeof(DataMemberAttribute).GetConstructor( Type.EmptyTypes ) ) ); // Add the DataContract attribute to the type. yield return new AspectInstance( targetType, introduceDataContractAspect ); // Add a DataMember attribute to every relevant property. foreach ( PropertyInfo property in targetType.GetProperties( BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance ) ) { if ( property.CanWrite ) { yield return new AspectInstance( property, introduceDataMemberAspect ); } } } }Note
Since the
ProvideAspectsmethod returns anIEnumerable, use theyieldkeyword to return aspects for PostSharp to apply.Apply the
AutoDataContractAttributeclass. In the following example, we apply it to aProductclass where it will decorateProductwith DataContractAttribute and each member with DataMemberAttribute:[AutoDataContract] public class Product { public int ID { get; set; } public string? Name { get; set; } public int RevisionNumber { get; set; } }When you inspect the
Productclass with reflection after the build, you see the following attributes:Product: [System.Runtime.Serialization.DataContractAttribute()] ID: [System.Runtime.Serialization.DataMemberAttribute()] Name: [System.Runtime.Serialization.DataMemberAttribute()] RevisionNumber: [System.Runtime.Serialization.DataMemberAttribute()]
Copying existing custom attributes
Another way to introduce attributes to class members is to copy them from another class. This is useful, for example, when distinct classes have members with the same names and the same types. In this case, attributes can be defined in one class and then that class can be used to decorate other similar classes with the same attributes.
In the following snippet, the Id and Name properties of Product have both been modified to contain attributes from the System.ComponentModel.DataAnnotations namespace: Editable and Required on Id, and Display and Required on Name. We want to copy these attributes to another class called ProductViewModel, which contains the same properties:
class Product
{
[Editable( false )]
[Required]
public int Id { get; set; }
[Display( Name = "The product's name" )]
[Required]
public string? Name { get; set; }
public int RevisionNumber { get; set; }
}
To copy the attributes from the properties of Product to the corresponding properties of ProductViewModel, create an attribute class which can be applied to ProductViewModel to perform this copy process:
Create a TypeLevelAspect that implements IAspectProvider. In the following snippet, the class is called
CopyCustomAttributesFrom. Its constructor takes the type from which the property attributes are copied. Then implement ProvideAspects(object):[PSerializable] class CopyCustomAttributesFrom : TypeLevelAspect, IAspectProvider { private Type sourceType; public CopyCustomAttributesFrom( Type sourceType ) { this.sourceType = sourceType; } public IEnumerable<AspectInstance> ProvideAspects( object targetElement ) { Type targetClassType = (Type) targetElement; // Loop through each property of the target type. foreach ( PropertyInfo targetPropertyInfo in targetClassType.GetProperties() ) { PropertyInfo? sourcePropertyInfo = this.sourceType.GetProperty( targetPropertyInfo.Name ); if ( sourcePropertyInfo == null ) { continue; } // Loop through all custom attributes of the source property and copy them to the target property. foreach ( CustomAttributeData customAttributeData in sourcePropertyInfo.GetCustomAttributesData() ) { // Filter out attributes that are not data annotations. if ( customAttributeData.AttributeType.Namespace == "System.ComponentModel.DataAnnotations" ) { CustomAttributeIntroductionAspect customAttributeIntroductionAspect = new CustomAttributeIntroductionAspect( new ObjectConstruction( customAttributeData ) ); yield return new AspectInstance( targetPropertyInfo, customAttributeIntroductionAspect ); } } } } }The ProvideAspects(object) method iterates through each property of the target class and then gets the corresponding property from the source class. It then iterates through all custom attributes defined for the source property, copying each to the corresponding property of the target class. ProvideAspects(object) also filters out attributes that are not from the
System.ComponentModel.DataAnnotationsnamespace to demonstrate how you may want to ignore some attributes during the copy process.Decorate the
ProductViewModelclass with theCopyCustomAttributesFromattribute, specifyingProductas the source type in the constructor. During compilation, the ProvideAspects(object) method ofCopyCustomAttributesFromwill then perform the copy process fromProducttoProductViewModel:[CopyCustomAttributesFrom( typeof(Product) )] class ProductViewModel { public int Id { get; set; } public string? Name { get; set; } public int RevisionNumber { get; set; } }
The following program prints the data annotations of the properties of ProductViewModel using reflection:
static void Main()
{
// Print the custom attributes of the properties of ProductViewModel.
foreach ( PropertyInfo property in typeof(ProductViewModel).GetProperties() )
{
var attributes = property.GetCustomAttributesData()
.Where( a => a.AttributeType.Namespace == "System.ComponentModel.DataAnnotations" );
Console.WriteLine( "{0}: {1}", property.Name, string.Join( " ", attributes ) );
}
}
The output shows that the Editable, Display and Required attributes were copied from Product to ProductViewModel at build time:
Id: [System.ComponentModel.DataAnnotations.EditableAttribute((Boolean)False)] [System.ComponentModel.DataAnnotations.RequiredAttribute()]
Name: [System.ComponentModel.DataAnnotations.DisplayAttribute(Name = "The product's name")] [System.ComponentModel.DataAnnotations.RequiredAttribute()]
RevisionNumber:
Note
It is not possible to delete or replace an existing custom attribute.
See Also
Reference
ProvideAspects(object)
CopyCustomAttributesAttribute
DataContractAttribute
DataMemberAttribute
TypeLevelAspect
IAspectProvider
AspectInstance
CustomAttributeIntroductionAspect