In .NET, both fields and properties are elements that can be read and written. You can intercept get and set operations using the LocationInterceptionAspect. It makes it possible to develop useful aspects, such as validation, filtering, change tracking, change notification, or property virtualization (where the property is backed by a registry value, for instance).
Intercepting get operations
In this example, we will see how to create an aspect that filters the value read from a field or property.
Creating an aspect that filters the value read from a field or property
Add a reference to the PostSharp package to your project.
Create an aspect that inherits from LocationInterceptionAspect and add the custom attribute [PSerializableAttribute].
Override the OnGetValue(LocationInterceptionArgs) method. Calling
base.OnGetValueactually retrieves the value from the underlying field or property, and populates theValueproperty. Add some code to check if the property currently is set tonull. If the current value isnull, we want to return a predefined value. To do this we can set theValueproperty. Any time this property is requested, and it is set tonull, the value"foo"will be returned.[PSerializable] public class StringCheckerAttribute : LocationInterceptionAspect { public override void OnGetValue( LocationInterceptionArgs args ) { base.OnGetValue( args ); if ( args.Value == null ) { args.Value = "foo"; } } }Now that you have a complete getter interception aspect, you can attach it to the target code. Add the attribute to properties or fields to intercept them.
public class Customer { [StringChecker] private readonly string? _address; public Customer( string? address ) { _address = address; } [StringChecker] public string? Name { get; set; } public string? Address { get { return _address; } } }Note
Adding aspects to target code one property or field at a time can be a tedious process. There are a number of techniques in the article Adding Aspects to Multiple Declarations Using Attributes that explain how to add aspects en masse.
Now when you create an instance of a customer and immediately try to access the
NameandAddressvalues, the get request will be intercepted and null values will be returned as"foo".class Program { static void Main() { var customer = new Customer( "123 Main Street" ); Console.WriteLine( "Address: {0}", customer.Address ); Console.WriteLine( "Name: {0}", customer.Name ); } }The program prints the following output:
Address: 123 Main Street Name: foo
Property and field interception is a simple and seamless task. Once you have intercepted your target, you can act on the target or allow the original code to execute.
Intercepting set operations
The previous section showed how to intercept a get accessor. Intercepting a set accessor is accomplished in a similar manner by implementing OnSetValue(LocationInterceptionArgs) in the LocationInterceptionAspect.
When applied to a property with a set accessor, OnSetValue(LocationInterceptionArgs) will intercept the set operation. In the Customer example shown below, OnSetValue(LocationInterceptionArgs) will be called whenever the Name property is set:
public class Customer
{
[StringChecker]
public string? Name { get; set; }
}
You can set the Value property of LocationInterceptionArgs and then call ProceedSetValue() to pass a different value for the property. For example, OnSetValue(LocationInterceptionArgs) could be used to check for a null string, and then change the string to a non-null value:
[PSerializable]
public class StringCheckerAttribute : LocationInterceptionAspect
{
public override void OnSetValue( LocationInterceptionArgs args )
{
if ( args.Value == null )
{
args.Value = "Empty String";
}
args.ProceedSetValue();
}
}
When the program sets the Name property to "Jane" and then to null, it prints the following values of the property:
Name: Jane
Name: Empty String
Warning
Inline initializers of instance fields and properties are not intercepted by OnSetValue(LocationInterceptionArgs). To react to the initialization of an instance field or property, use the OnInstanceLocationInitialized(LocationInitializationArgs) advice. See Reacting to initialization of instance fields and properties for details.
Getting and setting the underlying property
PostSharp provides a mechanism to check a property's underlying value via the GetCurrentValue() method. This can be useful to check the current property value when a setter is called and then take some appropriate action.
For example, the following snippet shows a modified OnSetValue(LocationInterceptionArgs) method that gets the current underlying property value and compares the (new) value passed into the setter against the current value. If the current and new values do not match, a message is written:
public override void OnSetValue( LocationInterceptionArgs args )
{
// Get the current underlying value.
string? existingValue = (string?) args.GetCurrentValue();
if ( existingValue != (string?) args.Value )
{
Console.WriteLine( "Value changed." );
args.ProceedSetValue();
}
}
Note
GetCurrentValue() will call the underlying property getter without going through OnGetValue(LocationInterceptionArgs). If several aspects are applied to the property (or to the property setter), GetCurrentValue() will go through the next aspect in the chain of invocation.
PostSharp also provides a mechanism to set the underlying property in a getter via the SetNewValue(object) method of LocationInterceptionArgs. This could be used, for example, to ensure that a default value is assigned to the underlying property if there is currently no value. The following snippet shows a modified OnGetValue(LocationInterceptionArgs) method that gets the current underlying value and sets a default value if the current value is null:
public override void OnGetValue( LocationInterceptionArgs args )
{
object? o = args.GetCurrentValue();
if ( o == null )
{
args.SetNewValue( "value not set" );
}
base.OnGetValue( args );
}
With both methods in the aspect, a program that reads the Name property, sets it to "John" twice, and reads it again prints the following output:
Name: value not set
Setting the name to John.
Value changed.
Setting the name to John again.
Name: John
Intercepting fields
One benefit of implementing a LocationInterceptionAspect is that it can be applied directly to fields, allowing for reads and writes to those fields to be intercepted, just like with properties.
To apply a LocationInterceptionAspect implementation to a field, set it as an attribute on the field, as you do for a property:
public class Customer
{
// ...
[StringChecker]
public string? name;
}
With the attribute applied to the name field, all attempts to get and set that field will be intercepted by StringChecker in its OnGetValue(LocationInterceptionArgs) and OnSetValue(LocationInterceptionArgs) methods.
Note that when a LocationInterceptionAspect is added to a field, the field is replaced by a property of the same name and visibility. The field itself is renamed and made private.
The following output shows that the get and set operations on the name field are intercepted, and that name is now a public property:
Setting name to Jane.
Getting name.
Name: Jane
Customer.name is a public property: True
Getting the property or field being accessed
Information about the property or field being intercepted can be obtained through the LocationInterceptionArgs via its Binding property.
Here you will find LocationInfo, which can represent a FieldInfo, a PropertyInfo, or a ParameterInfo (although LocationInterceptionAspect cannot be added to parameters).
One use for this is to reflect the property name whenever a property is changed. In the following example, we have an Entity class that implements INotifyPropertyChanged and a public OnPropertyChanged method that allows notifications to be made whenever a property is changed.
class Entity : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged( string propertyName )
{
if ( PropertyChanged != null )
{
PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
}
}
}
With the ability to invoke an OnPropertyChanged event, we can create a LocationInterceptionAspect that invokes this event when setting a value and passes in the property name from the underlying PropertyInfo object:
[PSerializable]
[MulticastAttributeUsage( MulticastTargets.Property )]
public class NotifyPropertyChangedAttribute : LocationInterceptionAspect
{
public override void OnSetValue( LocationInterceptionArgs args )
{
if ( !Equals( args.Value, args.GetCurrentValue() ) )
{
args.ProceedSetValue();
((Entity) args.Instance).OnPropertyChanged( args.Binding.LocationInfo.Name );
}
}
}
Note
This example is a simplistic implementation of the NotifyPropertyChangedAttribute aspect. For a production-ready implementation, see INotifyPropertyChanged.
The MulticastAttributeUsageAttribute attribute restricts the aspect to properties, so that it is not applied to fields, including the backing fields of automatic properties, when it is added to a class.
This aspect can then be applied to the Customer class, which derives from Entity:
[NotifyPropertyChanged]
class Customer : Entity
{
public string? Name { get; set; }
}
Now when the Name property is changed, the NotifyPropertyChanged aspect will invoke the Entity.OnPropertyChanged method, passing in the property name retrieved from its underlying property. When the program sets the Name property twice to the same value, only the first change raises the event:
Setting the name to John.
Property Name changed.
Setting the name to John again.
Reacting to initialization of instance fields and properties
C# and VB.NET allow fields and properties to be initialized on the same line as their declaration instead of in the constructor. This is called the initialization of the field or property. While the initialization of static fields and properties can be intercepted by OnSetValue(LocationInterceptionArgs), this method will not be called while initializing instance fields or properties. The reason for that limitation is that the OnSetValue(LocationInterceptionArgs) method requires a valid this object, which we do not have because inline initialization is performed before the base constructor has been called.
The initialization of instance fields or properties cannot be intercepted. However, you can react to their initial value by implementing the OnInstanceLocationInitialized(LocationInitializationArgs) advice. This advice will be invoked after the completion of the base constructor and before the execution of your own constructor.
Example
Suppose you have an item in an auction and you want to take an action each time the price of the item changes. You have the following class:
class AuctionItem
{
[Report]
public int Price { get; set; } = 40000;
}
And you write your LocationInterceptionAspect:
[PSerializable]
class ReportAttribute : LocationInterceptionAspect
{
public override void OnSetValue( LocationInterceptionArgs args )
{
args.ProceedSetValue();
Console.WriteLine( "The " + args.LocationName + " of the item changed to " + args.Value );
}
public override void OnInstanceLocationInitialized( LocationInitializationArgs args )
{
Console.WriteLine( "The " + args.LocationName + " of the item starts out at " + args.Value );
}
}
Then the following code snippet:
AuctionItem item = new AuctionItem();
item.Price += 2000;
will have this output:
The Price of the item starts out at 40000
The Price of the item changed to 42000
See the Remarks section of OnSetValue(LocationInterceptionArgs) and OnInstanceLocationInitialized(LocationInitializationArgs) for more details.
See Also
Reference
LocationInterceptionAspect
OnGetValue(LocationInterceptionArgs)
OnSetValue(LocationInterceptionArgs)
OnInstanceLocationInitialized(LocationInitializationArgs)
LocationInterceptionArgs
LocationInitializationArgs
LocationInfo
FieldInfo
PropertyInfo
ParameterInfo
INotifyPropertyChanged
NotifyPropertyChangedAttribute
Other Resources
Adding Aspects to Multiple Declarations Using Attributes
INotifyPropertyChanged