Automating .NET Core Services with PostSharp and Aspect-Oriented Code



If you’re writing software targeting .NET then there’s a good chance you’re using Object-Oriented principles to do your work. In the process you may run into design patterns which you find yourself often repeating. Some of these patterns start as an abstract need such as “I want to log what my code is doing”. In practice this is easy to solve but as your solutions grow in scope you may find yourself spending a significant amount of time just deciding where and how to log your program’s state. Eventually you may want to change the logging pattern but that will require a significant amount of changes to the source.

While many people have solved this in an Object-Oriented way, these cross-cutting concerns will eventually add up to significant time sinks for any development team. They also present an architectural challenge later down the line when you need to make a significant change. You will have to walk back through all of that boilerplate code at some point when you want to add new log sinks, monitors, or internal log exception handlers. This problem comes up again on many common concerns such as security, caching, threading, rate limiting, and even alerting a GUI that some viewable object has been updated.

Aspect-Oriented designs are focused on targeting those concerns by dealing with the problem in one place and then applying that solution everywhere in your code where it is needed. What I am trying to show here is a way to purposefully hide away those concerns after investing the time to fully model them out. By spending some extra time considering our approach to logging or threading then we can write Aspects which are more like qualities that can be assigned to areas of code. Once you write a [Log] attribute, then you can decide a class or a method will be logged simply by tagging it with the attribute. Later on the compiler does the rest of the work by going back over and adding the missing code where those Aspects are declared.

As a quick note before I get into the main example, Microsoft released .NET Core 3 Preview 1 last month and with it comes support for native Windows desktop namespaces. While this article deals with .NET Core 2.1, you can recompile it at any time under netcoreapp3.0 as a project target and it will support a wider surface of types, including UWP libraries, without any extra configuration.

Remarks

This article is an attempt to cover certain difficult topics which vary in design from one business to the next. The code I will reference is written to be as short as possible, not fast or efficient. This is in the interest of showcasing each of the features and creating a usable demonstration.

The goal here is to explore the cross-platform capabilities of the .NET Generic Host when combined with the power of a pattern-aware compiler. By wrapping the platform and runtime concerns around the generic host it becomes possible to seamlessly link almost any part of the .NET library for any platform. This doesn’t mean you can run a UWP library on Linux, but you won’t have to rewrite any code if you want to run that library when it detects Windows 10 as the OS.

PostSharp is a pattern-aware compiler extension for Visual Studio and will be featured heavily throughout the article. If you don’t have a license and want to follow along with the code then there is still a solution. The free Essentials version of this framework covers up to ten classes which is enough to implement the full program in this article. You will also need to download the extension for Visual Studio 2017.



Hosting Services

I spent the past couple months getting a handle on the new .NET Core Generic Host which is familiar to anyone who has worked in ASP.NET Core web applications. The host can also be used in console applications as well to handle built-in app configuration, dependency injection, and logging. Let’s take a moment to see how the service hosting fits into this design. The goal is to create a small application which can:

  • Host common services and load native classes based on OSVersion.

  • Automatically wrap logging around the entire solution.

  • Automatically thread the services for a multi-core computer.