Introduction
As aspect-oriented programming (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, which 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 AOP, and perhaps have some previous experience using AOP frameworks like PostSharp, Spring AOP and so on.
Examples
To 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 its 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 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.1\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 comparison 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 the 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" }
}
}
},
"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.1\attributeTarget.json
The expected result is similar to 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 assembly 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 class 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 an 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 working 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 outputting 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
- Binaries updated due to bug fix
- Binaries updated due to hash code support addition for
IExecution