Introduction
AOP is widely used in the Java world, but it is still not that prominent in the .NET world. There are actually a lot of AOP frameworks for .NET out there, but those frameworks are mostly proxies and their usage are not very straight-forward.
In this how-to, I will be introducing the Fody plugin Cauldron.Interception.Fody. Cauldron.Interception.Fody provides basic interceptors to intercept methods, properties and constructors and its feature-set is aimed towards eliminating boilerplate code.
Background
Back in 2015 I was tasked to add a non-intrusive logging to a huge application that is already maintained and running for ages. For me, there was only one possibility of implementing it; Interceptors. That time I already knew PostSharp, Fody+Plugins, Spring.NET and Castle. What I wanted though are not Proxies, but IL-weavers. IL-weavers manipulate the code while building the assembly, instead of creating and inheriting from intercepted classes on runtime. After days of searching for PostSharp alternatives, I came to the conclussion that there are no any. Don't get me wrong, PostSharp is great, but it is not free (Yes... there is a free version, but it comes with limitations).
For that project though, I end up using PostSharp, but the lack of real alternative motivated me to create my own. Since beginning of 2017 I made it available as a Nuget package.
Getting Cauldron.Interception.Fody
You can get this plugin directly from the Nuget gallery or from the Visual Studio Nuget Manager.
Supported .NET version
The current version of Cauldron.Interception.Fody supports NET45, NETStandard and UWP.
Creating a your first method interceptor
In this example we will create a simple method interceptor that logs the execution of a method.
The method interceptor has to implement the 'IMethodInterceptor
' interface and inherit the 'Attribute' class.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class LoggerAttribute : Attribute, IMethodInterceptor
{
public void OnEnter(Type declaringType, object instance, MethodBase methodbase, object[] values)
{
this.AppendToFile($"Enter -> {declaringType.Name} {methodbase.Name} {string.Join(" ", values)}");
}
public void OnException(Exception e)
{
this.AppendToFile($"Exception -> {e.Message}");
}
public void OnExit()
{
this.AppendToFile("Exit");
}
private void AppendToFile(string line)
{
File.AppendAllLines("log.txt", new string[] { line });
Console.WriteLine(">> " + line);
}
}
For this example we will create a method called 'Add' that we will decorate with the 'LoggerAttribute
'.
[Logger]
private static int Add(int a, int b)
{
return a + b;
}
Now every call of the 'Add
' method will be logged in 'log.txt'.
The console will also show the following:
How does this works?
The weaver modifies your assembly and adds the interceptor to your 'Add
' method. The resulting code will look like the following:
private static LoggerAttribute loggerAttribute;
private static int Add(int a, int b)
{
if(loggerAttribute == null)
loggerAttribute = new LoggerAttribute();
try
{
loggerAttribute.OnEnter(typeof(Program), null, methodof(Add), new object { a, b });
return a + b;
}
catch(Exception e)
{
loggerAttribute.OnException(e);
throw;
}
finally
{
loggerAttribute.OnExit();
}
}
The modification happens in the IL code of your assembly during build. This means that you will not see any modification in your code.
Introducing the AssignMethodAttribute
Everybody knows auto-properties in C#.
public string MyProperty { get; set; }
But what if you want to invoke an event in the setter?
In that case you need to implement the getter and setter like this:
private string _myProperty;
public string MyProperty
{
get { this._myProperty; }
set
{
this._myProperty = value;
this.MyPropertyChanged?.Invoke();
}
}
Wouldn't it be easier if you could do this instead?
[OnPropertySet]
public string MyProperty { get; set; }
private void OnMyPropertySet()
{
this.MyPropertyChanged?.Invoke();
}
The following sample implementation of the property interceptor is using the AssignMethodAttribute
to invoke the associated method. The associated method in the case of the above example is the 'OnMyPropertySet
' method.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class OnPropertySetAttribute : Attribute, IPropertySetterInterceptor
{
[AssignMethod("On{Name}Set", false)]
public Action onSetMethod = null;
public void OnException(Exception e)
{
}
public void OnExit()
{
}
public bool OnSet(PropertyInterceptionInfo propertyInterceptionInfo, object oldValue, object newValue)
{
this.onSetMethod?.Invoke();
return false;
}
}
The AssignMethodAttribute
is decorating the field 'onSetMethod
'. The field type 'Action
' describes the method return type and parameters. In this case the associated method has to be void and parameterless.
The first parameter of the AssignMethodAttribute
is the name of the associated method, while '{Name}
' is a placeholder which will be replaced by the property's name. If the interceptor is for example decorating a property named 'OrderData
', then the weaver will be searching for a method called 'OnOrderDataSet
'.
The second parameter of the AssignMethodAttribute
tells the weaver to throw an error if the described method is not found.
There are also cases where all properties are invoking the same event in their setters. A well known example would be the PropertyChanged
event in WPF.
Let us modify our property interceptor to accept method names in its constructor.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class OnPropertySetAttribute : Attribute, IPropertySetterInterceptor
{
[AssignMethod("{CtorArgument:0}", true)]
public Action<string> onSetMethod = null;
public OnPropertySetAttribute(string methodName)
{
}
public void OnException(Exception e)
{
}
public void OnExit()
{
}
public bool OnSet(PropertyInterceptionInfo propertyInterceptionInfo, object oldValue, object newValue)
{
this.onSetMethod?.Invoke(propertyInterceptionInfo.PropertyName);
return false;
}
}
As you may have noticed the 'On{Name}Set
' is changed to {CtorArgument:0}
. The {CtorArgument:0}
placeholder will be replaced by the weaver with the value of a constructor parameter. In this case the index 0 is the parameter named 'methodName
'.
The delegate Action type is also changed to Action<string>
to be able to pass the property's name.
The following code demonstate the usage of the interceptor:
[OnPropertySet(nameof(RaisePropertyChanged))]
public string DispatchDate { get; set; }
[OnPropertySet(nameof(RaisePropertyChanged))]
public string OrderDate { get; set; }
private void RaisePropertyChanged(string propertyName)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Now every time a new value is assigned to the decorated properties, the method 'RaisePropertyChanged
' is invoked. This is already much practical than calling the 'RaisePropertyChanged
' method in every setter, but it is still impractical since it has to be added to every property.
Let us modify the AttributeUsage
of the property interceptor and add 'Class
' to the attribute's target.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
...
Now the interceptor can be applied to the class and it will intercept all properties in the class.
[OnPropertySet(nameof(RaisePropertyChanged))]
public class CoolViewModel
{
public string DispatchDate { get; set; }
public string OrderDate { get; set; }
public int OrderCount { get; set; }
private void RaisePropertyChanged(string propertyName) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Conclusion
IL-weaved interception is a very useful tool that can help minimize your code and make it more readable and maintainable, while not compromising your application's performance too much. Like everything else the over-usage of interceptors will lead to the opposite. If everything is happenning auto-magically it will be hard to understand what is happening in your code. So use it wisely.
While logging might be the most obvious usage of interceptors (In fact almost every example I saw in the internet is about logging), interception shows its full potential in scenarios where a lot of boilerplate code is involved. Just to give you an idea where I personally use interceptors, here a list of some interceptors I currently use:
PerformanceLoggerAttribute
- A property and method interceptor, that logs the execution performance. OnDemandAttribute
- A property interceptor, that loads it's value when the getter is called; Some sort of LazyLoading. DBInsureConnectionAttribute
- A method interceptor that checks the DB connection and automatically established a connection if not connected. RegistryValueAttribute
- A property interceptor that get or set a defined registry value if the getter or setter is invoked. PriviledgeAttribute
- A method interceptor that checks if the user has the required privilege to execute a method; throws an exception otherwise. - RegisterChildrenAttribute - A property interceptor that registers the property value to the declaring viewmodel.
Links
https://github.com/Capgemini/Cauldron
https://github.com/Fody/Fody
https://github.com/Capgemini/Cauldron/wiki