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.
Validate architecture so that bugs throw exceptions in a deterministic way.
Now because I have spent a lot of time working on ETL solutions I tend to map out the flow of my program and look at what my goals are from a high level. Below I’ve drawn out just some basic shapes to illustrate what the structure is and try to see where the boundaries are going to be. These boundaries are important because they represent areas where concerns cross over and cut into the concerns of individual objects. When using AOP in a project I always find it helps to clearly define where those boundaries are because the goal is to hide how they work. You’ll have to excuse my MS Paint aesthetic here:
Immutable (Frozen) Runtime and Platform areas which host Services wrapped in an Actor Threading Model.
Program: A console app with Main() in this example. Runtime: This will be the top level for all of the PostSharp wrappers and the constructor for the rest of the program. This should launch the service host. Platform: This contains any methods needed to load native classes and provide a thread-safe reference to the Host in case any inter-service communication needs to be implemented. Frozen: Both of the previous layers will be frozen which provides some of the same guarantees as an immutable object. This will be covered later on. Hosting: The .NET Generic Host builder. Services: The services will be wrapped with an Actor model ensuring that they can run on a multi-core computer. Log Output Throughout this exercise you will see several examples of log output which describes the state of the program in a specific template. I’ve marked some of them here as an example of what the values represent.
Console log output generated automatically by PostSharp
Program
First, let’s start with a new .NET Core 2.1 console app.
namespace SharpCrafting
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}}
Next we need to add PostSharp to the project. If you have the extension for Visual Studio then you can right click on the project and add PostSharp to it through the menu. You can also use the Package Manager Console to run Install-Package PostSharp
You will also need to add the following NuGet packages:
PostSharp.Patterns.Common The bulk of the code contracts and aspects.
PostSharp.Patterns.Diagnostics Tracing, logging, and diagnostics.
PostSharp.Patterns.Diagnostics.Serilog Automatically inject Serilog.
PostSharp.Patterns.Threading Threading models, dispatching, and deadlock detection.
Serilog Logging framework.
Serilog.Sinks.Console and Serilog.Enrichers.Thread for the sink.
Now that we have our packages, let’s drop down one layer away from Program for now. I suggest a sealed runtime layer with static properties and an isolated, non-static entry-point. This allows the runtime to be instantiated as an object with its own threading model which can then launch onto a platform compatible with the caller’s context. The static properties are isolated away from the entry point and can then be used to attach async monitors or metrics later on.
namespace SharpCrafting{
public sealed class Runtime
{
static Runtime() {
Platform = new GenericPlatform();
Assembly = GetAssembly(typeof(Runtime));
ApplicationUri = Path.GetDirectoryName(Assembly.Location);
}
private static AssemblyGetAssembly<T>(Ttype)
=>type.GetType().GetTypeInfo().Assembly;
private static readonly AssemblyAssembly;
private static readonly string ApplicationUri;
private static readonly GenericPlatform Platform;
private readonly ILogger_log = Log.ForContext<Runtime>();
private const string LogTemplate="
{Timestamp:yyyy-MM-dd HH:mm:ss} | {Level:u3}: [{ThreadId}:
{SourceContext}] {Message:lj}{NewLine}{Exception}";
public void Entry ()
{ }
}}
Now that we have a basic structure, we can talk about threading models. PostSharp provides three major features in PostSharp.Patterns.Threading.
Threading Models: A threading model is a design pattern that gives guarantees that your code executes safely on a multi-core computer.
Thread Dispatching: Custom attributes DispatchedAttribute and BackgroundAttribute cause the execution of a method to be dispatched to the UI thread or to a background thread, respectively.
Deadlock Detection: Detects deadlocks at run time and throws an exception instead of allowing your application to freeze.
Threading models are named solutions to recurring problems and can be considered a design pattern which we will inject at compile time. Defects are discovered by validating the code both during build and while running. If the application is improperly threaded then it will fail in a deterministic way inside of these models. Without this assurance, race conditions or deadlocks tend to show up randomly and can corrupt data without any warning.
The primary threading model we will be looking at is the Actor model. Actors are services which run with their own state and a mailbox to receive messages in synchronous order. Their execution is more complex than this image describes, but they can be thought of as independent objects which run asynchronously against a queue of messages.
Actors do not map one-to-one with threads but it is helpful for illustrating execution.
The second threading model being used is the Freezable threading model. An immutable object can be safely accessed from multiple threads but the restrictions of immutability often makes configuration difficult. By using Freezable objects we can define at what point in time the object becomes immutable while still benefiting from mutability during creation.
using PostSharp.Patterns.Threading;
[Freezable]
public class Invoice
{
public long Id { get; set; }
}
public class Example
{
public void Create()
{
var invoice = new Invoice();
invoice.Id=123456;
((IFreezable)invoice).Freeze();
}}
Now let’s wrap the Runtime class with a frozen model.
To freeze an object we need to decorate it with an aspect called [Freezable]. To enforce most threading models, PostSharp relies on the [Aggregatable] attribute; this specifies that a class has its Parent/Child relationship explicitly defined. Any properties which are not children should be marked as a [Reference]. Any methods which return the same value for each input and make no observable state changes can be marked as [Pure] to inform PostSharp that they are safe in a threaded context. I opted to include [NotNull] to indicate which properties are available from the entry point.
[Freezable]
public sealed class Runtime
{
static Runtime()
{
Platform = new GenericPlatform();
Assembly = GetAssembly(typeof (Runtime));
ApplicationUri = Path.GetDirectoryName(Assembly.Location);
}
[Pure]
private static AssemblyGetAssembly<T>(Ttype)
=>type.GetType().GetTypeInfo().Assembly;
[NotNull, Reference]
private static readonly Assembly Assembly;
[NotNull, Reference]
private static readonly string ApplicationUri;
[NotNull, Child]
private static readonly GenericPlatform Platform;
[NotNull, Reference]
private readonly ILogger _log = Log.ForContext<Runtime>();
[NotNull, Reference]
private conststring Template=
"{Timestamp:yyyy-MM-dd HH:mm:ss} | {Level:u3}: [{ThreadId}:
{SourceContext}] {Message:lj}{NewLine}{Exception}";
public void Entry ()
{ }
}
Now that the Runtime class has been updated we can move on to the Platform. While developing for a cross-platform application on .NET Core, it’s helpful to have a layer where concerns of the platform can be safely handled. Let’s take a look at some functions you might see here:
GenericHost Creating the .NET generic host.
GetNativeClass Services may need to load platform-specific implementations and this will provide the correct type.
Crash If we are going to inject threading models safely then services also need a way to crash if state becomes fatal.
You will see the following patterns on the Platform code:
[Required] Throws if null is passed in so that the responsibility belongs to the caller.
INativeClass Serves as an interface to load and terminate native classes after importing them.
.Crash() Allows native platform classes to call for immediate debugging or termination if a fatal state is reached. It is marked with [ContractAnnotation (“=> halt”)] which specifies that for any given input the output will be a halted state. This allows the IDE to emit warnings appropriately.
GenericHost Will be decorated with [Reference] instead of [Child] which doesn’t guarantee thread safety anymore at the boundary but also doesn’t force the host to implement a compatible model.
public interface INativeClass{TaskInitialize <T> ([CanBeNull]Tparent);
TaskTerminate( string reason);
}
[Freezable]
internal class GenericPlatform{
[Reference]
public GenericHostHost = new GenericHost();
[Pure]
private Assembly_caller() =>Assembly.GetCallingAssembly();
[Pure]
privatebool InvalidNamespace([Required] Assemblyassembly,
[Required] string @namespace) =>assembly.GetTypes().All(type=>type.Namespace!=@namespace) ;
[Pure]
public INativeClassGetNativeClass([Required] string @namespace,
[Required]
string className) =>this.Create(_caller(), @namespace, className) ;
[Pure]
private INativeClassCreate([Required] Assemblyassembly,
[Required] string @namespace, [Required] string className)
{
if (this.InvalidNamespace(assembly, @namespace))
this.Crash($"[Generic Platform]: Namespace not be located:
{@namespace} in assembly: {assembly.FullName}");
// Platform looks like Win32NT for Windows.
string platformNamespace=$"{@namespace}.
{Environment.OSVersion.Platform.ToString()}";
// Check for any implementations of this platform.
if (this.InvalidNamespace(assembly, platformNamespace))
this.Crash($"[Generic Platform]:
{Environment.OSVersion.Platform.ToString()} was not found at
{@namespace} in assembly: {assembly.FullName}");
// Native implementations currently require usage of the same class
name but can exist under multiple platform namespaces.
try
{
var native=assembly.GetType($"{platformNamespace}.{className}");
var instance= (INativeClass)Activator.CreateInstance(native);
return instance;
}
catch (Exceptionex)
{
this.Crash($"Could not create an instance of a native platform
class called {className} --- {ex.Message}");
return null;
}
}
[Pure, ContractAnnotation("=> halt")]
public void Crash ( stringreason="Unexpected behavior.")
{
Log.Fatal(reason);
this.DebugHook();
// ReSharper disable once InconsistentNaming - 128 (0x80) indicates no need to wait for child processes
constint ERROR_WAIT_NO_CHILDREN = 128;
Environment.Exit(ERROR_WAIT_NO_CHILDREN);
}
[Pure, Conditional("DEBUG")]
private void DebugHook()
{
if (Debugger.IsAttached) Debugger.Break();
}}
Now we have a Runtime with a thread safe entry point and a Platform which can load native classes. At this point we can go ahead and use the .NET Generic Host, specifically HostBuiler to configure services and host them.
Several packages are helpful for using the HostBuilder:
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.CommandLine
Microsoft.Extensions.Configuration.EnvironmentVariables
Microsoft.Extensions.Configuration.Json
Microsoft.Extensions.DependencyInjection
Microsoft.Extensions.Hosting
Microsoft.Extensions.Options.ConfigurationExtensions
[ExplicitlySynchronized]
public class GenericHost{
[SingleEntryMethod]
public async TaskStart ( GenericPlatformplatform,
[ CanBeNull ] string[] args=null )
{
var builder = new HostBuilder () .ConfigureAppConfiguration ( configureDelegate: ( context, config ) => {
config.AddJsonFile ( "appsettings.json", optional: true ) ;
config.AddEnvironmentVariables () ;
if ( args!=null )
config.AddCommandLine ( args ) ;
}
)
.ConfigureServices ( configureDelegate: ( context, services ) => {
services.AddOptions () ;
services.Configure <AppConfig> ( context
.Configuration
.GetSection ( "AppConfig" )
) ;
services.AddHostedService <TimingService> () ;
services.Configure <AppConfig> ( options=>
{
options.Platform=platform ;
} ) ;
} ) ;
await builder.RunConsoleAsync () ;
}}
namespace SharpCrafting{
public class AppConfig
{
public GenericPlatform Platform { get; set; }
}
}
The .Start() method is decorated with a [SingleEntryMethod] attribute which automatically intercepts the method and returns null if it has been called more than once. This permits us to leave a reference to the Generic Host inside of the Platform without worrying about this method being called later on. Because this class is being intercepted, we can decorate it with the [ExplicitlySynchronized] aspect which informs PostSharp not to verify the safety of this class.
[SingleEntryMethod] is a custom aspect written with PostSharp. To create one of these we only need to decorate a class with [PSerializable] and then inherit from any of the PostSharp aspects. In this case we want to intercept calls at the boundary of the decorated method which is a feature provided by MethodInterceptionAspect.
namespace SharpCrafting.Aspects
{
[PSerializable]
public class SingleEntryMethodAttribute : MethodInterceptionAspect
{
/// Starts at zero and determines number of accesses.
public int CallCounter;
public bool Called = false;
public override void OnInvoke(MethodInterceptionArgsargs)
{
// Immediately increment so that a second thread cannot sneak
by without touching.
CallCounter++;
// Potential miss if multiple threads access simultaneously.
if (CallCounter>1)
{
if (!Called)
thrownew Exception("Two threads simultaneously entered a
single-entry method for the first time.");
args.ReturnValue = null;
return;
}
// Proceed with the original call this time.
Called = true;
args.Proceed();
}
}
}
SingleEntryMethodAttribute becomes [SingleEntryMethod]
As you can see in the Generic Host, one service was registered called TimingService. Let’s set that class up now. This will use the [Actor] threading model with a few new attributes:
[Actor] Generates code which prevents fields of an actor from being accessed from an invalid context. Requires [Aggregatable] parent/child decoration as well as the following two aspects.
[Reentrant] This attribute declares that an async method can be safely re-entered on each await statement. For the actor model, this means other methods can be invoked while waiting. This must be applied to all async actor methods.
[ExplicitlySynchronized] Opts out of the threading model by declaring that the object is handling its own safety.
[EntryPoint] This specifies that the method can be invoked from threads which do not currently have access to the object. This means an event handler, background task, or callback can safely enter the threading model. Only needs to be applied to private methods.
The TimingService class also inherits from IHostedService which integrates with the .NET Generic Host which is covered by this MSDN article. Each of these services will have StartAsync() called in the order they were registered during configuration earlier in the GenericHost class. The queue reverses this order when StopAsync() is called, starting with the last service registered and working its way back through the queue.
[Actor]
public class TimingService : IHostedService{
[Reference, ExplicitlySynchronized]
internal GenericPlatform_platform ;
[Reference]
private readonly ILogger_log = Log.ForContext <TimingService> () ;
[Child]
private INativeClass_nativeTimers { get ; set ; }
public TimingService ( IOptions <AppConfig> options )
{
_platform=options.Value.Platform ;
}
[Reentrant]
public async TaskStartAsync ( CancellationToken cancellationToken )
{
_log.Information ( "[Timing Service]:
Calling out to the platform for native timers." ) ;
_nativeTimers= (INativeClass)
_platform.GetNativeClass("SharpCrafting", "NativeTimers");
await _nativeTimers.Initialize(this);
}
[Reentrant]
public async TaskStopAsync ( CancellationTokencancellationToken )
{
_log.Warning ( "[Timing Service]: Terminating this service." ) ;
await _nativeTimers.Terminate("The timing service is being shut
down by the host.");
}
}
In the interest of keeping this example small we’re going to cheat here and just use a few Timer objects to run on threads and report back. This should illustrate that our class is appropriately using the threading model. These aren’t technically native namespaces either, but calling out to platform-specific code will work exactly the same way without any configuration.
With that in mind we need to implement the NativeTimers class which is imported from the Platform into TimingService after StartAsync() is called.
NativeTimers will need to be placed under a namespace compatible with the platform this code is running on. In my case I am running on Windows 10 17134 which will return “Win32NT” as the Platform. A list of potential platforms is provided by Microsoft.
MacOSX
Unix
Win32NT
using System.Threading.Timer;
namespace SharpCrafting.Win32NT
{
` [PrivateThreadAware]
class NativeTimers : INativeClass
{
[Reference]
private readonly ILogger_log = Log.ForContext<NativeTimers>();
[Reference]
private Timer _timerShort { get; set; }
[Reference]
private Timer _timerMedium { get; set; }
[Reference]
private Timer _timerLong { get; set; }
[Parent, CanBeNull]
private TimingService _parent { get ; set ; }
public async TaskInitialize<T> (Tparent)
{
TypeparentType = typeof( T ) ;
if ( parentType == typeof( TimingService ) )
_parent = (TimingService)(object)parent ;
_timerShort = new Timer
(e => WriteTime("short"),
null,
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1));
_timerMedium = new Timer(
e => WriteTime("medium"),
null,
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(3));
_timerLong = new Timer(
e => WriteTime("long"),
null,
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5)); }
public async TaskTerminate ( string reason ) {
_timerShort.Dispose () ;
_timerMedium.Dispose () ;
_timerLong.Dispose () ;
_log.Verbose("[Native Timers]: Disposed of the timers after
receiving reason: {reason}", reason);
}
[EntryPoint]
private async TaskWriteTime(string name)
{
_log.Verbose("[Native Timers] :
The current time is {UtcNow} on the
[ {name} ] timer.", DateTime.UtcNow , name);
}
}
}
At this point we have created the following areas of code:
Runtime Entered from Program, configures logging and then sets up the platform.
Platform Contains methods for loading native types and starts the generic host.
Generic Host .NET Core Generic Host which configures and creates services.
TimingService An example service being threaded with the Actor model.
NativeTimers An example being loaded natively by the Timing Service.
Now we just need to inject the logging wrapper and freeze our layers, starting with the .Entry() function in Runtime.
You will see the following patterns here:
LoggingServices.DefaultBackend This is the PostSharp logging back end. By setting our logger here we can then inject it later on.
Post.Cast<T, IFreezable>( object ).Freeze() This is the call to freeze an object and mark it as immutable.
public void Entry()
{
try
{
var log = VerboseLogger();
Log.Logger = log;
LoggingServices.DefaultBackend = new SerilogLoggingBackend
(log.ForContext("RuntimeContext", "PostSharp"));
}
catch (Exceptionex)
{
Platform.Crash();
}
try
{
Post.Cast<Runtime, IFreezable>(this).Freeze();
Post.Cast<GenericPlatform, IFreezable>(Platform).Freeze();
var cleanlyLaunch = Platform.Host.Start(Platform);
var returned=cleanlyLaunch.ContinueWith((t) =>
{
if (t.IsFaulted||t.IsCanceled)
Platform
.Crash($"Fatal exception in the generic platform at
runtime{Environment.NewLine}{t.Exception?.ToString()}");
else
_log
.Information("Successfully launched the common platform
from the runtime.");
});
returned.Wait();
// Results in sink monitors receiving StopMonitoring calls.
Log.CloseAndFlush();
}
catch (ObjectReadOnlyExceptionroEx)
{
_log.Fatal($"Property or state were attempted on a frozen object."+
$"${Environment.NewLine}Runtime Freeze(): {roEx.Data}");
Platform.Crash("Fatal attempt to modify a frozen object.");
}
catch (ThreadMismatchExceptionthreadEx)
{
_log.Fatal($"An object was accessed from a different thread than it
was created."+
$"{Environment.NewLine}Please use a threading model or
freeze the object before crossing boundaries."+
$"{threadEx}");
Platform.Crash("Fatal thread access exception was thrown.");
}
catch (Exceptionex)
{
_log.Fatal($"Could not freeze and launch the common platform from
the runtime."+$"{Environment.NewLine}{ex}");
Platform.Crash("Unknown failure occurred in an EntryPointAttribute
for the runtime.");
}
}
The VerboseLogger() implementation and BlueConsole theme:
private static ILoggerVerboseLogger()
{
var config = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.WriteTo.Console(outputTemplate: Template, theme:
ConsoleExtensions.BlueConsole);
return config.CreateLogger();
}
using Serilog.Sinks.SystemConsole.Themes ;
namespace SharpCrafting
{
public static class ConsoleExtensions
{
public static SystemConsoleTheme BlueConsole
{ get; } =new SystemConsoleTheme(
new Dictionary<ConsoleThemeStyle,
SystemConsoleThemeStyle> {
[ConsoleThemeStyle.Text] =new SystemConsoleThemeStyle {
Foreground=ConsoleColor.Blue },
[ConsoleThemeStyle.SecondaryText]
=new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkGray },
[ConsoleThemeStyle.TertiaryText]
=new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkGray },
[ConsoleThemeStyle.Invalid] =newSystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkYellow },
[ConsoleThemeStyle.Null] =new SystemConsoleThemeStyle {
Foreground=ConsoleColor.DarkRed },
[ConsoleThemeStyle.Name] =new SystemConsoleThemeStyle {
Foreground=ConsoleColor.Green },
[ConsoleThemeStyle.String] =new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.White },
[ConsoleThemeStyle.Number] =new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkMagenta },
[ConsoleThemeStyle.Boolean] =newSystemConsoleThemeStyle
{ Foreground=ConsoleColor.Yellow },
[ConsoleThemeStyle.Scalar] =new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkBlue },
[ConsoleThemeStyle.LevelVerbose]
=new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkGray,
Background=ConsoleColor.Yellow },
[ConsoleThemeStyle.LevelDebug]
=new SystemConsoleThemeStyle {
Foreground=ConsoleColor.DarkBlue,
Background=ConsoleColor.Green },
[ConsoleThemeStyle.LevelInformation]
=new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkGreen,
Background=ConsoleColor.White },
[ConsoleThemeStyle.LevelWarning]
=new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkCyan,
Background=ConsoleColor.Yellow },
[ConsoleThemeStyle.LevelError]
=new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkMagenta,
Background=ConsoleColor.White },
[ConsoleThemeStyle.LevelFatal]
=new SystemConsoleThemeStyle
{ Foreground=ConsoleColor.DarkRed,
Background=ConsoleColor.Gray }
});
}}
Last we just need to call the Runtime from the program and inject logging into the assembly.
Create a new GlobalAspects.cs class without any using or namespace declarations. Include these lines:
[assembly:Log(AttributePriority = 1,
AttributeTargetMemberAttributes = MulticastAttributes.Public |
MulticastAttributes.Private | MulticastAttributes.Internal |
MulticastAttributes.Protected)]
This will wrap all Public, Private, Internal, and Protected members with the [Log] attribute from the assembly level.
Now just call the runtime from Program and watch it run.
class Program
{
static void Main ( string[] args)
{
Console.WriteLine("Hello World!");
var runtime = new Runtime();
runtime.Entry();
Console.WriteLine("Press any key to stop.");
Console.ReadKey () ;
}
}
If everything is set up correctly then we should be able to watch the entire flow of execution print out to the console.
We expect to see something like this:
Generic Host starts up and creates the services.
Services access AppConfig which creates the Platform.
Services start asynchronously and ask for native types from the Platform.
Native Timers class is constructed and three timers are set
Application starts.
Timers callback to the private [EntryPoint] on WriteTime().
If you’ve been following along up to this point then you now can launch native classes onto services hosted cross-platform with the logging and threading handled.
Where can we go from here?
Calling any libraries or namespaces from the services will automatically pipe the log statements if those libraries also implement Serilog, whether or not they have a custom log handler. This can be made compatible with other loggers as well.
State machines will automatically be maintained by PostSharp so that asynchronous code is threaded safely. You can see these decorated in the logs above. The services are able to call into asynchronous code from other frameworks as well; this means a UWP .NET Framework 4.7.1 class library can be called from .NET Standard 2.0 library referenced by a service running under .NET Core 3.0. This so far has worked without any needed configuration for me other than to compile the console application under <TargetFramework>netcoreapp3.0</TargetFramework>.
Asynchronous log monitors can be attached to Serilog with the ability to call back into Runtime or Platform as needed. These can be used to monitor flow, handle dropped log events, and handle deterministic internal logger exceptions.
An Event-Sourcing timeline, Inter-Process Communication Channel, or API can all be attached to the service actor model. Each actor can write events back to a central source asynchronously while guaranteeing the messages are threaded properly.
Final Remarks
I will attach the source code for this entire project below if you’d like to clone it and play around with the solution. Feel free to modify or redistribute it as needed.
Hopefully this article was able to show you the power of Aspect Oriented Programming when combined with the new .NET Core architecture to provide highly extensible service hosting on many platforms.
Source: Medium
The Tech Platform
Commentaires