Introduction
Recently, I wrote several articles Configurable Aspects for MEF, Configurable Aspects for Unity Container and Configurable Aspects for Windsor Container. Though I can keep writing similar articles for other IoC containers, I think that it is better to come up with a general AOP model which can be applied to any IoC container.
In this article, I discuss an AOP model that has capabilities to retrieve aspects from configuration file, keep an inventory of the runtime aspects and attach aspects to objects. Then, I show you how to apply it to IoC containers like MEF, Unity and Windsor. You should be able to apply it to other IoC containers by following these examples. Finally, I show you how to define aspects, configure them in application configuration file, and use this model to add AOP capabilities to an application.
Background
The Dynamic Decorator is a tool for extending functionality of objects by attaching behaviors to them instead of by modifying their classes or creating new classes. It is attractive because it saves you from design or redesign of your classes. Please read the articles Dynamic Decorator Pattern and Add Aspects to Object Using Dynamic Decorator to understand more about the Dynamic Decorator, what problems it tries to solve, how it solves the problems and its limits.
There are several other articles that discuss how the Dynamic Decorator is used for application development and compare it with other similar technologies. Please refer to the References section of this article for them.
Once you go over some of the articles, you will see that the Dynamic Decorator is simple to use and powerful. In this article, I describe a configurable model of the Dynamic Decorator, which makes its use even easier and still as powerful. This model is abstracted into an AOP container, which has capabilities to retrieve aspects from configuration file, keep an inventory of runtime aspects and attach aspects to objects.
AOPContainer
AOPContainer
is an abstract
class. It parses aspect configurations in application configuration file and keeps an inventory of aspects at runtime. It provides a template for object creation and implements mechanism to attach aspects to objects. It defines a few public
methods as an application programming interface. Without involving implementation details, the following texts show the high level design of this class.
abstract public class AOPContainer
{
private object target;
abstract protected object ObjectResolver<T>();
public V Resolve<T, V>() where T : V
{
target = ObjectResolver<T>();
}
public V Resolve<T, V>(string methods) where T : V
{
target = ObjectResolver<T>();
}
public T GetTarget<T>()
{
}
}
The Resolve<T, V>()
creates a T
type target object using the ObjectResolver<T>()
and returns a V
interface which is implemented by T
. Note that the Resolve<T, V>()
is a template method. Derived classes have to implement the abstract
method ObjectResolver<T>()
to return an object. The rest of logic (code omitted) matches aspects configured in the configuration file and wires the target with the aspects. The Resolve<T, V>(string methods)
is an overload of the Resolve<T, V>()
and basically does the same as Resolve<T, V>()
with additional functionality to match aspect configuration with method names. The GetTarget<T>()
simply returns the target object. Please refer to the complete code in the download file for details.
To apply the functionality of the AOPContainer
to a particular IoC container, you need to create a class that derives from the AOPContainer
. The derived class should provide specifics related to the IoC container and implement ObjectResolver<T>()
method to return an object created by the IoC container.
Three classes are derived from the AOPContainer
as follows . They are AOPMefContainer
, AOPUnityContainer
and AOPWindsorContainer
for MEF, Unity and Windsor, respectively.
public class AOPMefContainer : AOPContainer
{
private CompositionContainer container;
public AOPMefContainer(CompositionContainer compositecontainer)
{
container = compositecontainer;
}
override protected object ObjectResolver<T>()
{
return container.GetExport<T>().Value;
}
}
public class AOPUnityContainer : AOPContainer
{
private IUnityContainer container;
public AOPUnityContainer(IUnityContainer unitycontainer)
{
container = unitycontainer;
}
override protected object ObjectResolver<T>()
{
return container.Resolve<T>();
}
}
public class AOPWindsorContainer : AOPContainer
{
private IWindsorContainer container;
public AOPWindsorContainer(IWindsorContainer windsorcontainer)
{
container = windsorcontainer;
}
override protected object ObjectResolver<T>()
{
return container.Resolve<T>();
}
}
As you see, it is extremely easy to extend AOPContainer
for an IoC container. All you need to do is provide a specific object of the IoC container to a constructor and override the ObjectResolver<T>()
method to create an object using the IoC container.
The design of AOPContainer
unveils a few important points. First, the AOP model is independent of IoC containers. Aspects are defined, configured and attached to objects without concerning about what IoC container is used or how objects are created. Second, with all the functionality provided by AOPContainer
, it is very easy to extend your favorite IoC container to have AOP capabilities. Just derive a class from AOPContainer
by providing specifics of your IoC container as shown above. Third, the programming interface of AOPContainer
is consistent across IoC containers. Therefore, changing IoC container doesn't change the code that uses the programming interface in an application.
In the following sections, an example is presented to show how to define aspects, configure them and use the derived classes to add AOP capabilities to an application.
Using the Code
Say, you have a simple component Employee
that implements two interfaces IEmployee
and INotifyPropertyChanged
as follows:
public interface IEmployee
{
System.Int32? EmployeeID { get; set; }
System.String FirstName { get; set; }
System.String LastName { get; set; }
System.DateTime DateOfBirth { get; set; }
System.Int32? DepartmentID { get; set; }
System.String DetailsByLevel(int iLevel);
}
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
[PartCreationPolicy(CreationPolicy.NonShared)]
[Export]
public class Employee : IEmployee, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#region Properties
public System.Int32? EmployeeID { get; set; }
public System.String FirstName { get; set; }
public System.String LastName { get; set; }
public System.DateTime DateOfBirth { get; set; }
public System.Int32? DepartmentID { get; set; }
#endregion
public Employee(
System.Int32? employeeid
, System.String firstname
, System.String lastname
, System.DateTime bDay
, System.Int32? departmentID
)
{
this.EmployeeID = employeeid;
this.FirstName = firstname;
this.LastName = lastname;
this.DateOfBirth = bDay;
this.DepartmentID = departmentID;
}
public Employee() { }
public System.String DetailsByLevel(int iLevel)
{
System.String s = "";
switch (iLevel)
{
case 1:
s = "Name:" + FirstName + " " + LastName + ".";
break;
case 2:
s = "Name: " + FirstName + " " + LastName + ",
Birth Day: " + DateOfBirth.ToShortDateString() + ".";
break;
case 3:
s = "Name: " + FirstName + " " + LastName + ",
Birth Day: " + DateOfBirth.ToShortDateString() + ",
Department:" + DepartmentID.ToString() + ".";
break;
default:
break;
}
return s;
}
}
The above code is normal C# code for interface and class definitions. The two attributes [PartCreationPolicy(CreationPolicy.NonShared)]
and [Export]
are used by MEF to specify that an instance of the Employee
is the exported object, and a new non-shared instance will be created for every requestor. They are not required by Unity or Windsor, and are simply ignored by Unity and Windsor.
Define Aspects
Aspects are cross-cutting concerns. For Dynamic Decorator, an aspect is a method that takes an AspectContext
type object as its first parameter and an object[]
type object as its second parameter and returns void
.
You may design your aspects in a more general way to use them in various situations. You may also design your aspects for some particular situations. For example, you can define your entering/exiting log aspects in a general way and put them in the class SysConcerns
as follows:
public class SysConcerns
{
static SysConcerns()
{
ConcernsContainer.runtimeAspects.Add
("DynamicDecoratorAOP.SysConcerns.EnterLog",
new Decoration(SysConcerns.EnterLog, null));
ConcernsContainer.runtimeAspects.Add
("DynamicDecoratorAOP.SysConcerns.ExitLog",
new Decoration(SysConcerns.ExitLog, null));
}
public static void EnterLog(AspectContext ctx, object[] parameters)
{
StackTrace st = new StackTrace(new StackFrame(4, true));
Console.Write(st.ToString());
IMethodCallMessage method = ctx.CallCtx;
string str = "Entering " + ctx.Target.GetType().ToString() + "." +
method.MethodName +
"(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
public static void ExitLog(AspectContext ctx, object[] parameters)
{
IMethodCallMessage method = ctx.CallCtx;
string str = "Exiting " + ctx.Target.GetType().ToString() + "." +
method.MethodName +
"(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
}
As you can see, these methods access Target
and CallCtx
in a general way and can be shared by various types of objects to write entering/exiting logs.
On the other hand, some aspects may need to access more specific information. For example, you want to attach a change notification functionality only to an Employee
object when its property is set. The following code defines some specific aspects:
class LocalConcerns
{
static LocalConcerns()
{
ConcernsContainer.runtimeAspects.Add
("ConsoleUtil.LocalConcerns.NotifyChange",
new Decoration(LocalConcerns.NotifyChange, null));
ConcernsContainer.runtimeAspects.Add
("ConsoleUtil.LocalConcerns.SecurityCheck",
new Decoration(LocalConcerns.SecurityCheck, null));
}
public static void NotifyChange(AspectContext ctx, object[] parameters)
{
((Employee)ctx.Target).NotifyPropertyChanged(ctx.CallCtx.MethodName);
}
public static void SecurityCheck(AspectContext ctx, object[] parameters)
{
Exception exInner = null;
try
{
if (parameters != null && parameters[0] is WindowsPrincipal &&
((WindowsPrincipal)parameters[0]).IsInRole
("BUILTIN\\" + "Administrators"))
{
return;
}
}
catch ( Exception ex)
{
exInner = ex;
}
throw new Exception("No right to call!", exInner);
}
}
In the above code, the NotifyChange
method can only be used by a target of Employee
while the SecurityCheck
requires a WindowsPrincipal
object as a parameter.
Note: You may already have noticed that there is a static
constructor in each of the classes SysConcerns
and LocalConcerns
. Inside the static
classes, each of aspect methods defined in the classes is used to create an instance of Decoration
which is then added to a dictionary ConcernsContainer.runtimeAspects
. The definition of ConcernsContainer
is as follows:
public class ConcernsContainer
{
static public Dictionary<string, Decoration> runtimeAspects =
new Dictionary<string, Decoration>();
}
The purpose of this Dictionary
and the static
constructors is to keep an inventory for all Decoration
objects based on the aspect methods defined in the application and make them accessible by the corresponding method names. It makes it possible to configure aspects in the application configuration file by specifying the corresponding method names.
Configure Aspects
In the configuration file, you specify how aspects are associated with objects. Here are some examples to demonstrate how the aspects are configured:
<configuration>
<configSections>
<section name="DynamicDecoratorAspect"
type="DynamicDecoratorAOP.Configuration.DynamicDecoratorSection,
DynamicDecoratorAOP.Configuration" />
</configSections>
<DynamicDecoratorAspect>
<objectTemplates>
<add name="1"
type="ThirdPartyHR.Employee"
interface="ThirdPartyHR.IEmployee"
methods="DetailsByLevel,get_EmployeeID"
predecoration="SharedLib.SysConcerns.EnterLog,
SharedLib.SysConcerns"
postdecoration=""/>
<add name="2"
type="ThirdPartyHR.Employee"
interface="ThirdPartyHR.IEmployee"
methods="set_EmployeeID"
predecoration="ConsoleUtil.LocalConcerns.SecurityCheck"
postdecoration="ConsoleUtil.LocalConcerns.NotifyChange"/>
</objectTemplates>
</DynamicDecoratorAspect>
</configuration>
First of all, you need to add a section <DynamicDecoratorAspect>
in your configuration file. Then, inside <objectTemplates>
of <DynamicDecoratorAspect>
, you add individual elements. For each element inside <objectTemplates>
, the following attributes need to be specified:
type
- target type interface
- interface returned methods
- names of target methods which will be attached the aspects specified by predecoration
and postdecoration
predecoration
- preprocessing aspect postdecoration
- postprocessing aspect
Notes
- The names in the value of the
methods
attribute are comma separated. For example, "DetailsByLevel,get_EmployeeID"
. - The value of the
predecoration
attribute has two parts and is separated by a comma. The first part specifies the aspect name while the second part specifies the assembly name in which the aspect is defined, for example, "SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
. If the second part is not specified, it is assumed that the aspect is defined in the entry assembly, for example, "ConsoleUtil.LocalConcerns.SecurityCheck"
. - The value of the
postdecoration
attribute has two parts and is separated by a comma. The first part specifies the aspect name while the second part specifies the assembly name in which the aspect is defined. If the second part is not specified, it is assumed that the aspect is defined in the entry assembly.
Use AOP Containers
You create an IoC container and register types to it as you normally do with an IoC container. Then, instead of resolving an object from the container, you pass the container to its extended AOP container, then use the Resolve<T, V>()
method of the AOPContainer
. It creates an object and attaches aspects to the object. The object returned from this method has AOP capabilities already. The following code demonstrates how the AOPMefContainer
, AOPUnityContainer
and AOPWindsorContainer
are used to add AOP capabilities to an Employee
object.
class Program
{
static void Main(string[] args)
{
AOPContainer aopcontainer = null;
IDisposable container = null;
switch (args[0])
{
case "0":
{
container = new WindsorContainer();
((IWindsorContainer)container).Register(AllTypes
.FromAssembly(Assembly.LoadFrom("Employee.dll"))
.Where(t => t.Name.Equals("Employee"))
.Configure(c => c.LifeStyle.Transient)
);
aopcontainer = new AOPWindsorContainer
((IWindsorContainer)container);
Console.WriteLine("Using AOPWindsorContainer.");
}
break;
case "1":
{
container = new UnityContainer();
((IUnityContainer)container).RegisterType<IEmployee,
Employee>(new InjectionConstructor());
aopcontainer = new AOPUnityContainer
((IUnityContainer)container);
Console.WriteLine("Using AOPUnityContainer.");
}
break;
case "2":
{
AggregateCatalog catalog = new AggregateCatalog();
catalog.Catalogs.Add(new AssemblyCatalog
(Assembly.LoadFrom("Employee.dll")));
container = new CompositionContainer(catalog);
aopcontainer = new AOPMefContainer
((CompositionContainer)container);
Console.WriteLine("Using AOPMefContainer.");
}
break;
default:
{
Console.WriteLine("Invalid number for an AOP container.");
Console.ReadLine();
}
return;
}
IEmployee emp = aopcontainer.Resolve<Employee, IEmployee>();
emp.EmployeeID = 1;
emp.FirstName = "John";
emp.LastName = "Smith";
emp.DateOfBirth = new DateTime(1990, 4, 1);
emp.DepartmentID = 1;
emp.DetailsByLevel(2);
Employee target = null;
IEmployee emp1 = aopcontainer.Resolve<Employee, IEmployee>
("set_EmployeeID");
try
{
Thread.GetDomain().SetPrincipalPolicy
(PrincipalPolicy.WindowsPrincipal);
Decoration dec = ConcernsContainer.runtimeAspects
["ConsoleUtil.LocalConcerns.SecurityCheck"];
dec.Parameters = new object[] { Thread.CurrentPrincipal };
target = aopcontainer.GetTarget<Employee>();
target.PropertyChanged += new PropertyChangedEventHandler
(PropertyChanged_Listener);
emp1.EmployeeID = 2;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
container.Dispose();
Console.ReadLine();
}
static void PropertyChanged_Listener(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine(e.PropertyName.ToString() + " has changed.");
}
}
The statements in switch
clause are IoC container specific. Take the case "0"
as example. An object of WindsorContainer
is created and assigned to container
, which is used to register types. Instead of using it to resolve an object, an instance of AOPWindsorContainer
is created by passing the container
into its constructor and assigned to aopcontainer
. The rest of code after the switch
clause is IoC independent.
To create an object and attach aspects to it, the Resolve<T, V>()
method is used by specifying the target type T
and the returned interface type V
. For example, in the above code, the Employee
is specified as the target type and IEmployee
as returned interface type. It returns a proxy of IEmployee
type. That's it. Now, when using the emp
, it starts writing the entering log.
The Resolve<T, V>()
finds the first matched element in the configuration file by looking for type
attribute as T
and interface
attribute as V
, and then, uses the element settings to create a proxy. For Resolve<Employee, IEmployee>()
, it tries to find type
attribute as "ThirdPartyHR.Employee"
and interface
attribute as "ThirdPartyHR.IEmployee"
in the configuration file. The first element in the configuration is matched. Therefore, the value "DetailsByLevel,get_EmployeeID"
of methods
attribute, the value "SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
of predecoration
attribute and the value ""
of postdecoration
attribute are used to create a proxy. When using emp
, only methods DetailsByLevel
and get_EmployeeID
will write the entering log.
The Resolve<T, V>(string methods)
finds the first matched element in the configuration file by looking for type
attribute as T
, interface
attribute as V
and methods
attribute as methods
. For example, the code Resolve<Employee, IEmployee>("set_EmployeeID")
tries to match type
attribute as "ThirdPartyHR.Employee"
, interface
attribute as "ThirdPartyHR.IEmployee"
and methods
attribute as "set_EmployeeID"
, and the second element in the configuration is matched. Therefore, the value "ConsoleUtil.LocalConcerns.SecurityCheck"
of predecoration
attribute and the value "ConsoleUtil.LocalConcerns.NotifyChange"
of postdecoration
attribute are used to create a proxy. When using emp1
, only method set_EmployeeID
will check the security before its invocation and raise a notification after its invocation.
There are a few more points worth noting. Before the emp1
invokes the aspect LocalConcerns.SecurityCheck
, its parameters
argument needs to be updated to a WindowsPrincipal
object. It can be achieved by getting the Decoration
object associated with the aspect using the Dictionary
of the ConcernsContainer
and then setting its Parameters
property. The first three lines of the above code in the try
block does this.
In order to capture the event raised from the aspect LocalConcerns.NotifyChange
after the method set_EmployeeID
sets the property, you need to register a listener to the target object of Employee
before you call the method set_EmployeeID
. To achieve this, you use GetTarget<T>()
method of AOPWindsorContainer
to get the target object, then, register the listener to the target object. The last three lines of code in try
block does this.
When executing the program, you see the following output:
If you comment out the line Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
, you see the following output:
Points of Interest
Any IoC container can have AOP capabilities by configuration by extending AOPContainer
.
Configurable aspects provided by AOPContainer
combined with IoC containers makes AOP very simple. You define your aspect methods and then add elements in application configuration file to associate your objects with the aspects. Most of time, that is all you need to do. In some special cases, you can still update the parameters argument of an aspect and get target object using code.
References
The following articles help you understand Dynamic Decorator and its features:
- Dynamic Decorator Pattern
- Add Aspects to Object Using Dynamic Decorator
The following articles discuss how the Dynamic Decorator can help you improve your application development:
- Components, Aspects and Dynamic Decorator
- Components, Aspects and Dynamic Decorator for ASP.NET Application
- Components, Aspects and Dynamic Decorator for ASP.NET MVC Application
- Components, Aspects and Dynamic Decorator for Silverlight / WCF Service Application
- Components, Aspects and Dynamic Decorator for MVC/AJAX/REST Application
The following articles compare the Dynamic Decorator with some other similar tools:
- Dynamic Decorator and Castle DynamicProxy Comparison
- Dynamic Decorator, Unity and Castle DynamicProxy Comparison
The following articles discuss some miscellaneous topics regarding Dynamic Decorator and AOP:
- Performance of Dynamic Decorator
- Generic Dynamic Decorator
- Aspects to Object vs. Aspects to Class
History
- 19th September, 2011: Initial post