Introduction
In my previous article, I discussed about Aspect Oriented Programming and how we can use Spring.NET to implement AOP concepts in a multi-layered application.
In this article, I will enhance the Advice that was created in the earlier article, adding more flexibility which can be used in a real time application.
Limitations of the earlier implementation
Why do we need this part 2 in the same series? Well, there are some limitations in my earlier article and implementation. The gist of my earlier article was to introduce AOP concepts and explain how we can do a simple implementation, to help developers who are new to Spring.NET and AOP. I intentionally made it simple.
The limitations are:
- The Advice always writes to a log file, one cannot turn on or off.
- The file name path is hardcoded.
We need greater flexibility for Advices. We should be able to pass arguments to it, so that we can avoid hard-coding and allow flexibility. We should allow developers to create custom Advices to suit their business needs, yet show them a standard way of implementing. To overcome all these limitations, I have come up with this part, in which I have explained how we can implement Advices, putting the best OO concepts and design principles.
Dive deep into creating custom Advices
Application architecture
The application has three logical layers - UI, Business Logic, and Data Access. The Advice is applied to the Data Access Layer. Advices are created as a separate C# class library project. The following diagram gives the logical architecture of the application.
The code uses the Northwind database, the scripts can be downloaded from here.
Solution structure
The following diagram gives the solution structure of this application. In the interest of real estate, I have expanded only the AOP project and the rest of the projects are of similar structure like in my previous AOP article.
Detailed design
This section is the meat of the article. I will focus on creating the Advices with more flexibility. Throughout the article, I will be focusing on an Around Advice. I will leave it to you for other types of Advices.
IAroundInterface & AroundAdviceBase
I have created my custom interface called "IAroundAdvice
". This will internally inherit from the IMethodInterceptor
(Spring.NET) interface. My custom interface defines methods like Initialize
, BeforeMethodCall
, and AfterMethodCall
. The method names are self explanatory. This interface has a getter and setter property called Argument
. This interface will also inherit IDisposable
. If you need to create an Around advice, you need to implement this interface.
I have created an abstract base class called "AroundAdviceBase
" that implements the IAroundAdvice
interface. This class provides the default functionality for Around advices.
The AroundAdviceBase
class declares the Initialize
, BeforeMethodCall
, and AfterMethodCall
(methods of IAroundAdvice
) as abstract methods. The Argument
property (property of IAroundAdvice
) is declared as an abstract
property. The expectation is, the derived class will provide the actual implementation and this class will provide the structure.
The AroundAdviceBase
class has an overloaded constructor, in which it calls the Initialize()
method.
The AroundAdviceBase
class provides the implementation of the IMethodInterceptor.Invoke()
method. Inside the method, it calls the abstract methods - BeforeMethodCall()
, AfterMethodCall()
. The following code snippet shows the Invoke
method implementation of the AroundAdviceBase
class. The invocation.Proceed()
statement calls the actual target method.
public virtual object Invoke(IMethodInvocation invocation)
{
BeforeMethodCall(invocation);
object returnValue = invocation.Proceed();
AfterMethodCall(invocation);
return returnValue;
}
Instead of you implementing IAroundAdvice
and implementing all the methods, you can derive from the AroundAdviceBase
class and implement only the three abstract methods and one abstract property.
Creating the Around Advice
Once we have the interface and an abstract class defined, it's now time to create a concrete implementation of an Around advice. We want to create an Advice that would write trace messages before and after a method call. In our case, we need to derive from the AroundAdviceBase
class, and implement the abstract methods and property. The "TraceAdviceBase
" class does this for us.
The TraceAdviceBase
class provides an implementation for the Initialize
, BeforeMethodCall
, and AfterMethodCall
methods and the Argument
property.
The Initialize()
method accepts an object
as the argument, and sets the value to an instance of type "TraceAdviceArgument
". I will shortly discuss about the type "TraceAdviceArgument
". But for now, you can assume that the object is of type "TraceAdviceArgument
" and the value is set in the Initialize()
method. Initialize()
creates a TextWriteTraceListener
(System.Diagnostics
), creates a file stream object, and adds the same to the trace listener. The name of the file is passed through "TraceAdviceArgument
". The method finally initializes a bool
variable to indicate whether the TraceAdviceBase
has been initialized or not. The following code snippet shows the implementation of the Initialize()
method.
public override bool Initialize(object argument)
{
if (argument != null)
{
_traceArgument = (TraceAdviceArgument)argument;
_stream = File.Create(_traceArgument.TraceName);
_listener = new TextWriterTraceListener(_stream);
Trace.Listeners.Add(_listener);
_isInitialized = true;
}
else
{
_isInitialized = false;
}
return true;
}
The BeforeMethodCall()
method checks for the initialized status and writes a "Begin" message to the trace listener that has been created in the Initialize()
method.
public override bool BeforeMethodCall(IMethodInvocation argument)
{
if ( (_isInitialized == true ) && ( argument != null))
{
Trace.WriteLine("Begin method call " + argument.Method.Name);
Trace.Flush();
return true;
}
return false;
}
The AfterMethodCall()
method checks for the initialized status and writes a "Completed" message to the trace listener.
public override bool AfterMethodCall(IMethodInvocation argument)
{
if ((_isInitialized == true) && (argument != null))
{
Trace.WriteLine("Completed method call " + argument.Method.Name);
Trace.Flush();
return true;
}
return false;
}
The Argument
property setter will type cast the value to the "TraceAdviceArgument
" type and call Initialize()
. The implementation is as shown below:
public override object Argument
{
get { return _traceArgument; }
set
{
_traceArgument = (TraceAdviceArgument)value;
Initialize(_traceArgument);
}
}
Passing arguments
Having seen "TraceAdviceArgument
" many times, it's now time to look at this class. As the name suggests, the purpose of this class is to pass arguments to the Advice through the configuration. The class defines public get/set
properties. If you wish to pass multiple parameters to an Advice, you can wrap all such properties into a custom class, provide public get/set
properties, and configure it in the Spring object graph. Spring will create an instance of this class and pass it to Advices. The argument class implementation is given below.
public sealed class TraceAdviceArgument : IDisposable
{
# region Private Members
private string _traceName;
# endregion
# region Public Constructor
public TraceAdviceArgument()
{
_traceName = string.Empty;
}
public TraceAdviceArgument(string traceName)
{
_traceName = traceName;
}
# endregion
# region Public Property
public string TraceName
{
get { return _traceName; }
set { _traceName = value; }
}
# endregion
#region IDisposable Members
public void Dispose()
{
_traceName = null;
return;
}
#endregion
}
I have defined only one argument in this case, it is the file name. You can extend this functionality by adding another property called enabled/disabled which will determine whether to write to trace or not and pass a boolean value from the configuration.
AOP class diagram
The detailed class diagram for the entire AOP project is shown below:
Spring configuration
As we all know, everything is hooked at runtime and Spring.NET needs a configuration for the same. The following diagram gives the configuration of the Advice, Argument, and they have been applied to the Customer
DAL class.
The object "CustomerDALWithAdvice
" points to the CustomerDAL
class in the DAL assembly. It has an interceptor, TraceAroundAdvice
. This is the Advice that we have created in the previous section.
The "TraceAroundAdvice
" object is defined with the type "TraceAdviceBase
" (Advice class that we created), and the property named "Argument
" points to the "TraceAdviceArgument
" class. The TraceAdviceArgument
class takes a property called "TraceName
" and we have set "C:\Temp\Trace.Log".
So, when you use "CustomerDALWithAdvice
", the "TraceAroundAdvice
" will be applied, which takes "TraceAdviceArgument
" as an instance to the "Argument
" property. The argument class in turn defines a property called "TraceName
" which represents the physical file name.
Extending the AOP project
So far, I have discussed about the default implementation - "TraceAdviceBase
". This takes an argument from the configuration and writes trace messages into a file.
If you need to create an Around Advice that needs to perform some other task like, trace messages to DB, monitor performance statistics (time taken) etc., all you need is to derive from AroundAdviceBase
and implement the abstract methods and property.
Conclusion
This article gives detailed info about creating Advices and passing arguments. It introduces you to how you can provide a robust and flexible component that lets developers use the default functionality, or create new functionality and plug it into the system without writing too much code, without breaking the flow and design, and by following design standards set by the component.
Happy reading! Happy coding!