Click here to Skip to main content
13,861,039 members
Click here to Skip to main content
Add your own
alternative version

Stats

2K views
4 bookmarked
Posted 23 Jan 2019
Licenced CPOL

Mimick - A Fody aspect-oriented weaving framework

, 23 Jan 2019
Rate this:
Please Sign up or sign in to vote.
A managed library for automated dependency injection, contract validation, and custom aspect-oriented decorator implementations

Mimick

The source code for the Mimick framework is available on GitHub, and the latest version is available to install from NuGet, at the below:

If not using NuGet for package management, the build instructions are also included on the GitHub page, which is to run the following command in the base directory of the solution:

dotnet publish -c Release

Introduction

The Mimick framework is an aspect-oriented (AOP) weaving tool designed to alleviate some of the repetitive and time-consuming tasks involved with designing and implementing an application structure. The framework provides several useful utilities to assist when developing applications, including:

  • Contract validation
  • Configuration resolution
  • Dependency injection
  • Scheduled tasks

This article is designed to provide a background on the framework, on how the weaving works, and how the framework can be configured and used within an application.

The framework supports both .NET 4.6.1 and later, and .NET Standard 2.0 and later.

Background

The framework was designed following a small mobile project I was working on using Xamarin. I found myself wanting to automate a variety of different tasks, such as automatically resolving service implementations and validating method parameters. I discovered the Fody weaving framework after trying to find an existing library which provided these features, and began using a combination of different add-ins to accomplish what I needed.

Soon after my work was complete, I began investigating developing my own variation of some of the add-ins. More specifically, I wondered whether it was possible to build a framework which provided a good selection of features which could be re-used, and also be used in conjunction with existing add-ins.

Fody

The Mimick framework leverages the Fody weaving framework for aspect-oriented decorator support. Fody is a library which, when installed against a project, introduces a build task which executes once the project binary has been compiled and allows for modifying (or "weaving") the binary to include amendments.

Installation

Installation of the Mimick framework can be achieved through NuGet. Either install the Fody and Mimick.Fody packages through the Visual Studio package manager interface, or by running the following commands in the package manager console:

Install-Package Fody
Install-Package Mimick.Fody

Once the packages have been installed, a file must be present in the root of the project directory in order for Fody to correctly locate and run the Mimick weaver. If it does not exist, create an XML document in the root of the directory with the name FodyWeavers.xml and ensure that the file contains the following content:

<?xml version="1.0" encoding="utf-8"?>
<Weavers>
  <Mimick />
</Weavers>

Once the above steps have been completed run a rebuild of the project and ensure that the project builds successfully. There may be messages regarding Fody printed to the Build output window, however if these messages are not present it may just be that the verbosity of the Build output is configured to hide them.

Configuration

The Mimick framework requires some small configuration in order to function fully. This configuration is not required if the framework is only going to be used for the contract validation attributes; however, if any of the other features (such as dependency injection, configuration resolution, scheduling) is intended to be used, the framework must be configured.

The configuration of the framework should occur in the entry-point of the application. This may differ depending on the type of application being developed. For example, this could be a Main() method, or an OnStartup() method, etc.

To begin configuring the framework, collect the current instance of the framework as associated with the running application. A single framework instance will exist for the life-time of the application, or until disposed.

IFrameworkContext context = FrameworkContext.Current;

The IFrameworkContext interface contains properties for configuring the framework, as described below.

Configuration Context

The configuration context of the framework is used to setup the sources where configuration values can be resolved from. The context contains a Register method which should be used to introduce the sources.

The Register method accepts an implementation of an IConfigurationSource as a parameter. There are some configuration sources available, and additional ones available by adding add-in NuGet packages.

KeyValueConfigurationSource

This is a basic source which reads values from an IDictionary<string, string> implementation. The source dictionary is considered mutable, so configuration values can be changed during the life-time of the configuration source.

var values = new Dictionary<string, string>();
values.Add("A", "Apple");
values.Add("B", "Banana");

context.ConfigurationContext.Register(new KeyValueConfigurationSource(values));

XmlConfigurationSource

This is a source which processes the content of an XML document, either from a file, stream or XmlDocument value, and exposes the elements of the document as configuration sources. When resolving configuration values from an XML configuration source, XPath must be used to identify the element or attribute containing the value.

<?xml version="1.0" encoding="utf-8"?>
<Config>
  <Id>1234</Id>
  <Application Name="Test" Version="1.0" />
</Config>
context.ConfigurationContext.Register(new XmlConfigurationSource("document.xml"));

var id = context.Resolve("//Config/Id");
var name = context.Resolve("//Config/Application/@Name");.

