Click here to Skip to main content
13,191,877 members (55,012 online)
Click here to Skip to main content
Add your own
alternative version

Stats

20K views
174 downloads
19 bookmarked
Posted 17 May 2017
MIT

CrossCutterN: A Light Weight AOP Tool for .NET

, 26 Sep 2017
Rate this:
Please Sign up or sign in to vote.
A brief introduction to CrossCutterN tool remade for AOP programming in .NET technologies

Introduction

As AOP has become a well known and exercised concept in programming, developers have become more and more dependent on proper AOP tools.

In .NET programming, the best known AOP tool would be PostSharp, which allows injection of custom AOP code using custom attributes. As good things always don't come free, besides some troublesome manual certificate acquiring process, PostSharp express version also has limitations which makes developers who concern about their project scale hesitate to use it, and the price of ultimate version would become a major concern of quite some developers.

To have a free AOP tool for .NET, CrossCutterN is implemented. It provides AOP functionalities, and works a bit differently than most existing AOP tools.

The advantages of CrossCutterN tool include:

  • Free: CrossCutterN is open source and free under MIT license.
  • Light Weight: Instead of adding compile time dependency to projects, CrossCutterN injects AOP code at post build stage. This approach allows AOP code injection into assemblies whose source code is not available, and decouples project code from AOP code as much as possible.
  • Cross Platform: CrossCutterN works in both .net framework and .net core environments.
  • Out of the box aspect switching support: CrossCutterN allows users to switch on/off AOP code that is injected to methods/properties during project run-time at multiple granularity levels.
  • Designed for optimized performance: CrossCutterN uses IL weaving technology to make the injected AOP code work as efficient as directly coded in target projects, and the implementation is optimized to avoid unnecessary local variable initializations and method calls.

Background

This article assumes that readers are familiar with the concept of Aspect Oriented Programming, and perhaps have some previous experience using AOP frameworks like PostSharp, Spring AOP and so on.

Examples

To perform weave AOP code into an assembly, CrossCutterN requires the following process:

  • Prepare the AOP code module following CrossCutterN convention. The AOP code content is fully customizable by developers.
  • Prepare the configuration file for the AOP module. 
  • Prepare the configuration file for the target module, which requires the AOP code to be injected to.
  • Execute console application tool to weave the original assembly together with the AOP code information into a new assembly.

Then it's done.

Let's take a very simple C# method for example:

namespace CrossCutterN.Sample.Target
{
    using System;

    internal class Target
    {
        public static int Add(int x, int y)
        {
            Console.Out.WriteLine("Add starting");
            var z = x + y;
            Console.Out.WriteLine("Add ending");
            return z;
        }
    }
}

When executed, the output to console would be:

Now what if I want to inject some AOP code to the Add method? For example, to log the function call and all it's parameter values upon entering the method call, and the return value before the method returns? CrossCutterN currently provides 2 ways to do so.

Before executing console application tool in each of the the examples, please make sure to rebuild the sample target project, to get a fresh target assembly for CrossCutterN to perform IL weaving to.

Using Name of Methods to Find Target Methods to Be Injected

By following the steps listed:

Implement AOP Module

Implement some utility properties and methods first:

namespace CrossCutterN.Sample.Advice
{
    using System;
    using System.Text;
    using CrossCutterN.Base.Metadata;

    internal sealed class Utility
    {
        internal static string CurrentTime => 
        DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss.fff tt");

        internal static string GetMethodInfo(IExecution execution)
        {
            var strb = new StringBuilder(execution.Name);
            strb.Append("(");
            if (execution.Parameters.Count > 0)
            {
                foreach (var parameter in execution.Parameters)
                {
                    strb.Append(parameter.Name).Append("=").
                    Append(parameter.Value).Append(",");
                }

                strb.Remove(strb.Length - 1, 1);
            }

            strb.Append(")");
            return strb.ToString();
        }

        internal static string GetReturnInfo(IReturn rReturn) 
            => rReturn.HasReturn ? 
            $"returns {rReturn.Value}" : "no return";
    }
}

Please note that IExecution and IReturn interfaces are provided by CrossCutterN.Base.dll assembly. For CrossCutterN tool to work, developers must follow its conventions and provided interfaces.

Now implement methods to output logs upon entry and before return of a method:

namespace CrossCutterN.Sample.Advice
{
    using System;
    using CrossCutterN.Base.Metadata;

    public static class AdviceByNameExpression
    {
        public static void OnEntry(IExecution execution)
            => Console.Out.WriteLine($"{Utility.CurrentTime} 
               Injected by method name on entry: {Utility.GetMethodInfo(execution)}");

        public static void OnExit(IReturn rReturn)
            => Console.Out.WriteLine($"{Utility.CurrentTime} 
               Injected by method name on exit: {Utility.GetReturnInfo(rReturn)}");
    }
}

Just for easy demonstration purposes, we directly output the log to console. AOP module implementation is done.

Prepare AOP Module Configuration

Add a json file to the AOP module project, make sure it's copied together with the assembly. Name the json file as "adviceByNameExpression.json".

{
  "CrossCutterN": {
    "sample": {
      "AssemblyPath": "CrossCutterN.Sample.Advice.dll",
      "Advices": {
        "CrossCutterN.Sample.Advice.AdviceByNameExpression": {
          "testEntry": {
            "MethodName": "OnEntry",
            "Parameters": [ "Execution" ]
          },
          "testExit": {
            "MethodName": "OnExit",
            "Parameters": [ "Return" ]
          }
        }
      }
    }
  }
}

Meaning of the configuration file is like the following:

  • I have an assembly which contains AOP code to be injected, the key used to refer to this assembly is "sample".
  • Path of this assembly is "CrossCutterN.Sample.Advice.dll"; it's not an absolute path, so the assembly path is relevant to the path of configuration file, in this case, it's in the same folder with the configuration file.
  • It has the following AOP methods (Namely "Advices") to be injected in class "CrossCutterN.Sample.Advice.AdviceByNameExpression".
  • One method named "OnEntry", with one parameter type marked as "Execution" (which is the "IExecution" type in C# code). This method will be referred to as "testEntry" in target assembly configuration.
  • One method named "OnExit", with one parameter type marked as "Return" (which is the "IReturn" type in C# code). This method will be referred to as "testExit" in target assembly configuration.

Prepare Target Module Configuration

Add a json file to the target module project, and make sure it's copied together with the assembly to be injected with AOP method call. Name the json file as "nameExpressionTarget.json".

{
  "CrossCutterN": {
    "DefaultAdviceAssemblyKey": "sample",
    "AspectBuilders": {
      "aspectByMethodName": {
        "AspectBuilderKey": "CrossCutterN.Aspect.Builder.NameExpressionAspectBuilder",
        "Includes": [ "CrossCutterN.Sample.Target.Target.Ad*" ],
        "Advices": {
          "Entry": { "MethodKey": "testEntry" },
          "Exit": { "MethodKey": "testExit" }
        }
      }
    },
    "Targets": {
      "CrossCutterN.Sample.Target.exe": { "Output": "CrossCutterN.Sample.Target.exe" }
    }
  }
}

Meaning of the configuration file is like the following:

  • I have a default AOP code module which can be referred to as "sample".
  • The following AspectBuilders are defined to help me to do the injection.
  • One aspect builder can be referred to as "CrossCutterN.Aspect.Builder.NameExpressionAspectBuilder". This reference is implemented and provided by CrossCutterN tool which will find methods to inject AOP code into by checking the methods' names.
  • This aspect builder will inject all methods whose full name is like "CrossCutterN.Sample.Target.Target.Ad*"
  • This aspect builder will inject a method call to a method which can be referred to as "testEntry" upon "Entry" of the target method call.
  • This aspect builder will inject a method call to a method which can be referred to as "testExit" before "Exit" of the target method call.
  • AOP code added by this aspect builder can be referred to as "aspectByMethodName" in configuration for ordering and C# code to switch on/off.
  • One assembly is in the Targets assemblies to be injected. The assembly is "CrossCutterN.Sample.Target.exe". It's not an absolute path, so the path is relevant to the configuration file, in this case it's in the same folder of the configuration file. The weaved assembly will be saved as "CrossCutterN.Sample.Target.exe", path relevant to the configuration file, in this case also the same folder of the configuration file. The file name of the output assembly is exactly the same with the target assembly, so the original assembly will be overwritten by the weaved one. Please note that exe is for .net framework sample. It should be CrossCutterN.Sample.Target.dll for .net core sample.

Execute Console Application Tool

Build the AOP and target assemblies with Release configuration, navigate to CrossCutterN.Sample\ folder, execute:

CrossCutterN.Console\CrossCutterN.Console.exe 
/d:CrossCutterN.Sample.Advice\bin\Release\adviceByNameExpression.json 
/t:CrossCutterN.Sample.Target\bin\Release\nameExpressionTarget.json

Meaning of the command is:

Execute console application of CrossCutterN, using CrossCutterN.Sample.Advice\bin\Release\adviceByNameExpression.json file as AOP code assembly configuration (/d: doesn't mean D: drive, it means this configuration is for AOP code assembly), and using CrossCutterN.Sample.Target\bin\Release\nameExpressionTarget.json file as target assembly configuration (/t: means target assembly configuration).

For net core example, the command is like the following:

dotnet CrossCutterN.Console\CrossCutterN.Console.dll /d:CrossCutterN.Sample.Advice\bin\Release\netstandard2.0\adviceByNameExpression.json /t:CrossCutterN.Sample.Target\bin\Release\netcoreapp2.0\nameExpressionTarget.json

If the execution is successful, the original CrossCutterN.Sample.Target.exe file is replaced with newly generated one. Execute the new assembly, something like the following output is expected:

The result suggests that the AOP method calls have been successfully injected.

To keep the original target assembly for comparation or other purposes, just change the "Output" configuration in "Targets" section in target assembly configuration to other values than the assembly name of the original, in this case, maybe "CrossCutterN.Sample.Target.Weaved.exe" or something else. Please note that though CrossCutterN outputs assembly and pdb files, it doesn't take care of configuration file of the assembly. User will need to copy over the original exe.config file and rename it to match the new exe assembly name to execute the exe assembly with the new name if they decide not to overwrite original assembly.

Using Custom Attributes to Mark Target Methods to Be Injected

CrossCutterN tool also provides a way to mark target methods to be injected using customized attributes. And the process is similar to the previous:

Implement AOP Module

namespace CrossCutterN.Sample.Advice
{
    using System;
    using CrossCutterN.Base.Concern;
    using CrossCutterN.Base.Metadata;

    public static class AdviceByAttribute
    {
        public static void OnEntry(IExecution execution) 
            => Console.Out.WriteLine($"{Utility.CurrentTime} 
               Injected by attribute on entry: {Utility.GetMethodInfo(execution)}");

        public static void OnExit(IReturn rReturn) 
            => Console.Out.WriteLine($"{Utility.CurrentTime} 
               Injected by attribute on exit: {Utility.GetReturnInfo(rReturn)}");
    }

    public sealed class SampleConcernMethodAttribute : ConcernMethodAttribute
    {
    }
}

Note that this time, there is an attribute "SampleConcernMethodAttribute" declared for marking target methods. The attribute should be added to target "Add" method

namespace CrossCutterN.Sample.Target
{
    using System;

    internal class Target
    {
        [CrossCutterN.Sample.Advice.SampleConcernMethod]
        public static int Add(int x, int y)
        {
            Console.Out.WriteLine("Add starting");
            var z = x + y;
            Console.Out.WriteLine("Add ending");
            return z;
        }
    }
}

Prepare AOP Module Configuration

{
  "CrossCutterN": {
    "sample": {
      "AssemblyPath": "CrossCutterN.Sample.Advice.dll",
      "Attributes": { "method": "CrossCutterN.Sample.Advice.SampleConcernMethodAttribute" },
      "Advices": {
        "CrossCutterN.Sample.Advice.AdviceByAttribute": {
          "entry1": {
            "MethodName": "OnEntry",
            "Parameters": [ "Execution" ]
          },
          "exit1": {
            "MethodName": "OnExit",
            "Parameters": [ "Return" ]
          }
        }
      }
    }
  }
}

In Attributes section, an attribute of type "CrossCutterN.Sample.Advice.SampleConcernMethodAttribute" is defined to mark target methods. It can be referred to as "method" in target configurations. The configuration file name is adviceByAttribute.json.

Prepare Target Module Configuration

{
  "CrossCutterN": {
    "DefaultAdviceAssemblyKey": "sample",
    "AspectBuilders": {
      "aspectByAttribute": {
        "AspectBuilderKey": "CrossCutterN.Aspect.Builder.ConcernAttributeAspectBuilder",
        "ConcernMethodAttributeType": { "TypeKey": "method" },
        "Advices": {
          "Entry": { "MethodKey": "entry1" },
          "Exit": { "MethodKey": "exit1" }
        }
        //,"IsSwitchedOn": false
      }
    },
    "Targets": {
      "CrossCutterN.Sample.Target.exe": { "Output": "CrossCutterN.Sample.Target.exe" }
    }
  }
}

Here, AspectBuilderKey is changed to "CrossCutterN.Aspect.Builder.ConcernAttributeAspectBuilder", which is also implemented and provided by CrossCutterN tool, it will find methods marked by checking predefined attributes. The configuration file is attributeTarget.json.

Still, exe is for .net framework sample. For .net core sample, it should be CrossCutterN.Sample.Target.dll.

Execute Console Application Tool

Build the AOP and target assemblies with Release configuration, navigate to CrossCutterN.Sample\ folder, execute:

CrossCutterN.Console\CrossCutterN.Console.exe 
/d:CrossCutterN.Sample.Advice\bin\Release\adviceByAttribute.json 
/t:CrossCutterN.Sample.Target\bin\Release\attributeTarget.json

For net core example, the command is like the following:

dotnet CrossCutterN.Console\CrossCutterN.Console.dll /d:CrossCutterN.Sample.Advice\bin\Release\netstandard2.0\adviceByAttribute.json /t:CrossCutterN.Sample.Target\bin\Release\netcoreapp2.0\attributeTarget.json

The expected result is similar with the previous example when executing the weaved assembly:

Perform AOP Code Injection Using Multiple Aspect Builders

Surely to inject multiple AOP method calls, multiple aspect builders can be declared in single AOP assemlby configuration files and single target assembly configuration files. Please check "advice.json" and "target.json" configuration files in the sample project. Detailed processes are ignored to reduce text redundancy.

One thing to mention though, for multiple aspect builders to work together, AOP method call order must be specified, like the "Order" section in "target.json":

"Order": {
  "Entry": [
    "aspectByAttribute",
    "aspectByMethodName"
  ],
  "Exit": [
    "aspectByMethodName",
    "aspectByAttribute"
  ]
}

It means when applying multiple aspect builders to one target method, upon entry, method call injected by aspect builder referred to as "aspectByAttribute" is applied first, and method call injected by aspect builder referred to as aspectByMethodName will be applied after the former. And before exiting the target method call, the injected AOP method call ordering is reversed according to the configuration. Please note that "Order" section can be ignored for single aspect builder in target configuration files, but is mandatory for multiple aspect builders in target configuration files.

Runtime AOP Methods Calls Switching

In case sometimes users intend to temporarily disable some of the AOP methods calls and enable them on later, CrossCutterN provides a way to switch on and off injected AOP methods calls during program run time.

Note the "//,"IsSwitchedOn": false" configuration item in the samples, it is the configuration entry for such switching:

  • If not specified, the AOP method calls injected by the aspect builder will not be switchable, which means they always get executed when the target methods are triggered.
  • If set to false, the AOP method calss injected by the aspect builder will be switchable, but by default not executed, unless switched on at runtime. They can be switched on and off during the program run time.
  • If set to true, the AOP method calls injected by the aspect builder will be switchable, and by default executed, unless switched off at run time. They can be switched off and on during the program run time.

So we uncomment this configuration entry, save the configuration file, and go through the "Using Custom Attributes to Mark Target Methods to Be Injected" example again, the output of the weaved assembly will not include the AOP output:

In the program, execute the following statement before calling the Add method:

CrossCutterN.Base.Switch.SwitchFacade.Controller.SwitchOn("aspectByAttribute");

Note that "aspectByAttribute" is the key we used to refer to the aspect builder in target configuration. Go through with the "Using Custom Attributes to Mark Target Methods to Be Injected" example again, the output of the weaved assembly will include the AOP output again.

More Details

The above is just a simple demonstration of CrossCutterN tool.

For AOP code injection, it can inject methods and properties at points of entry, exception and exit with various of configuration options to easily include/exclude injection target by method/property/constructor, accessibility and static/instance.

For AOP code switching operation, it allows various granularities, like switching all AOP code injected by an aspect builder, all AOP code injected into a class, a method, and so on.

In case under some circumstances, some objects must be passed among advices at entry, exception or exit, injection advices declared with the parameter type CrossCutterN.Base.Metadata.IExecutionContext are designed for this purpose. This interface allows advices to store an object in it with an object key and retrieve, update or remove it in advices called later.

CrossCutterN is much more flexible, configurable and extendable than the introduction above. Interested readers, please visit GitHub to download the source code and find out more details about it.

Considering this tool is still evolving, its documentation is most likely not perfect, plus though test cases are written and passed, there might be defects. If there are any issues found during usage of the tool, or if there are any questions, suggestions and requirements, please don't hesitate to leave a comment to this article, submit an issue to the project, or send and email to keeper013@gmail.com.

Attentions

  • Please don't use this tool to inject the already injected assemblies. Take the assembly CrossCutterN.SampleTarget.exe mentioned above for example, if this assembly is injected twice using CrossCutterN tool, there is no guarantee that it still works perfectly.
  • There is no guarantee that CrossCutterN works with any other AOP tools.
  • There is no point to do this AOP code injection process using multi-thread style, for developers tend to develop their own tools based on CrossCutterN source code, please be reminded that the AOP code injection part isn't designed for multi-threading at all (why would someone want 2 threads to inject one assembly).
  • Multi-threading is considered and implemented for AOP code switching feature.
  • There is no guarantee that CrossCutterN works with obfuscation tools.

Points of Interest

  • Custom MsBuild Task: Having an msbuild task certainly helps the tool to be integrated into projects much easier. The situation is that currently msbuild tool has an assembly binding redirection issue that custom msbuild tasks won't work with certain assembly binding redirection, and unfortunately CrossCutterN is one of them (mostly for the json configuration feature). Either msbuild solves this issue or CrossCutterN tries to fix the issue, otherwise can this feature be provided.
  • DotNetCore and DotNetStandard: Due to the reason that Mono.Cecil doesn't support strong name for netstandard yet, the strong name feature doesn't work for netstandard branch. Besides, since support to generic methods is not complete in Mono.Cecil, some metadata builder interface can't inherit from IBuilder interface.
  • CI Support: I haven't found any free CI environment that supports building multiple targets including net461 and netcore2.0 yet. Besides, as appveyor stated, it doesn't support dotnet test yet, so currently CI is not workin for netstandard branch of this project (which builds netcore sample in this article), but that branch is actually tested locally to be OK (all NUnit test cases pass).
  • Weaver Interface Design: Currently, CrossCutterN.Weaver.Weaver.IWeaver interface requires file names instead of streams for input and output assemblies. This is because current Mono.Cecil support for outputing weaved assemblies with pdb files using stream completely, which leads to the current design. This can be approved after Mono.Cecil is updated.

History

  • Initial version
  • Aspect switching feature added
  • Tool completely remade

License

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

Share

About the Author

David_Cui
Singapore Singapore
No Biography provided

You may also be interested in...

Pro

Comments and Discussions

 
QuestionExtending IExecution and IReturn Pin
dink9918hrs 5mins ago
memberdink9918hrs 5mins ago 
AnswerRe: Extending IExecution and IReturn Pin
David_Cui5hrs 47mins ago
memberDavid_Cui5hrs 47mins ago 
QuestionAbout the link Pin
Nelek31-May-17 18:50
protectorNelek31-May-17 18:50 
AnswerRe: About the link Pin
David_Cui31-May-17 20:10
memberDavid_Cui31-May-17 20:10 
GeneralRe: About the link Pin
Nelek31-May-17 20:11
protectorNelek31-May-17 20:11 
Questiona few questions Pin
BillWoodruff18-May-17 20:23
mvpBillWoodruff18-May-17 20:23 
AnswerRe: a few questions Pin
CuiZiqiang19-May-17 5:50
memberCuiZiqiang19-May-17 5:50 
GeneralRe: a few questions Pin
BillWoodruff21-May-17 13:18
mvpBillWoodruff21-May-17 13:18 
GeneralRe: a few questions Pin
CuiZiqiang21-May-17 14:45
memberCuiZiqiang21-May-17 14:45 

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
Web01 | 2.8.171017.2 | Last Updated 26 Sep 2017
Article Copyright 2017 by David_Cui
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid