Click here to Skip to main content
13,352,280 members (72,567 online)
Click here to Skip to main content
Add your own
alternative version

Stats

13.9K views
1 download
17 bookmarked
Posted 6 Sep 2017
MIT

Aspect Oriented Programming in C# with RealProxy

, 12 Dec 2017
Rate this:
Please Sign up or sign in to vote.
Example of implementing logging using RealProxy class.

Source Code

Code could be found on GitHub or downloaded from here .

Introduction

Aspect Oriented Programming (AOP) is very powerful approach to avoid boilerplate code and archive 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 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 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 continue reading

This article is how to fix some problems in solution, provided by Bruno Sonnino (Example1). That article has great explanation 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 example how to do it in my article Aspect Oriented Programming in C# using DispatchProxy

Source Code

Code of this articles 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 impossible to create 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 LogAfrer 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)

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)
                        {
                            //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

var decoratedInstance = LoggingAdvice<IInstanceInteface>.Create(
                instance,
                s => Console.WriteLine("Info:" + s),
                s => Console.WriteLine("Error:" + s),
                o => o?.ToString());

Example

Let 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.

  1. Calculator class coupled with logging. Loosely coupled (because ILoger it 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 don't just add numbers. It logs input values, add values and logs result. The same for Subtract.

Code in this article allows you don't touch 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 – fill 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

Share

About the Author

Valerii Tereshchenko
Software Developer
United States United States
Software Developer with major experience in enterprise software development for different industries

You may also be interested in...

Comments and Discussions

 
QuestionSnippet Pin
Nelek12-Sep-17 21:25
protectorNelek12-Sep-17 21:25 
AnswerRe: Snippet Pin
Valerii Tereshchenko13-Sep-17 17:10
professionalValerii Tereshchenko13-Sep-17 17:10 
GeneralRe: Snippet Pin
Nelek13-Sep-17 18:21
protectorNelek13-Sep-17 18:21 
GeneralRe: Snippet Pin
Valerii Tereshchenko13-Sep-17 20:56
professionalValerii Tereshchenko13-Sep-17 20:56 
GeneralRe: Snippet Pin
Nelek13-Sep-17 22:12
protectorNelek13-Sep-17 22:12 
GeneralMy vote of 2 Pin
John B Oliver10-Sep-17 13:02
memberJohn B Oliver10-Sep-17 13:02 
GeneralRe: My vote of 2 Pin
Valerii Tereshchenko10-Sep-17 21:19
professionalValerii Tereshchenko10-Sep-17 21:19 
GeneralRe: My vote of 2 Pin
Valerii Tereshchenko11-Sep-17 8:35
professionalValerii Tereshchenko11-Sep-17 8: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.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.180111.1 | Last Updated 13 Dec 2017
Article Copyright 2017 by Valerii Tereshchenko
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid