In a previous article, Aspect Oriented Programming (AOP) in C# with SOLID, I talked about AOP, and how it helps with reducing code duplication and code tangling that is encountered while addressing cross-cutting concerns. I also mentioned different approaches for doing AOP in C#. These approaches are:
1. AOP by using the IQueryHandler and ICommandHandler abstractions. I will call this approach the Command Query Separation approach or simply the CQS approach.
2. AOP via dynamic proxies
3. AOP via compile-time weaving
4. AOP via T4
Note: Please refer to the mentioned article for more details about AOP and these approaches. I recommend that the reader reads that article before reading this once - at least read it up to the “The First Problem: Specificity” section. This will enable me to assume that you already understand the CQS approach to AOP.
In a nutshell, we can model all behavior in the system as classes that implement either the IQueryHandler<TQuery, TResult> generic interface or the ICommandHandler<TCommand> generic interface. Then, to create an aspect, we create a generic decorator for each one of these two interfaces.
Are you keeping up with new developer technologies? Advance your IT career and learn best practices with our Free Developer magazines covering C#, Design Patterns, .NET Core, MVC, Azure, Angular, React, and more. Subscribe to the DotNetCurry (DNC) Magazine for FREE and download all previous, current and upcoming editions.
To apply the aspect to an object, we simply decorate it using the aspect decorator.
For a complete example on how to do this, please refer to the mentioned article Aspect Oriented Programming (AOP) in C# with SOLID.
There are two problems with the CQS approach:
1. For each aspect, we need to create two decorators instead of one. These two decorators are almost identical. This is not a big problem though.
2. Verbosity - For example, in the case of a command handler, for each behavior class, we need to create a data object to represent a command. In the case of a query handler, we need two data objects to represent the query and the query result.
Additionally, when a behavior class has a dependency on another handler, it looks like this:
private readonly IQueryHandler<ParseQuery, ParseResult> parser;
..instead of simply:
private readonly IParser parser;
The same thing is also true for when we specify the implemented interface when we define a behavior class. That is, we specify the implemented interface like this:
public class CustomerReportGenerator
: IQueryHandler<CustomerReportQuery, CustomerReportResult>
{
}
..instead of simply doing this:
public class CustomerReportGenerator : ICustomerReportGenerator
{
}
When invoking a dependency, it would be much simpler to do the following:
var result = parser.Parse(document);
..instead of doing the following:
var result = parser.Handle(new ParseQuery(document));
Still, the CQS approach to AOP has many advantages:
1. It requires no special tools.
2. Writing an aspect is done simply by writing two decorators. (compare this to writing the aspect in T4 for example)
3. Applying an aspect to an object is done simply by decorating the object.
4. It enables the aspect to have access to input/output data in a strongly typed way. I will explain this point in detail later in this article after I speak about AOP via functions.
In the next sections, I will talk about an approach to AOP that has most of the advantages of the CQS approach, but that attempts to solve its issues.
AOP via functions
A function in mathematics is a relation between a set of inputs and a set of outputs.
In programming, a function is a unit of code that takes some input and produces some output. In .NET, we can use the Func delegate to represent a function.
For example, Func<Customer, Discount> represents a function that takes a Customer data object as input and returns a Discount data object as output. We can also create a special IFunction<TInput,TOutput> interface to represent a function like this:
public interface IFunction<in TInput, out TOutput>
{
TOutput Invoke(TInput input);
}
This generic interface has a single method named Invoke that takes a single parameter of type TInput and returns a value of type TOutput.
If all our behavior classes implement this interface (similar to the CQS approach), then we can create an aspect by simply creating a single decorator for IFunction<TInput,TOutput>. This would solve the first disadvantage I mentioned above about the CQS approach. Still, this approach would be as verbose (the second disadvantage) as the CQS approach, if not more.
Instead I propose the following:
- Create aspects as decorators for IFunction<TInput,TOutput>
- To apply the aspect to a behavior object that implements some interface, say IParser, we first convert IParser to IFunction, we then decorate the resulting function, then we convert the result back to IParser.
To convert from and to IFunction, we can create two adapters. One adapter would implement IFunction and take a dependency of type IParser, and the second adapter would implement IParser and take a dependency of type IFunction.
Figure 1: Applying an aspect to an object of type IParser
Figure 1 shows how an original IParser object was first adapted to IFunction, then the adapter was decorated by the aspect decorator, which was then adapted back to IParser.
The code to apply the aspect would look like something like this:
IParser originalParser = ...
IParse parserWithAspectApplied =
new FunctionToParserAdapter(
new SomeAspect( //The aspect is applied here
new ParserToFunctionAdapter(originalParser)));
SomeAspect represents an aspect. It is a generic decorator class that implements the generic IFunction interface, i.e., IFunction<TInput,TOutput>. It also has a dependency of type IFunction<TInput,TOutput>. I will show you an example of aspects later in this article.
While this approach may fix the issues with the CQS approach, there are still some issues that we need to address:
- How to convert any interface to IFunction? For example, while some methods return a specific type that we can use for TOutput, others return void. What would be the type of TOutput in this case? Also, methods can have multiple parameters, what would be the type of TInput of this case? What about interfaces with multiple methods?
- Assuming we know how to map between any interface and IFunction, creating the adapters is a time-consuming process and is hard in many cases. Also, such adapters are code that we need to maintain. Any plausible approach to AOP must allow developers to apply aspects in a practical way.
- The code for applying the aspect is also verbose.
- What about performance? How much performance are we sacrificing by having these adapters?
I will address these issues next.
Mapping between any method and IFunction
Let’s assume for a moment that all interfaces have a single method. We will deal with multi-method interfaces later.
To create the two adapters between some single-method interface and IFunction, we need to consider the following:
1. The method either returns a value of some type. Or its return type is void.
2. The method has either no parameters, a single parameter, or multiple parameters.
For methods that return a specific type, TOutput would simply be that type. For methods that return void, we can use a special type called the Unit type as TOutput.
Unit is simply a type that contains no data. It’s only purpose is to represent the output of functions that actually have no output. You can read more about this type here: https://en.wikipedia.org/wiki/Unit_type
For methods that take no parameters, we can use Unit again for TInput. If the method takes a single parameter, we can use the type of this parameter as TInput. If the method takes more than one parameter, we can use a tuple as TInput. Value tuples introduced in C#7 are very convenient to use in this case.
Let’s look at some examples:
public interface IEmailSender
{
void SendEmail(
EmailAddress from, EmailAddress to, string subject, EmailBody body);
}
This SendEmail method has four parameters but returns nothing. The IEmailSender interface would be adapted to this version of IFunction:
IFunction<(EmailAddress from, EmailAddress to, string subject, EmailBody body), Unit>
TInput in this case is a C# 7 tuple consisting of four items representing the original parameters, and TOutput is Unit because the original method returned void.
Here is how the adapter from IEmailSender to IFunction looks like:
public class EmailSenderToFunctionAdapter
: IFunction<(EmailAddress from, EmailAddress to, string subject, EmailBody body), Unit>
{
private readonly IEmailSender instance;
public EmailSenderToFunctionAdapter(IEmailSender instance)
{
this.instance = instance;
}
public Unit Invoke(
(EmailAddress from, EmailAddress to, string subject, EmailBody body) input)
{
this.instance.SendEmail(input.from, input.to, input.subject, input.body);
return Unit.Default;
}
}
This adapter takes IEmailSender as a dependency in the constructor and implements IFunction. When the Invoke method is called, the IEmailSender dependency is invoked. Then to satisfy the signature of method, we return Unit.Default. Unit.Default is a static property of the Unit type that returns the only instance of the Unit type.
Here is how the Unit type looks like:
public class Unit
{
private Unit()
{
}
public static Unit Default { get; } = new Unit();
}
To adapt back from IFunction to IEmailSender, we create the following adapter:
public class FunctionToEmailSenderAdapter : IEmailSender
{
private readonly IFunction<(EmailAddress from, EmailAddress to, string subject, EmailBody body), Unit> function;
public FunctionToEmailSenderAdapter(
IFunction<(EmailAddress from, EmailAddress to, string subject, EmailBody body), Unit> function)
{
this.function = function;
}
public void SendEmail(EmailAddress from, EmailAddress to, string subject, EmailBody body)
{
function.Invoke((from, to, subject, body));
}
}
By the way, there is an interesting article series by Mark Seemann called Software Design Isomorphisms where he talks about how some concepts in programming can be expressed in multiple ways.
In particular, the Unit isomorphisms article talks about using the Unit type as a different way to represent the return type for methods that return nothing. And the Argument list isomorphisms article talks about different ways to represent an argument list.
You can find the article series here: http://blog.ploeh.dk/2018/01/08/software-design-isomorphisms/
The InterfaceMappingHelper Visual Studio extension
Creating the adapters manually is a hard process.
You need first to figure out the correct values for TInput and TOutput, and then you need to write two adapters correctly. This gets even harder if the original interface itself is generic. It could also have type parameter constraints. What if the developer decided to add a new parameter to the interface method? He/she needs to update the adapters accordingly.
To make it easy to generate and maintain these adapters, I created a Visual Studio extension called InterfaceMappingHelper. You can download this extension from inside Visual Studio from the Extensions and Updates tool found in the Tools menu.
Once installed, you can hover over any interface, click on the Rolsyn based refactorings bulb icon, and then click on the “Create interface-mapping extension methods class”.
Figure 2: The InterfaceMappingHelper extension
This will create a static class right after the interface. This static class will have five members:
1. A static extension method called Map. I will talk about this method in the next section.
2. A private (nested) class representing the adapter from the original interface to its IFunction equivalent. This class is called ToFunctionClass.
3. Another private class representing the adapter from IFunction to the original interface. This class is called FromFunctionClass.
4. A static extension method called ToFunction. This method extends the original interface to allow you to adapt to IFunction by simply invoking .ToFunction().
5. A static extension method that named ToXXXXXX where XXXXXX is the name of the original interface with the potential prefix “I” removed from the interface name. In the example above, the method name is ToEmailSender. This method is an extension method of IFunction. It makes it easier to adapt back from IFunction to the original interface.
Next, I will talk about how the Map method can be used to apply an aspect easily.
The Map method
The Map method generated by the extension has the following signature:
public static IOriginalInterface Map(
this IOriginalInterface instance,
Func<IFunction, IFunction> decorationFunction)
IOriginalInterface is the name of the original interface. I use IFunction here as an abbreviation for IFunction<TInput, TOutput> for whatever TInput and TOutput that match the method signature in the interface. Here is how the Map method generated for the IEmailSender interface looks like:
public static IEmailSender Map(
this IEmailSender instance,
Func<
IFunction<(EmailAddress from, EmailAddress to, string subject, EmailBody body), Unit>,
IFunction<(EmailAddress from, EmailAddress to, string subject, EmailBody body), Unit>
>
decorationFunction)
{
return new FunctionToEmailSenderAdapter(
decorationFunction(
new EmailSenderToFunctionAdapter(instance)));
}
The Map method is an extension method for the original interface, and its return type is also the original interface. It takes a Func from IFunction to IFunction.
So basically, this method allows you transform an object implementing some interface by telling it how to transform a function that somehow looks like it.
For example, we can use the Map method to apply an aspect (a class that decorates the generic IFunction interface) like this:
IParser originalParser = ...
IParser parserWithAspectApplied =
originalParser.Map(x => x.ApplySomeAspect());
where ApplySomeAspect is an extension method that decorates IFunction.
Let’s see a real example.
I have created a repository on Github called AOPViaFunctionsExamples, you can find it here.
Let’s first look at the RetryAspect class. The retry aspect can be applied to behavior units to make them retry the operation in case of failure.
The RetryAspect static class contains two members; a nested private Decorator class, and static extension method called ApplyRetryAspect.
Take a look at the nested Decorator class. This generic class decorates IFunction<TInput, TOutput>. It takes three parameters in the constructor; the IFunction<TInput, TOutput> instance to decorate, the number of times to try/retry the operation, and a Timespan representing the time that we should wait before we retry the operation.
The ApplyRetryAspect static method is an extension method for IFunction<TInput, TOutput>. This method is created for convenience as it will make applying the aspect much easier.
Next, take a look at the IEmailSender and IReportGenerator interfaces. I have already used the InterfaceMappingHelper extension to generate the mapping static class for each of these interfaces. You can see each one of these mapping classes immediately after each of the corresponding interface.
I have created very simple implementations of these interfaces. Take a look at the EmailSender, and the ReportGenerator classes.
Next, go to the Main method in the Program class to see how the aspect is applied. Look for example how I create an instance of the EmailSender class, and then call the Map method on it as an extension method. Notice how the Map method allows me to call the ApplyRetryAspect extension method on the lambda parameter which is of type IFunction.
I hope that you see how easy it is to apply an aspect using this approach.
Multi-method interfaces
So far, we have been dealing with interfaces that have a single method. Although when we apply the SOLID principles (the SRP and ISP principles in particular), most interfaces will have a single method, still some interfaces will have more than one method. Any practical approach to AOP should support multi-method interfaces.
To apply aspects to multi-method interfaces, we first adapt the original interface to multiple functions. We then apply the aspects to the individual functions. Then, we adapt these functions back to the original interface.
The InterfaceMappingHelper extension supports multi-method interfaces. It will generate all the necessary adapters and the Map method. Consider this interface that contains two methods:
public interface ITwoMethodsInterface
{
void Method1();
void Method2();
}
Here is the signature of the generated Map method:
public static ITwoMethodsInterface Map(
this ITwoMethodsInterface instance,
Func<
(IFunction<Unit, Unit> method1, IFunction<Unit, Unit> method2),
(IFunction<Unit, Unit> method1, IFunction<Unit, Unit> method2)
> decorationFunction)
Notice how the Func parameter of the Map method maps between a tuple containing two functions (method1 and method2) to a tuple containing two functions. This allows the caller to apply any aspects to each of the methods. For example, you might decide that you want to apply some aspect to the first method but leave the second method as is. Consider this example:
ITwoMethodsInterface instance =
new TwoMethodsClass()
.Map(methods => (
methods.method1.ApplyRetryAspect(
numberOfTimesToRetry: 4,
waitTimeBeforeRetries: TimeSpan.FromSeconds(5)),
methods.method2));
In the above example, we call the map method on an instance of a TwoMethodsClass class (which implements the ITwoMethodsInterface interface). Notice how we apply the retry aspect on method1 but leave method2 intact.
Performance
In this section, I will examine the performance implications of the AOP via functions approach.
I will compare three approaches to AOP:
- The CQS approach
- AOP by using DynamicProxy
- The AOP via Functions approach
The aspect that we will apply will be a do-nothing aspect. That is, it simply calls the original object immediately.
The cost of the aspect should be measured relatively to the cost of the original operation. Here are the operations that I will consider:
- An operation that immediately returns some value.
- An operation that does some relatively complex mathematical operations. In specific, the operation calculates
- An operation that uses entity framework to read data from a single row table.
Note: ns means nanosecond
Consider the following:
- For any non-trivial operation, e.g. the operation that did some mathematical calculations and the database access operation, any AOP approach does not add any significant overhead relatively and its performance cost can be ignored. I think that the error in measurement is actually more than the overhead introduced. I think that this is the most important result.
- For very simply operations, e.g. the operation that simply returns a value immediately, AOP via functions preforms well compared to other approaches.
- AOP via functions approach seems to be as fast as the CQS approach or even faster. This was against my expectations before measurements. The reason might be that when invoking a query handler in the CQS approach, we have to create instances of the query and query result objects (this comparison is only relevant for the very simply operation).
More examples
In this section I will show two examples: an authorization aspect example, and a logging aspect example.
Authorization aspect example
Let’s consider an authorization aspect example. Let’s say we are developing a public API for some file system service that allows clients to manage files in some remote files system. Consider the following interface:
public interface IFileSystemService
{
FileContent GetFileContent(FileId fileId);
FolderContents GetFolderContents(FolderId folderId);
void CopyFileToFolder(FileId fileId, FolderId folderId);
}
This interface has three methods, one to get the contents of a file, one to get the contents of a folder, and one to copy some file into some folder.
Consider the following authorization rules:
- GetFileContent requires that the caller has read access to the file.
- GetFolderContents requires that the caller has read access to the folder.
- CopyFileToFolder requires that the caller has both read access to the file and write access to the folder.
Let’s look at the authorization aspect. Take a look at the AuthorizationAspect class. The ApplyAuthorizationAspect extension method has three parameters:
1. The decorated parameter representing the function we are trying to decorate.
2. The authorizer parameter is an instance of the IResourceAuthorize<TResourceId> interface where TResourceId is a type parameter representing the type of the resource id. For example, this could be FileId or FolderId. The aspect would invoke such authorizer to determine if the current user has access to the specified resource.
3. The resourceIdSelector parameter is of type Func<TInput, TResourceId> and allows the caller to tell the aspect how to get the resource id from the input. For example, this could be used to select the FolderId parameter.
Take a look at the Main method in Program.cs. Take a look at the statement where I apply the authorization aspect on an instance of the FileSystemService class. See how I call the Map method to apply the authorization aspect on each of the three methods.
For the GetFileContent method, I apply the aspect specifying the input itself as the resource id. Remember that for single-parameter methods, TInput is the type of a single parameter. For GetFolderContents, I do a similar thing.
What I do for CopyFileToFolder is more special, though.
Notice how I apply the aspect twice, once to make sure that the caller has read access to the file, and another time to make sure the caller has write access to the folder. Notice how I select the resource identifier in each case. Remember that TInput in this case is a tuple, i.e., (FileId fileId, FolderId folderId).
This is what I meant when I said that one of the advantages of the CQS approach to AOP is that it enables the aspect to have access to input/output data in a strongly typed way.
Logging aspect example
Let’s take a look at another example: Logging.
Take a look at the static LoggingAspect class. The nested Aspect class takes four dependencies in the constructor:
- The decorated function
- A dependency of type ILogger. This is the logger that will receive the logging data and record them somewhere. As this is just an example, I have created a simple implementation that simply writes to the console; the ConsoleLogger class.
- A dependency of type ILoggingDataExtractor<TInput, TOutput>. Notice how this interface is generic. Take a look at the definition of this interface. It has three methods; a method to extract logging data from the input, a method to extract logging data from the output, and a method to extract logging data from exceptions in the case of error. Being generic, this will enable interesting ways of selecting logging data. I will show you this very soon.
- Logging data to always include. This allows us to supply constant logging data at the time of applying the aspect.
Look now at the ApplyLoggingAspect extension method in the LoggingAspect static class. There are two overloads of this method for convenience.
One interesting thing to note about this method is that the last parameter is of type Action<LoggingDataExtractorBuilder<TInput, TOutput>>. This allows for an interesting way of selecting logging data when applying the aspect.
Take a look at the LoggingDataExtractorBuilder<TInput, TOutput> class if you want. But I am more interested in showing you an example of applying the logging aspect. Take a look at the Main method in Program.cs. Take a look at the way I called the ApplyLoggingAspect method to apply the logging aspect on an object of type EmailSender. Note how I select which input parameters to include using the IncludeInputs method in the LoggingDataExtractorBuilder class.
Conclusion:
In a previous article, I talked about the CQS (Command Query Separation) approach to AOP among many things. This approach has many advantages including the easiness to create and apply an aspect and the ability to have the aspect access input/output data in a strongly typed way. Still this approach requires the developer to design behavior classes in a certain way and makes the code more verbose.
In this article, I have presented a new approach to AOP which I call AOP via functions as an attempt to mitigate the issues of the CQS approach. The basic idea of this new approach is that we adapt the object we need to apply an aspect on to single or multiple functions, then we decorate this function/these functions with the decorator that represents the aspect, then we adapt this function/these functions back to the original interface.
To make the process of creating these adapters and using them a painless process, I have created the InterfaceMappingHelper Visual Studio extension. This extension allows the developer to auto-generate the adapters and a Map method that makes applying aspects easy.
This article was technically reviewed by Damir Arh.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Yacoub Massad is a software developer and works mainly on Microsoft technologies. Currently, he works at Zeva International where he uses C#, .NET, and other technologies to create eDiscovery solutions. He is interested in learning and writing about software design principles that aim at creating maintainable software. You can view his blog posts at
criticalsoftwareblog.com. He is also the creator of DIVEX(
https://divex.dev), a dependency injection tool that allows you to compose objects and functions in C# in a way that makes your code more maintainable. Recently he started a
YouTube channel about Roslyn, the .NET compiler.