JsonConfigurationSource

This is a source which processes the content of a JSON document, either from a file or stream, and exposes the objects of the document as configuration sources. When resolving configuration values from a JSON configuration source the full path to the configuration should be delimited with periods (.)

{ "Configuration": { "Id": "1234", "Name": "Test" } }
context.ConfigurationContext.Register(new JsonConfigurationSource("document.json"));

var id = context.Resolve("Configuration.Id");
var name = context.Resolve("Configuration.Name");

Note

The JSON configuration is only available by installing the separate Mimick.Config.Json package which has a dependency on the Newtonsoft.Json package.

YamlConfigurationSource

This is a source which processes the content of a YAML document, either from a file or stream. The source should be used in the same manner as the JSON configuration source.

Configuration
  Id: 1
  Name: Test
context.ConfigurationContext.Register(new YamlConfigurationSource("document.yaml"));

var id = context.Resolve("Configuration.Id");
var name = context.Resolve("Configuration.Name");

Note

The YAML configuration is only available by installing the separate Mimick.Config.Yaml package which has a dependency on the YamlDotNet.Signed package.

Component Context

The component context of the framework is used to register classes as dependencies of the default dependency container. The component context can be replaced by calling the IFrameworkContext.SetComponentContext method, which is useful for introducing a custom dependency container.

The context contains methods for registering and resolving dependencies. The dependencies of the framework should be registered when the application is starting, before any of the classes are used.

var components = context.ComponentContext;

components.Register<Service>();
components.Register<IService, Service>();

components.Register(typeof(Service));
components.Register(typeof(IService), typeof(Service));

components.Register(new Service());
components.Register<IService>(new Service());

The context also attempts to resolve, automatically, interface contracts with the concrete classes implement. Therefore, if you have a service class which implements a service interface, the context will deduce this and register the binding in the background. The context will not bind against interfaces defined within the standard .NET and netstandard libraries.

The context also supports the registration of assemblies, which are scanned when the framework is initialized for all classes which have been decorated with the Component or Configuration attributes. Assemblies can be registered either by providing a type reference, or by registering the assembly directly.

var components = context.ComponentContext;

components.RegisterAssembly<Program>();
components.RegisterAssembly(Assembly.GetExecutingAssembly());

The context also accepts names for components, so it's possible to register one or more of the same class or interface. This is resolved later using a named qualifier.

var components = context.ComponentContext;

components.Register<IService, Service1>("svc1");
components.Register<IService, Service2>("svc2");

Components may also be registered using the Component attribute against class implementations, as described in the attributes section further below.

Initialization

Once the configuration and component contexts have been configured, the framework must be initialized to properly populate the dependency container and to begin any scheduled tasks. This is achieved by calling the Initialize method on the framework context instance.

context.Initialize();

Shutdown

Once the application is being terminated, it is recommended that the framework is shutdown to release any background tasks and to free any components which might require disposal. The framework implements the IDisposable interface, which cascades down to the individual contexts.

context.Dispose();

Attributes

The framework exposes many different attributes, some which are designed explicitly for the framework to leverage, and others which introduce AOP to automate certain behaviours. The following section lists some of the core attributes.

Configuration

Indicates that the associated class is a configuration provider. Any class which is decorated with this attribute, where the containing assembly has been registered in the component context, will be resolved during framework initialization to provide configuration values and component definitions.

[Configuration]
public class ApplicationConfiguration
{

}

Component

Indicates that the associated class is a component of the dependency injection system. The attribute can also be applied to methods and properties when declared within a class decorated with the Configuration attribute to indicate that the return value of the member is a component.

[Component]
public class Service
{

}

-

[Configuration]
public class ApplicationConfiguration
{
    [Component]
    public Service Service { get; }

    [Component]
    public Service GetService() { }
}

The attribute supports providing a scope, which determines how the dependency persists within the framework, and an optional collection of names.

Provide

Indicates that the associated method or property produces a configuration value. This allows for an extension of configurations over the configuration context source. The attribute requires a configuration name, which can be later resolved. The attribute is only detected in classes decorated with the Configuration attribute.

[Configuration]
public class ApplicationConfiguration
{
    [Provide("application.name")]
    public string Name { get; }

    [Provide("application.time")]
    public DateTime GetTime() => DateTime.Now;
}

Autowire

Indicates that the associated property, parameter or field should be populated with a dependency which exists in the component context. The member will be populated when first accessed, akin to lazy-populating. If applied to a method, the attribute applies to all optional parameters.

public class Service
{
    [Autowire]
    private Dependency dependency;

    [Autowire]
    public Dependency Dependency { get; set; }

    [Autowire]
    public Service(Dependency dependency = null) { }

    public void Execute([Autowire] Dependency dependency = null) { }
}

Value

Indicates that the associated property, parameter or field should be populated with a constant, dynamic or variable value. The attribute requires an expression be present which will be evaluated when the member is accessed for the first time.

When resolving configuration values, ensure that the configuration name is wrapped in curly brackets ({ and }) with the configuration name between.

public class Service
{
    [Value("Test")]
    private String text; // "Test"

    [Value("123")]
    private int number; // 123

    [Value("1 + 2 + 3")]
    private int added; // 6

    [Value("1 + 3 * (8 / 4) - 1 / 2 + 4")]
    private double complex; // 10.5

    [Value("'Value is ' + (1 + 2)")]
    private String concatenated;

    [Value("{application.name}")]
    private String name;
}

Note that you can also use the Value class directly to parse and evaluate expressions. This can be done by providing the expression in the constructor:

using Mimick.Values;

var value = new Value("10 + 15 * 2 - {x} + 5");
value.Variables.First(x => x.Expression == "x").Value = 20;

var result = value.Evaluate(); // 25

More

There are many more attributes available, and can be found documented on the GitHub Attributes page.

Scheduler

The scheduler system was implemented as part of the configuration context. As the Mimick framework is initialized and discovers component classes, the framework registers those classes and later scans them for methods decorated with the Scheduled attribute.

The Scheduled attribute supports an interval specified in milliseconds, which indicates the time between the framework having completed initializing that the method should be invoked. There is intended support for Cron expressions, however the library that the framework was leveraging does not have a signed package.

[Component]
public class BackgroundTasks
{
    [Scheduled(60000)]
    public static void ExecuteStatic()
    {
        Console.WriteLine($"I have been executed statically");
    }

    [Scheduled(90000)]
    public void Execute()
    {
        Console.WriteLine($"I have been executed against the component instance");
    }
}

All scheduled methods get placed into a task context, which launches a background thread monitoring for tasks which need to be executed (named "Mimick Schedule Thread") which frequently polls the collection of scheduled methods for execution. When a method is ready to be executed, the method is invoked using a TaskFactory.StartNew() invocation.

It's important to note that invocations will never overlap, and will instead execute at the interval after the last invocation was successful. For example, if a scheduled method has an interval of 30 seconds but takes 40 seconds to complete, the second invocation will occur at 100 seconds since the framework was initialized (30 seconds initial interval + 40 seconds process time + 30 seconds interval)

Decorators

Mimick supplies interfaces which can be implemented for custom decorator and interceptor patterns. Attributes should implement an interface which reflects the operation that is expected. For example, an attribute which is expected to validate the value of a property or parameter should implement the relevant IPropertySetInterceptor and IParameterInterceptor interfaces, respectively.

There are some fundamental attributes and interfaces which may be used to customise how a decorator or interceptor is implemented, and the information available.

CompilationOptions

An attribute which, when used on a decorator or interceptor attribute, provides additional options for customising how the attribute is integrated during compile-time.

[CompilationOptions(Scope = AttributeScope.Instanced)]
public class InterceptorAttribute : Attribute, IMethodInterceptor { }

The CopyArguments property indicates whether, following an interception, any updates method arguments or return values should be copied back into their original variables. This option is disabled by default, so when intending on introducing an interceptor which changes or resolves values, this option may need enabling.

The Scope property determines how the interceptor attribute should be instantiated. An attribute should be cached as early as possible to prevent unnecessary slow-down. The available options are:

  • Adhoc
    The attribute is created each time that it is required: never cached.
  • Instanced
    The attribute is created once when a new object instance is created.
  • MultiInstanced
    The attribute is created once per member when a new object instance is created.
  • Singleton
    The attribute is created once per application.
  • MultiSinglton
    The attribute is created once per member per application.

CompilationImplements

An attribute which, when used on a decorator or interceptor attribute, indicates that the attribute should implement an interface on the class associated with the member. The attribute expects an interface type, and also expects the associated attribute to implement the same interface.

[CompilationImplements(typeof(IComparable))]
public class ComparableAttribute : Attribute, IComparable
{
    public int CompareTo(object obj) { .. }
}

When a class is compiled and has an implementing attribute, the interface methods are added and the implementation is routed to the attribute. A compiled class may look like:

public class CompareClass : IComparable
{
    private readonly IComparable __attribute = new ComparableAttribute();

    public int CompareTo(object obj) => __attribute.CompareTo(obj);
}

IInstanceAware

