Open sandboxFocusImprove this doc
  • Article

Injecting dependencies into aspects

Many aspects require services injected from a dependency injection container. For example, a caching aspect may depend on the IMemoryCache service. If you use the Microsoft.Extensions.DependencyInjection framework, your aspect should pull this service from the constructor. If the target type of the aspect does not already accept this service from the constructor, the aspect will need to append this parameter to the constructor.

However, the code pattern that must be implemented to pull any dependency depends on the dependency injection framework used by the project. As we have seen, the default .NET Core framework requires a constructor parameter, but other frameworks may use an [Import] or [Inject] custom attribute.

In some cases, as the author of the aspect, you may not know which dependency injection framework will be used for the classes to which your aspect will be applied.

This is where the Metalama.Extensions.DependencyInjection project comes in. Thanks to this namespace, your aspect can consume and pull a dependency with a single custom attribute. The code pattern to pull the dependency is abstracted by the IDependencyInjectionFramework interface, which is chosen by the user project.

The Metalama.Extensions.DependencyInjection namespace is open source and hosted on GitHub. It currently has implementations for the following dependency injection frameworks:

The Metalama.Extensions.DependencyInjection project is designed to make implementing other dependency injection frameworks easy.

Consuming dependencies from your aspect

To consume a dependency from an aspect:

  1. Add the Metalama.Extensions.DependencyInjection package to your project.
  2. Add a field or automatic property of the desired type in your aspect class.
  3. Annotate this field or property with the IntroduceDependencyAttribute custom attribute. The following attribute properties are available:
  • IsLazy resolves the dependency upon first use instead of upon initialization, and
  • IsRequired throws an exception if the dependency is not available.
  1. Use this field or property from any template member of your aspect.

Example: default dependency injection patterns

The following example uses Microsoft.Extensions.Hosting, typical to .NET Core applications, to build an application and inject services. The Program.Main method builds the host, and the host then instantiates our Worker class. We add a [Log] aspect to this class. The Log aspect class has a field of type IMessageWriter marked with the IntroduceDependencyAttribute custom attribute. As you can see in the transformed code, this field is introduced into the Worker class and pulled from the constructor.

1using Doc.LogDefaultFramework;
2using Metalama.Framework.Aspects;
3using Metalama.Extensions.DependencyInjection;
4
5[assembly:
6    AspectOrder( AspectOrderDirection.RunTime, typeof(LogAttribute), typeof(DependencyAttribute) )]
7
8namespace Doc.LogDefaultFramework;
9
10// Our logging aspect.
11public class LogAttribute : OverrideMethodAspect
12{
13    // Defines the dependency consumed by the aspect. It will be handled by the dependency injection framework configured for the current project.
14    // By default, this is the .NET Core system one, which pulls dependencies from the constructor.
15    [IntroduceDependency]
16    private readonly IMessageWriter _messageWriter;
17
18    public override dynamic? OverrideMethod()
19    {
20        try
21        {
22            this._messageWriter.Write( $"{meta.Target.Method} started." );
23
24            return meta.Proceed();
25        }
26        finally
27        {
28            this._messageWriter.Write( $"{meta.Target.Method} completed." );
29        }
30    }
31}

Example: ServiceLocator

The following example is similar to the previous one but uses the ServiceLocator pattern instead of pulling dependencies from the constructor.

1using Doc.LogServiceLocator;
2using Metalama.Framework.Aspects;
3using Metalama.Extensions.DependencyInjection;
4
5[assembly:
6    AspectOrder( AspectOrderDirection.RunTime, typeof(LogAttribute), typeof(DependencyAttribute) )]
7
8namespace Doc.LogServiceLocator;
9
10// Our logging aspect.
11public class LogAttribute : OverrideMethodAspect
12{
13    // Defines the dependency consumed by the aspect. It will be handled initialized from a service locator,
14    // but note that the aspect does not need to know the implementation details of the dependency injection framework.
15    [IntroduceDependency]
16    private readonly IMessageWriter _messageWriter;
17
18    public override dynamic? OverrideMethod()
19    {
20        try
21        {
22            this._messageWriter.Write( $"{meta.Target.Method} started." );
23
24            return meta.Proceed();
25        }
26        finally
27        {
28            this._messageWriter.Write( $"{meta.Target.Method} completed." );
29        }
30    }
31}

Selecting a dependency injection framework

By default, Metalama generates code for the default .NET dependency injection framework implemented in the Microsoft.Extensions.DependencyInjection namespace (also called the .NET Core dependency injection framework).

If you want to select a different framework for a project, generally adding a reference to the package implementing this dependency framework is sufficient, e.g., Metalama.Extensions.DependencyInjection.ServiceLocator. These packages typically include a TransitiveProjectFabric that registers itself. This works well when the project has a single dependency injection framework.

When several dependency injection frameworks can handle a specified dependency, Metalama select the one with the lowest priority value among them. This selection strategy can be customized for the whole project or for specified namespaces or types.

To customize the selection strategy of the dependency injection framework for a specific aspect and dependency:

  1. Add a ProjectFabric or NamespaceFabric as described in Configuring aspects with fabrics.
  2. From the AmendProject or AmendNamespace method, call the amender.Outgoing.ConfigureDependencyInjection method. Supply the empty delegate builder => {} as an argument to this method.
  3. From this delegate, do one of the following things:

Implementing an adaptor for a new dependency injection framework

If you need to support a dependency injection framework or pattern for which no ready-made implementation exists, you can implement an adapter yourself.

See Metalama.Extensions.DependencyInjection.ServiceLocator on GitHub for a working example.

The steps are as follows:

  1. Create a class library project that targets netstandard2.0.
  2. Add a reference to the Metalama.Extensions.DependencyInjection package.
  3. Implement the IDependencyInjectionFramework interface in a new public class. It is easier to start from the DefaultDependencyInjectionFramework class. In this case, you must override the DefaultDependencyInjectionStrategy class. See the source code and the class documentation for details.
  4. Optionally create a TransitiveProjectFabric that registers the framework by calling amender.Outgoing.ConfigureDependencyInjection, then builder.RegisterFramework.

Example

The following example shows how to implement the correct code generation pattern for the ILogger service under .NET Core. Whereas normal services usually require a parameter of the same type of the constructor, the ILogger service requires a parameter of the generic type ILogger<T>, where T is the current type used as a category.

Our implementation of IDependencyInjectionFramework implements the CanHandleDependency method and returns true only when the dependency is of type ILogger. The only difference in the default implementation strategy is the parameter type.

1using Doc.LogCustomFramework;
2using Metalama.Framework.Aspects;
3using Metalama.Extensions.DependencyInjection;
4using Microsoft.Extensions.Logging;
5
6[assembly:
7    AspectOrder( AspectOrderDirection.RunTime, typeof(LogAttribute), typeof(DependencyAttribute) )]
8
9namespace Doc.LogCustomFramework;
10
11// Our logging aspect.
12public class LogAttribute : OverrideMethodAspect
13{
14    // Defines the dependency consumed by the aspect. It will be handled by LoggerDependencyInjectionFramework.
15    // Note that the aspect does not need to know the implementation details of the dependency injection framework.
16    [IntroduceDependency]
17    private readonly ILogger _logger;
18
19    public override dynamic? OverrideMethod()
20    {
21        try
22        {
23            this._logger.LogWarning( $"{meta.Target.Method} started." );
24
25            return meta.Proceed();
26        }
27        finally
28        {
29            this._logger.LogWarning( $"{meta.Target.Method} completed." );
30        }
31    }
32}