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:
- 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[assembly:
6 AspectOrder( AspectOrderDirection.RunTime, typeof(LogAttribute), typeof(DependencyAttribute) )]
7
8namespace Doc.LogDefaultFramework;
9
10
11public class LogAttribute : OverrideMethodAspect
12{
13
14
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}
1using Metalama.Documentation.Helpers.ConsoleApp;
2using System;
3
4namespace Doc.LogDefaultFramework;
5
6
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
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 = null)
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
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
20public interface IMessageWriter
21{
22 void Write( string message );
23}
24
25
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[assembly:
6 AspectOrder( AspectOrderDirection.RunTime, typeof(LogAttribute), typeof(DependencyAttribute) )]
7
8namespace Doc.LogServiceLocator;
9
10
11public class LogAttribute : OverrideMethodAspect
12{
13
14
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}
1using System;
2using System.Threading.Tasks;
3
4namespace Doc.LogServiceLocator;
5
6
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
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
9public static class Program
10{
11 private static Task Main()
12 {
13
14 var serviceCollection = new ServiceCollection();
15 serviceCollection.AddSingleton<IMessageWriter>( new MessageWriter() );
16 var serviceProvider = serviceCollection.BuildServiceProvider();
17
18
19 ServiceProviderProvider.ServiceProvider = () => serviceProvider;
20
21
22 return new Worker().ExecuteAsync();
23 }
24}
25
26
27public interface IMessageWriter
28{
29 void Write( string message );
30}
31
32
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:
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[assembly:
7 AspectOrder( AspectOrderDirection.RunTime, typeof(LogAttribute), typeof(DependencyAttribute) )]
8
9namespace Doc.LogCustomFramework;
10
11
12public class LogAttribute : OverrideMethodAspect
13{
14
15
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}
1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Fabrics;
3
4namespace Doc.LogCustomFramework;
5
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
12 public override bool CanHandleDependency(
13 DependencyProperties properties,
14 in ScopedDiagnosticSink diagnostics )
15 {
16 return properties.DependencyType.IsConvertibleTo( typeof(ILogger) );
17 }
18
19
20 protected override DefaultDependencyInjectionStrategy GetStrategy(
21 DependencyProperties properties )
22 {
23 return new InjectionStrategy( properties );
24 }
25
26
27
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
40 private class LoggerPullStrategy : DefaultPullStrategy
41 {
42 public LoggerPullStrategy(
43 DependencyProperties properties,
44 IFieldOrProperty introducedFieldOrProperty ) : base(
45 properties,
46 introducedFieldOrProperty ) { }
47
48
49
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
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
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 = null)
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
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}