An interface which, when implemented by a decorator or interceptor attribute, indicates that the attribute should be updated with an instance reference to the current object instance when the attribute is created. The Instance property is updated with the current object instance, unless the attribute has been scoped to singleton or multi-singleton.

public class InterceptorAttribute : Attribute, IInstanceAware
{
    public object Instance { get; set; }
}

It's important to remember that the Instance property will not be assigned a value until after the attribute constructor has been called.

IMemberAware

An interface which, when implemented by a decorator or interceptor attribute, indicates that the attribte should be updated with information on the member that the attribute was associated with. The Member property is updated with the member information. If the member is a parameter then the value will not be set. The interface should only be used with attributes scoped to adhoc or multi-instanced.

public class InterceptorAttribute : Attribute, IMemberAware
{
    public MemberInfo Member { get; set; }
}

It's important to remember that the Member property will not be assigned a value until after the attribute constructor has been called.

IRequireInitialization

An interface which, when implemented by a decorator or interceptor attribute, indicates that the attribute requires further initialization after the attribute has been constructed. The Initialize method is called following the constructor, and the assignment of any IInstanceAware or IMemberAware properties.

public class InterceptorAttribute : Attribute, IRequireInitialization
{
    public void Initialize() { }
}

IParameterInterceptor

An interceptor interface used to intercept parameters at the beginning of a method body, An attribute implementing this interface can be associated with both a method and a parameter, where the former applies to all parameters of the method.

The interceptor event arguments contain information on the object instance and the intercepted parameter. In addition, the Value property contains the value of the argument when the method was invoked. This value can be updated to a new value, but requires the CopyArguments flag enabled in order to cascade this to the method.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter)]
public class InterceptorAttribute : Attribute, IParameterInterceptor
{
    public void OnEnter(ParameterInterceptionArgs e)
    {
        var message = $"Parameter {e.Parameter.Name} has a value of {e.Value}";
    }

    public void OnException(ParameterInterceptionArgs e, Exception ex)
    {
        ..
    }

    public void OnExit(ParameterInterceptionArgs e)
    {
        ..
    }
}

IMethodInterceptor

An interceptor interface used to intercept method invocations. An attribute implementing this interface can be associated with a method, parameter or class, where the latter applies to all methods.

The interceptor event arguments contain information on the object instance and the intercepted method. The Arguments property can be used to get or set the arguments passed into the method, but the CopyArguments flag must be enabled in order to cascade any changes to the method.

The Cancel property indicates to the method that no further processing should take place. This does not prevent further interceptors from executing, but does skip the method body from being processed.

The Return property contains the return result of the method. In the OnEnter method this will always be the default value of the return type. In the OnExit method this will be the value which was returned from the method body. Similar to the Arguments property, this value can be updated and will apply prior to the method returning. If the method does not return a value then this will always be null.

[AttributeUsage(AttributeTargets.Method)]
public class InterceptorAttribute : Attribute, IMethodInterceptor
{
    public void OnEnter(MethodInterceptionArgs e)
    {
        var message = $"Method {e.Method.Name} has been intercepted";
    }

    public void OnException(MethodInterceptionArgs e, Exception ex)
    {
        ..
    }

    public void OnExit(MethodInterceptionArgs e)
    {
        var message = $"Method {e.Method.Name} is returning {e.Return}";
    }
}

IMethodReturnInterceptor

An interceptor interface used to intercept method bodies prior to the method returning. An attribute implementing this interface can be associated with a method return value. For example:

public class Example
{
    [return: Interceptor]
    public int GetId() => 1;
}

The interceptor event arguments contain information on the object instance and the intercepted method. The Return property follows the same conditions as IMethodInterceptor.

[AttributeUsage(AttributeTargets.ReturnValue)]
public class InterceptorAttribute : Attribute, IMethodReturnInterceptor
{
    public void OnReturn(MethodReturnInterceptionArgs e)
    {
        var message = $"Method {e.Method.Name} is returning {e.Return}";
    }
}

IPropertyGetInterceptor

An interceptor interface used to intercept property getter methods. An attribute implementing this interface can be associated with a property or field, where the latter will be converted into a property and all references replaced with a reference to the new property instead.

The interceptor event arguments contain information on the object instance and the intercepted property. The Value property can be updated with a new value in the OnExit method to have that value returned from the property, and to have it cascade to the setter of the property (where applicable.)

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class InterceptorAttribute : Attribute, IPropertyGetInterceptor
{
    public void OnGet(PropertyInterceptionArgs e)
    {
        var message = $"Property {e.Property.Name} getter is called";
    }

    public void OnException(PropertyInterceptionArgs e, Exception ex)
    {
        ..
    }

    public void OnExit(PropertyInterceptionArgs e)
    {
        var message = $"Property {e.Property.Name} is returning {e.Value}";
    }
}

IPropertySetInterceptor

An interceptor interface used to intercept property setter methods. An attribute implementing this interface can be associated with a property or field, following the same rules as the IPropertyGetInterceptor.

The interceptor event arguments contain information on the object instance and the intercepted property. The Value property can be updated with a new value in the OnSet method to have that value cascade to the setter.

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class InterceptorAttribute : Attribute, IPropertySetInterceptor
{
    public void OnSet(PropertyInterceptionArgs e)
    {
        var message = $"Property {e.Property.Name} is being set to {e.Value}";
    }


    public void OnException(PropertyInterceptionArgs e, Exception ex)
    {
        ..
    }

    public void OnExit(PropertyInterceptionArgs e)
    {
        ..
    } 
}

Example

Let's create an attribute for a specific requirement. We need an attribute which intercepts fields, properties and parameters and, if a valid number type, replaces the value with a random number. With the knowledge that we need to intercept these members, we must leverage the IParameterInterceptor (for parameters) and IPropertyGetInterceptor (for fields and properties) interfaces.

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property)]
public class RandomAttribute : Attribute, IParameterInterceptor, IPropertyGetInterceptor
{
}

The attribute must support changing the value of the intercepted member, so we must enable the CopyArguments flag to ensure that this gets picked up when the assembly is compiled. We also know that the attribute doesn't require any parameters or member-specific options as it should update any numeric type, so the attribute can be stored as a singleton.

[CompilationOptions(CopyArguments = true, Scope = AttributeScope.Singleton)]
...

Next, the attribute should retain a reference to the Random class so that it can be used across all invocations. The Random class is not thread-safe so an accompanying method should be introduced to generate the next random value. Since the attribute will be created as a singleton, we can safely introduce a field to store the value.

private readonly Random random = new Random();

private double GetNextRandom(double min, double max)
{
    lock (random) return min + (random.NextDouble() * (max - min));
}

Next, the method used to generate the new value needs to be implemented. This method will be defined once and used in both IParameterInterceptor and IPropertyGetInterceptor methods.

private object GetGenerated(Type type)
{
    switch (Type.GetTypeCode(type))
    {
        case TypeCode.Byte:
            return (byte)GetNextRandom(byte.MinValue, byte.MaxValue);
        case TypeCode.SByte:
            return (sbyte)GetNextRandom(sbyte.MinValue, sbyte.MaxValue);
        case TypeCode.Int16:
            return (short)GetNextRandom(short.MinValue, short.MaxValue);
        ...
    }

    return null;
}

Finally, the interception methods need implementing to support the new logic. The default behaviour of the GetGenerated method is to return null if the provided type is not a compatible number, so we can use that to check whether we actually need to update the value of the intercepted member.

// IParameterInterceptor.OnEnter
public void OnEnter(ParameterInterceptionArgs e)
{
    var value = GetGenerated(e.Parameter.ParameterType);

    if (value != null)
        e.Value = value;
}

// IPropertyGetInterceptor.OnExit
public void OnExit(PropertyInterceptionArgs e)
{
    var value = GetGenerated(e.Property.PropertyType);

    if (value != null)
        e.Value = value;
}

That completes the interceptor. The other IParameterInterceptor and IPropertyGetInterceptor methods should be implemented but can be left blank as redundant or empty methods are automatically inlined. If the attribute is now used against a field, property or parameter the value of the member should generate a random value each time it is accessed.

Summary

This article was aimed to provide some background knowledge on configuring and using the Mimick framework. The framework is intended to be used as a foundation layer in applications, and attempts to expose as many options as possible to provide this. If you find any issues with the framework please raise an issue against the GitHub project, or leave a comment on here.

History

  • 18/01/2019 - Initial version of the article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Chris Copeland
Software Developer (Senior)
United Kingdom United Kingdom
I began developing at age 12, in 2002, when I learned to code basic HTML, CSS and JavaScript. I soon moved on to PHP web-development with MySQL to create interactive websites for personal projects and clients.

Later, in 2005, I explored developing in VB6 before advancing to VB.NET. After much resistance, I picked up C# development, and never turned back. Shortly afterward, I was introduced to C and C++ after working on an open-source game project. I contributed to this for several years, before hanging up my pointer->pants.

In 2008 I decided to join the growing community of Java developers, and began learning to code in this alternative model for cross-platform application development. Eventually, I would end up entering a job comprised of both Java and C# development.

Continue? Insert 20p..

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web02 | 2.8.190214.1 | Last Updated 23 Jan 2019
Article Copyright 2019 by Chris Copeland
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid