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:
- DefaultDependencyInjectionFramework implements the default .NET Core pattern (see Dependency injection in .NET).
- ServiceLocatorDependencyInjectionFramework can be used by classes or projects that are not instantiated by a dependency injection framework thanks to a simple service locator pattern.
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:
- Add the
Metalama.Extensions.DependencyInjection
package to your project. - Add a field or automatic property of the desired type in your aspect class.
- 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.
- 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#pragma warning disable CS0649, CS8618
6
7[assembly:
8 AspectOrder( AspectOrderDirection.RunTime, typeof(LogAttribute), typeof(DependencyAttribute) )]
9
10namespace Doc.LogDefaultFramework;
11
12// Our logging aspect.
13public class LogAttribute : OverrideMethodAspect
14{
15 // Defines the dependency consumed by the aspect. It will be handled by the dependency injection framework configured for the current project.
16 // By default, this is the .NET Core system one, which pulls dependencies from the constructor.
17 [IntroduceDependency]
18 private readonly IMessageWriter _messageWriter;
19
20 public override dynamic? OverrideMethod()
21 {
22 try
23 {
24 this._messageWriter.Write( $"{meta.Target.Method} started." );
25
26 return meta.Proceed();
27 }
28 finally
29 {
30 this._messageWriter.Write( $"{meta.Target.Method} completed." );
31 }
32 }
33}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3
4namespace Doc.LogDefaultFramework;
5
6// The class using the Log aspect. This class is instantiated by the host builder and dependencies are automatically passed.
7public class Worker : IConsoleMain
8{
9 [Log]
10 public void Execute()
11 {
12 Console.WriteLine( "Hello, world." );
13 }
14}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3
4namespace Doc.LogDefaultFramework;
5
6// The class using the Log aspect. This class is instantiated by the host builder and dependencies are automatically passed.
7public class Worker : IConsoleMain
8{
9 [Log]
10 public void Execute()
11 {
12 try
13 {
14 _messageWriter.Write("Worker.Execute() started.");
15 Console.WriteLine("Hello, world.");
16 return;
17 }
18 finally
19 {
20 _messageWriter.Write("Worker.Execute() completed.");
21 }
22 }
23
24 private IMessageWriter _messageWriter;
25
26 public Worker(IMessageWriter? messageWriter = default)
27 {
28 this._messageWriter = messageWriter ?? throw new System.ArgumentNullException(nameof(messageWriter));
29 }
30}
1using Doc.LogCustomFramework;
2using Metalama.Documentation.Helpers.ConsoleApp;
3using System;
4
5namespace Doc.LogDefaultFramework;
6
7// Program entry point. Creates the host, configure dependencies, and runs it.
8public static class Program
9{
10 private static void Main()
11 {
12 var appBuilder = ConsoleApp.CreateBuilder();
Error CS0246: The type or namespace name 'ConsoleMain' could not be found (are you missing a using directive or an assembly reference?)
13 appBuilder.Services.AddConsoleMain<ConsoleMain>();
14 using var app = appBuilder.Build();
15 app.Run();
16 }
17}
18
19// Definition of the interface consumed by the aspect.
20public interface IMessageWriter
21{
22 void Write( string message );
23}
24
25// Implementation actually consumed by the aspect.
26public class MessageWriter : IMessageWriter
27{
28 public void Write( string message )
29 {
30 Console.WriteLine( message );
31 }
32}
Worker.ExecuteAsync(CancellationToken) started. Hello, world. Worker.ExecuteAsync(CancellationToken) completed.
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#pragma warning disable CS0649, CS8618
6
7[assembly:
8 AspectOrder( AspectOrderDirection.RunTime, typeof(LogAttribute), typeof(DependencyAttribute) )]
9
10namespace Doc.LogServiceLocator;
11
12// Our logging aspect.
13public class LogAttribute : OverrideMethodAspect
14{
15 // Defines the dependency consumed by the aspect. It will be handled initialized from a service locator,
16 // but note that the aspect does not need to know the implementation details of the dependency injection framework.
17 [IntroduceDependency]
18 private readonly IMessageWriter _messageWriter;
19
20 public override dynamic? OverrideMethod()
21 {
22 try
23 {
24 this._messageWriter.Write( $"{meta.Target.Method} started." );
25
26 return meta.Proceed();
27 }
28 finally
29 {
30 this._messageWriter.Write( $"{meta.Target.Method} completed." );
31 }
32 }
33}
1using System;
2using System.Threading.Tasks;
3
4namespace Doc.LogServiceLocator;
5
6// The class using the Log aspect. This class is NOT instantiated by any dependency injection container.
7public class Worker
8{
9 [Log]
10 public Task ExecuteAsync()
11 {
12 Console.WriteLine( "Hello, world." );
13
14 return Task.CompletedTask;
15 }
16}
1using System;
2using System.Threading.Tasks;
3using Metalama.Extensions.DependencyInjection.ServiceLocator;
4
5namespace Doc.LogServiceLocator;
6
7// The class using the Log aspect. This class is NOT instantiated by any dependency injection container.
8public class Worker
9{
10 [Log]
11 public Task ExecuteAsync()
12 {
13 try
14 {
15 _messageWriter.Write("Worker.ExecuteAsync() started.");
16 Console.WriteLine("Hello, world.");
17
18 return Task.CompletedTask;
19 }
20 finally
21 {
22 _messageWriter.Write("Worker.ExecuteAsync() completed.");
23 }
24 }
25
26 private IMessageWriter _messageWriter;
27
28 public Worker()
29 {
30 _messageWriter = (IMessageWriter)ServiceProviderProvider.ServiceProvider().GetService(typeof(IMessageWriter)) ?? throw new InvalidOperationException("The service 'IMessageWriter' could not be obtained from the service locator.");
31 }
32}
1using Metalama.Extensions.DependencyInjection.ServiceLocator;
2using Microsoft.Extensions.DependencyInjection;
3using System;
4using System.Threading.Tasks;
5
6namespace Doc.LogServiceLocator;
7
8// The program entry point.
9public static class Program
10{
11 private static Task Main()
12 {
13 // Creates a service collection, add the service, and build a service provider.
14 var serviceCollection = new ServiceCollection();
15 serviceCollection.AddSingleton<IMessageWriter>( new MessageWriter() );
16 var serviceProvider = serviceCollection.BuildServiceProvider();
17
18 // Assigns the service provider to the global service locator.
19 ServiceProviderProvider.ServiceProvider = () => serviceProvider;
20
21 // Executes the program.
22 return new Worker().ExecuteAsync();
23 }
24}
25
26// Definition of the interface consumed by the aspect.
27public interface IMessageWriter
28{
29 void Write( string message );
30}
31
32// Implementation of the interface actually used by the aspect.
33public class MessageWriter : IMessageWriter
34{
35 public void Write( string message )
36 {
37 Console.WriteLine( message );
38 }
39}
Worker.ExecuteAsync() started. Hello, world. Worker.ExecuteAsync() completed.
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:
- Add a ProjectFabric or NamespaceFabric as described in Configuring aspects with fabrics.
- From the AmendProject or AmendNamespace method, call the amender.Outgoing.ConfigureDependencyInjection method. Supply the empty delegate
builder => {}
as an argument to this method. - From this delegate, do one of the following things:
- Call the SetFrameworkPriority method, to change the priority of a given framework (lower values win), or
- Set the builder.Selector property to your own implementation of IDependencyInjectionFrameworkSelector interface.
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:
- Create a class library project that targets
netstandard2.0
. - Add a reference to the
Metalama.Extensions.DependencyInjection
package. - 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.
- 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#pragma warning disable CS0649, CS8618
7
8[assembly:
9 AspectOrder( AspectOrderDirection.RunTime, typeof(LogAttribute), typeof(DependencyAttribute) )]
10
11namespace Doc.LogCustomFramework;
12
13// Our logging aspect.
14public class LogAttribute : OverrideMethodAspect
15{
16 // Defines the dependency consumed by the aspect. It will be handled by LoggerDependencyInjectionFramework.
17 // Note that the aspect does not need to know the implementation details of the dependency injection framework.
18 [IntroduceDependency]
19 private readonly ILogger _logger;
20
21 public override dynamic? OverrideMethod()
22 {
23 try
24 {
25 this._logger.LogWarning( $"{meta.Target.Method} started." );
26
27 return meta.Proceed();
28 }
29 finally
30 {
31 this._logger.LogWarning( $"{meta.Target.Method} completed." );
32 }
33 }
34}
1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Fabrics;
3
4namespace Doc.LogCustomFramework;
5#pragma warning disable CS0649, CS8618
6public class Fabric : ProjectFabric
7{
8 public override void AmendProject( IProjectAmender amender )
9 {
10 amender.ConfigureDependencyInjection(
11 dependencyInjection
12 => dependencyInjection.RegisterFramework<LoggerDependencyInjectionFramework>() );
13 }
14}
1using Metalama.Extensions.DependencyInjection;
2using Metalama.Extensions.DependencyInjection.Implementation;
3using Metalama.Framework.Code;
4using Metalama.Framework.Diagnostics;
5using Microsoft.Extensions.Logging;
6
7namespace Doc.LogCustomFramework;
8
9public class LoggerDependencyInjectionFramework : DefaultDependencyInjectionFramework
10{
11 // Returns true if we want to handle this dependency, i.e. if is a dependency of type ILogger.
12 public override bool CanHandleDependency(
13 DependencyProperties properties,
14 in ScopedDiagnosticSink diagnostics )
15 {
16 return properties.DependencyType.Is( typeof(ILogger) );
17 }
18
19 // Return our own customized strategy.
20 protected override DefaultDependencyInjectionStrategy GetStrategy(
21 DependencyProperties properties )
22 {
23 return new InjectionStrategy( properties );
24 }
25
26 // Our customized injection strategy. Decides how to create the field or property.
27 // We actually have no customization except that we return a customized pull strategy instead of the default one.
28 private class InjectionStrategy : DefaultDependencyInjectionStrategy
29 {
30 public InjectionStrategy( DependencyProperties properties ) : base( properties ) { }
31
32 protected override IPullStrategy GetPullStrategy(
33 IFieldOrProperty introducedFieldOrProperty )
34 {
35 return new LoggerPullStrategy( this.Properties, introducedFieldOrProperty );
36 }
37 }
38
39 // Our customized pull strategy. Decides how to assign the field or property from the constructor.
40 private class LoggerPullStrategy : DefaultPullStrategy
41 {
42 public LoggerPullStrategy(
43 DependencyProperties properties,
44 IFieldOrProperty introducedFieldOrProperty ) : base(
45 properties,
46 introducedFieldOrProperty ) { }
47
48 // Returns the type of the required or created constructor parameter. We return ILogger<T> where T is the declaring type
49 // (The default behavior would return just ILogger).
50 protected override IType ParameterType
51 => ((INamedType) TypeFactory.GetType( typeof(ILogger<>) )).WithTypeArguments(
52 this.IntroducedFieldOrProperty.DeclaringType );
53 }
54}
1using Metalama.Documentation.Helpers.ConsoleApp;
2
3using Microsoft.Extensions.Logging;
4
5namespace Doc.LogCustomFramework;
6
7// The class using the Log aspect. This class is instantiated by the host builder and dependencies are automatically passed.
8public class ConsoleMain : IConsoleMain
9{
10 [Log]
11 public void Execute()
12 {
Error CS0103: The name '_logger' does not exist in the current context
13 _logger.LogInformation( "Hello, world." );
14 }
15}
1using Metalama.Documentation.Helpers.ConsoleApp;
2
3using Microsoft.Extensions.Logging;
4
5namespace Doc.LogCustomFramework;
6
7// The class using the Log aspect. This class is instantiated by the host builder and dependencies are automatically passed.
8public class ConsoleMain : IConsoleMain
9{
10 [Log]
11 public void Execute()
12 {
13 try
14 {
15 _logger.LogWarning("ConsoleMain.Execute() started.");
16 _logger.LogInformation("Hello, world.");
17 return;
18 }
19 finally
20 {
21 _logger.LogWarning("ConsoleMain.Execute() completed.");
22 }
23 }
24
25 private ILogger _logger;
26
27 public ConsoleMain(ILogger<ConsoleMain> logger = default)
28 {
29 this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
30 }
31}
ConsoleMain.Execute() started. Hello, world. ConsoleMain.Execute() completed.
1using Metalama.Documentation.Helpers.ConsoleApp;
2
3namespace Doc.LogCustomFramework;
4
5// Program entry point. Creates the host, configure dependencies, and runs it.
6public static class Program
7{
8 private static void Main()
9 {
10 var builder = ConsoleApp.CreateBuilder();
11 builder.Services.AddConsoleMain<ConsoleMain>();
12 using var app = builder.Build();
13 app.Run();
14 }
15}