Introduction
Aspect Oriented Programming (AOP) is a very powerful approach to avoid boilerplate code and achieve better modularity. The main idea is to add behavior (advice) to the existing code without making any changes in the code itself. In Java, this idea is implemented in AspectJ and Spring frameworks. There are PostSharp (not free), NConcern and some other frameworks (not very popular and easy to use) to do almost the same in .NET.
It is also possible to use RealProxy
class to implement AOP. You can find some examples of how to do it:
Example1: Aspect-Oriented Programming : Aspect-Oriented Programming with the RealProxy Class
Example2: MSDN.
Unfortunately, these examples have some significant drawbacks. Example1 does not support out parameters. Example2 has limitation. Decorated class should be inherited from MarshalByRefObject
(it could be a problem if it is not your class). Also, both examples do not support asynchronous functions. Ok, technically it is supported. But I’m expecting “after” code execution AFTER task completion, and if “after” code tries to get value of Result
property from Task
object (for example, during result serialization), it makes asynchronous code synchronous (not cool ☹).
I tried to fix the first example.
Before You Continue Reading
This article is how to fix some problems in solution, provided by Bruno Sonnino (Example1). That article has great explanation on how code is supposed to work and what kind of problems it solves. Please read Aspect-Oriented Programming : Aspect-Oriented Programming with the RealProxy Class first.
Alternative Solution
It is also possible to use DispatchProxy
to do the same. You can find an example of how to do it in my article, Aspect Oriented Programming in C# using DispatchProxy.
Source Code
Code of this article and example of using DispatchProxy
with unit tests for both can be found on GitHub.
Solution
This solution is an example of logging implementation. Code could be found here.
Differences with original code:
- Extension method
GetDescription
was added to log Exception data (Extensions.cs). DynamicProxy
class was renamed to LoggingAdvice
. - Constructor was made
private
. Static
function Create
creates class instance and returns TransparentProxy
(LoggingAdvice.cs - lines 35-41). It makes it impossible to create an instance of LoggingAdvice
class, because only proxy, created by this class, is going to be used. LoggingAdvice
receives actions to log function calls and errors, and function to serialize complex type values as parameters (LoggingAdvice.cs - lines 19-20). TaskScheduler
was added as an optional parameter to support task results logging using different task scheduler. TaskScheduler.FromCurrentSynchronizationContext()
will be used by default (LoggingAdvice.cs - line 36). - Functions
LogException
, LogBefore
and LogAfter
were added to log corresponding data (LoggingAdvice.cs - lines 150-209). Try
/catch
blocks were added to handle situation when logInfo
function throws an exception (LoggingAdvice.cs - lines 53-61, 99-107). - If result value of the function is
Task
, execution result will be logged after task completion (LoggingAdvice.cs - lines 69-96).
- Added output parameters support (LoggingAdvice.cs - lines 63, 110-111).
Extension to Log Exception (Extensions.cs)
using System;
using System.Text;
namespace AOP
{
public static class Extensions
{
public static string GetDescription(this Exception e)
{
var builder = new StringBuilder();
AddException(builder, e);
return builder.ToString();
}
private static void AddException(StringBuilder builder, Exception e)
{
builder.AppendLine($"Message: {e.Message}");
builder.AppendLine($"Stack Trace: {e.StackTrace}");
if (e.InnerException != null)
{
builder.AppendLine("Inner Exception");
AddException(builder, e.InnerException);
}
}
}
}
Logging Advice (LoggingAdvice.cs)
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Text;
using System.Threading.Tasks;
namespace AOP
{
public class LoggingAdvice<T> : RealProxy
{
private readonly T _decorated;
private readonly Action<string> _logInfo;
private readonly Action<string> _logError;
private readonly Func<object, string> _serializeFunction;
private readonly TaskScheduler _loggingScheduler;
private LoggingAdvice(T decorated, Action<string> logInfo, Action<string> logError,
Func<object, string> serializeFunction, TaskScheduler loggingScheduler)
: base(typeof(T))
{
if (decorated == null)
{
throw new ArgumentNullException(nameof(decorated));
}
_decorated = decorated;
_logInfo = logInfo;
_logError = logError;
_serializeFunction = serializeFunction;
_loggingScheduler = loggingScheduler ?? TaskScheduler.FromCurrentSynchronizationContext();
}
public static T Create(T decorated, Action<string> logInfo, Action<string> logError,
Func<object, string> serializeFunction, TaskScheduler loggingScheduler = null)
{
var advice = new LoggingAdvice<T>
(decorated, logInfo, logError, serializeFunction, loggingScheduler);
return (T)advice.GetTransparentProxy();
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
if (methodCall != null)
{
var methodInfo = methodCall.MethodBase as MethodInfo;
if (methodInfo != null)
{
try
{
try
{
LogBefore(methodCall, methodInfo);
}
catch (Exception ex)
{
LogException(ex);
}
var args = methodCall.Args;
var result = typeof(T).InvokeMember(
methodCall.MethodName,
BindingFlags.InvokeMethod | BindingFlags.Public |
BindingFlags.Instance, null, _decorated, args);
if (result is Task)
{
((Task)result).ContinueWith(task =>
{
if (task.Exception != null)
{
LogException(task.Exception.InnerException ?? task.Exception,
methodCall);
}
else
{
object taskResult = null;
if (task.GetType().IsGenericType &&
task.GetType().GetGenericTypeDefinition() == typeof(Task<>))
{
var property = task.GetType().GetProperties()
.FirstOrDefault(p => p.Name == "Result");
if (property != null)
{
taskResult = property.GetValue(task);
}
}
LogAfter(methodCall, methodCall.Args, methodInfo, taskResult);
}
},
_loggingScheduler);
}
else
{
try
{
LogAfter(methodCall, args, methodInfo, result);
}
catch (Exception ex)
{
LogException(ex);
}
}
return new ReturnMessage(result, args, args.Length,
methodCall.LogicalCallContext, methodCall);
}
catch (Exception ex)
{
if (ex is TargetInvocationException)
{
LogException(ex.InnerException ?? ex, methodCall);
return new ReturnMessage(ex.InnerException ?? ex, methodCall);
}
}
}
}
throw new ArgumentException(nameof(msg));
}
private string GetStringValue(object obj)
{
if (obj == null)
{
return "null";
}
if (obj.GetType().IsPrimitive || obj.GetType().IsEnum || obj is string)
{
return obj.ToString();
}
try
{
return _serializeFunction?.Invoke(obj) ?? obj.ToString();
}
catch
{
return obj.ToString();
}
}
private void LogException(Exception exception, IMethodCallMessage methodCall = null)
{
try
{
var errorMessage = new StringBuilder();
errorMessage.AppendLine($"Class {_decorated.GetType().FullName}");
errorMessage.AppendLine($"Method {methodCall?.MethodName} threw exception");
errorMessage.AppendLine(exception.GetDescription());
_logError?.Invoke(errorMessage.ToString());
}
catch (Exception)
{
}
}
private void LogAfter(IMethodCallMessage methodCall, object[] args,
MethodInfo methodInfo, object result)
{
var afterMessage = new StringBuilder();
afterMessage.AppendLine($"Class {_decorated.GetType().FullName}");
afterMessage.AppendLine($"Method {methodCall.MethodName} executed");
afterMessage.AppendLine("Output:");
afterMessage.AppendLine(GetStringValue(result));
var parameters = methodInfo.GetParameters();
if (parameters.Any())
{
afterMessage.AppendLine("Parameters:");
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var arg = args[i];
afterMessage.AppendLine($"{parameter.Name}:{GetStringValue(arg)}");
}
}
_logInfo?.Invoke(afterMessage.ToString());
}
private void LogBefore(IMethodCallMessage methodCall, MethodInfo methodInfo)
{
var beforeMessage = new StringBuilder();
beforeMessage.AppendLine($"Class {_decorated.GetType().FullName}");
beforeMessage.AppendLine($"Method {methodCall.MethodName} executing");
var parameters = methodInfo.GetParameters();
if (parameters.Any())
{
beforeMessage.AppendLine("Parameters:");
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var arg = methodCall.Args[i];
beforeMessage.AppendLine($"{parameter.Name}:{GetStringValue(arg)}");
}
}
_logInfo?.Invoke(beforeMessage.ToString());
}
}
}
How to Use
var decoratedInstance = LoggingAdvice<IInstanceInteface>.Create(
instance,
s => Console.WriteLine("Info:" + s),
s => Console.WriteLine("Error:" + s),
o => o?.ToString());
Example
Let's assume that we are going to implement calculator which adds and subtracts integer numbers.
namespace AOP.Example
{
public interface ICalculator
{
int Add(int a, int b);
int Subtract(int a, int b);
}
}
namespace AOP.Example
{
public class Calculator : ICalculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Subtract(int a, int b)
{
return a - b;
}
}
}
It is easy. Each method has only one responsibility.
One day, some users start complaining that sometimes Add(2, 2)
returns 5
. You don’t understand what's going on and decide to add logging.
namespace AOP.Example
{
public class CalculatorWithoutAop: ICalculator
{
private readonly ILogger _logger;
public CalculatorWithoutAop(ILogger logger)
{
_logger = logger;
}
public int Add(int a, int b)
{
_logger.Log($"Adding {a} + {b}");
var result = a + b;
_logger.Log($"Result is {result}");
return result;
}
public int Subtract(int a, int b)
{
_logger.Log($"Subtracting {a} - {b}");
var result = a - b;
_logger.Log($"Result is {result}");
return result;
}
}
}
There are 3 problems with this solution:
Calculator
class coupled with logging. Loosely coupled (because ILogger
is an interface), but coupled. Every time you make changes in this interface, it affects Calculator
. - Code become more complex.
- It breaks Single Responsibility principle.
Add
function doesn't just add numbers. It logs input values, adds values and logs result. The same for Subtract
.
Code in this article allows you not to touch the Calculator
class at all.
You just need to change creation of the class.
namespace AOP.Example
{
public class CalculatorFactory
{
private readonly ILogger _logger;
public CalculatorFactory(ILogger logger)
{
_logger = logger;
}
public ICalculator CreateCalculator()
{
return LoggingAdvice <ICalculator >.Create(
new Calculator(),
s => _logger.Log("Info:" + s),
s => _logger.Log("Error:" + s),
o => o?.ToString());
}
}
}
Conclusion
This code works for my cases. If you have any examples when this code does not work or how this code could be improved – feel free to contact me in any way.
That's it — enjoy!