Click here to Skip to main content
15,884,176 members
Articles / Programming Languages / C#

Aspect Oriented Programming in C# with RealProxy

Rate me:
Please Sign up or sign in to vote.
4.95/5 (15 votes)
12 Dec 2017MIT3 min read 32.8K   2   19   8
Example of implementing logging using RealProxy class

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:

  1. Extension method GetDescription was added to log Exception data (Extensions.cs).
  2. DynamicProxy class was renamed to LoggingAdvice.
  3. 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.
  4. LoggingAdvice receives actions to log function calls and errors, and function to serialize complex type values as parameters (LoggingAdvice.cs - lines 19-20).
  5. 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).
  6. Functions LogException, LogBefore and LogAfter were added to log corresponding data (LoggingAdvice.cs - lines 150-209).
  7. Try/catch blocks were added to handle situation when logInfo function throws an exception (LoggingAdvice.cs - lines 53-61, 99-107).
  8. 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)

C#
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)

C#
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)
                        {
                            //Do not stop method execution if exception
                            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)
                            {
                                //Do not stop method execution if exception
                                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)
            {
                // ignored
                //Method should return original 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

C#
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.

C#
namespace AOP.Example
{
    public interface ICalculator
    {
        int Add(int a, int b);
        int Subtract(int a, int b);
    }
}
C#
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.

C#
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:

  1. 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.
  2. Code become more complex.
  3. 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.

C#
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!

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer
United States United States
Full Stack Software Developer with major experience in enterprise software development for different industries.
Have a experience in wide range of technologies:
- JavaScript: React, Redux, TypeScript, Saga, Thunk, Cordova, Jest, Enzyme, Material Design, React-MD, Semantic UI, WebStorm
- .Net: C#, WPF, WCF, Windows Forms
- Java: Spring, Spring Boot, Spring Cloud, Spring Data, microservices, jUnit, Mochito, IntelliJ
- DB: Kafka, Oracle, SQL Server, PL/SQL

Comments and Discussions

 
QuestionSnippet Pin
Nelek12-Sep-17 20:25
protectorNelek12-Sep-17 20:25 
AnswerRe: Snippet Pin
Valerii Tereshchenko13-Sep-17 16:10
professionalValerii Tereshchenko13-Sep-17 16:10 
Thanks.
Could you please clarify what snippet do you mean?
GeneralRe: Snippet Pin
Nelek13-Sep-17 17:21
protectorNelek13-Sep-17 17:21 
GeneralRe: Snippet Pin
Valerii Tereshchenko13-Sep-17 19:56
professionalValerii Tereshchenko13-Sep-17 19:56 
GeneralRe: Snippet Pin
Nelek13-Sep-17 21:12
protectorNelek13-Sep-17 21:12 
GeneralMy vote of 2 Pin
John B Oliver10-Sep-17 12:02
John B Oliver10-Sep-17 12:02 
GeneralRe: My vote of 2 Pin
Valerii Tereshchenko10-Sep-17 20:19
professionalValerii Tereshchenko10-Sep-17 20:19 
GeneralRe: My vote of 2 Pin
Valerii Tereshchenko11-Sep-17 7:35
professionalValerii Tereshchenko11-Sep-17 7:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.