Metalama 1.0 / / Metalama Documentation / Conceptual Documentation / Creating Aspects / Injecting Dependencies Into Aspects

Injecting Dependencies Into Aspects

Many aspects need to consume services that must be injected from a dependency injection container. For instance, a caching aspect may depend on the IMemoryCache service. If you are using the Microsoft.Extensions.DependencyInjection framework, your aspect will need to 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 have 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 in use for that project. As we have seen, the default .NET Core framework require a constructor parameter, but other frameworks may use an [Import] or [Inject] custom attribute.

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

Enter the Metalama.Framework.DependencyInjection project. 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 implemented by the implementation of the IDependencyInjectionFramework interface, which is chosen by the user project.

The Metalama.Framework.DependencyInjection namespace is an open source hosted on GitHub. It currently has implementations for the following dependency injection framework:

The Metalama.Framework.DependencyInjection project is designed to make it easy to implement other dependency injection frameworks.

Consuming dependencies from your aspect

To consume a dependency from an aspect:

  1. Add the Metalama.Framework.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.
  4. Use this field or property from any template member of your aspect.

Example: default dependency injection patterns

The following example uses the Microsoft.Extensions.Hosting, typical to .NET Core applications, to build an application and inject services. The Program.Main method builds the host, which 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.

using Metalama.Framework.Aspects;
using Metalama.Framework.DependencyInjection;

#pragma warning disable CS0649, CS8618

[assembly: AspectOrder( typeof(Doc.LogDefaultFramework.LogAttribute), typeof(DependencyAttribute))] 

namespace Doc.LogDefaultFramework
{
    // Our logging aspect.
    public class LogAttribute : OverrideMethodAspect
    {
        // Defines the dependency consumed by the aspect. It will be handled by the dependency injection framework configured for the current project.
        // By default, this is the .NET Core system one, which pulls dependencies from the constructor.
        [IntroduceDependency]
        private readonly IMessageWriter _messageWriter;

        public override dynamic? OverrideMethod()
        {
            try
            {
                this._messageWriter.Write( $"{meta.Target.Method} started." );

                return meta.Proceed();
            }
            finally
            {
                this._messageWriter.Write( $"{meta.Target.Method} completed." );
            }

        }
    }


}
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Doc.LogDefaultFramework
{
    // The class using the Log aspect. This class is instantiated by the host builder and dependencies are automatically passed.
    public class Worker : BackgroundService
    {
        [Log]
        protected override Task ExecuteAsync( CancellationToken stoppingToken )
        {
            Console.WriteLine( "Hello, world." );
            return Task.CompletedTask;
        }
    }

}
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace Doc.LogDefaultFramework
{
    // Program entry point. Creates the host, configure dependencies, and runs it.
    public static class Program
    {
        private static Task Main() =>
            CreateHostBuilder( Array.Empty<string>() ).Build().RunAsync();

        private static IHostBuilder CreateHostBuilder( string[] args ) =>
            Host.CreateDefaultBuilder( args )
                .ConfigureServices( ( _, services ) =>
                    services.AddHostedService<Worker>()
                            .AddScoped<IMessageWriter, MessageWriter>() )
                                        .ConfigureLogging( loggingBuilder => loggingBuilder.ClearProviders() );

    }

    // Definition of the interface consumed by the aspect.
    public interface IMessageWriter
    {
        void Write( string message );
    }

    // Implementation actually consumed by the aspect.
    public class MessageWriter : IMessageWriter
    {
        public void Write( string message )
        {
            Console.WriteLine( message );
        }
    }
}
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Doc.LogDefaultFramework
{
    // The class using the Log aspect. This class is instantiated by the host builder and dependencies are automatically passed.
    public class Worker : BackgroundService
    {
        [Log]
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            try
            {
                this._messageWriter.Write($"Worker.ExecuteAsync(CancellationToken) started.");
                Console.WriteLine("Hello, world.");
                return Task.CompletedTask;
            }
            finally
            {
                this._messageWriter.Write($"Worker.ExecuteAsync(CancellationToken) completed.");
            }
        }

        private IMessageWriter _messageWriter;

        public Worker(IMessageWriter? messageWriter = default)
        {
            this._messageWriter = messageWriter ?? throw new System.ArgumentNullException(nameof(messageWriter));
        }
    }

}
Worker.ExecuteAsync(CancellationToken) started.
Hello, world.
Worker.ExecuteAsync(CancellationToken) completed.

Example: ServiceLocator

What follows is the same example as the previous one, but using the ServiceLocator pattern instead of pulling dependencies from the constructor.

using Metalama.Framework.Aspects;
using Metalama.Framework.DependencyInjection;

#pragma warning disable CS0649, CS8618

[assembly: AspectOrder( typeof(Doc.LogServiceLocator.LogAttribute), typeof(DependencyAttribute))] 

namespace Doc.LogServiceLocator
{

    // Our logging aspect.
    public class LogAttribute : OverrideMethodAspect
    {

        // Defines the dependency consumed by the aspect. It will be handled initialized from a service locator,
        // but note that the aspect does not need to know the implementation details of the dependency injection framework.
        [IntroduceDependency]
        private readonly IMessageWriter _messageWriter;

        public override dynamic? OverrideMethod()
        {
            try
            {
                this._messageWriter.Write( $"{meta.Target.Method} started." );

                return meta.Proceed();
            }
            finally
            {
                this._messageWriter.Write( $"{meta.Target.Method} completed." );
            }

        }
    }


}
using System;
using System.Threading.Tasks;

namespace Doc.LogServiceLocator
{
    // The class using the Log aspect. This class is NOT instantiated by any dependency injection container.
    public class Worker 
    {
        [Log]
        public Task ExecuteAsync( )
        {
            Console.WriteLine( "Hello, world." );
            return Task.CompletedTask;
        }
    }

}
using Metalama.Framework.DependencyInjection.ServiceLocator;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

namespace Doc.LogServiceLocator
{
    // The program entry point.
    public static class Program
    {
        private static Task Main()
        {
            // Creates a service collection, add the service, and build a service provider.
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddSingleton<IMessageWriter>(new MessageWriter());
            var serviceProvider = serviceCollection.BuildServiceProvider();

            // Assigns the service provider to the global service locator.
            ServiceProviderProvider.ServiceProvider = () => serviceProvider;

            // Executes the program.
            return new Worker().ExecuteAsync();
        }

    }

    // Definition of the interface consumed by the aspect.
    public interface IMessageWriter
    {
        void Write( string message );
    }

    // Implementation of the interface actually used by the aspect.
    public class MessageWriter : IMessageWriter
    {
        public void Write( string message )
        {
            Console.WriteLine( message );
        }
    }
}
using System;
using System.Threading.Tasks;
using Metalama.Framework.DependencyInjection.ServiceLocator;

namespace Doc.LogServiceLocator
{
    // The class using the Log aspect. This class is NOT instantiated by any dependency injection container.
    public class Worker
    {
        [Log]
        public Task ExecuteAsync()
        {
            try
            {
                this._messageWriter.Write($"Worker.ExecuteAsync() started.");
                Console.WriteLine("Hello, world.");
                return Task.CompletedTask;
            }
            finally
            {
                this._messageWriter.Write($"Worker.ExecuteAsync() completed.");
            }
        }

        private IMessageWriter _messageWriter;

        public Worker()
        {
            this._messageWriter = (IMessageWriter)ServiceProviderProvider.ServiceProvider().GetService(typeof(IMessageWriter)) ?? throw new InvalidOperationException($"The service 'IMessageWriter' could not be obtained from the service locator.");
        }
    }

}
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, it is generally sufficient to add a reference to the package implementing this dependency framework i.e. for instance Metalama.Framework.DependencyInjection.ServiceLocator. These packages generally include a TransitiveProjectFabric that register themselves. This works well when there is a single dependency injection framework in the project.

When there are several dependency injection frameworks in a project, Metalama will call the DependencyInjectionOptions.Selector delegate. Its default implementation is to return the first eligible framework in the input list, i.e. the topmost in the RegisteredFrameworks list.

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

  1. Add a ProjectFabric to your project as described in Configuring Aspects using Project Fabrics.
  2. From the AmendProject method, call the amender.Project.DependencyInjectionOptions() method to access the options.
  3. Set the DependencyInjectionOptions.Selector property.

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.Framework.DependencyInjection.ServiceLocator on GitHub for a working example.

The steps are the following:

  1. Create a class library project that targets netstandard2.0.
  2. Add a reference to the Metalama.Framework.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 will also need to override the DefaultDependencyInjectionStrategy class. See the source code and the class documentation for details.
  4. Optionally create a TransitiveProjectFabric that registers the framework in amender.Project.DependencyInjectionOptions() using the RegisterFramework method.

Example

The following example shows how to implement the right code generation pattern for the ILogger service under .NET Core. Unlike normal services which 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.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.DependencyInjection;
using Metalama.Framework.DependencyInjection.Implementation;
using Metalama.Framework.Fabrics;
using Microsoft.Extensions.Logging;

#pragma warning disable CS0649, CS8618

[assembly: AspectOrder( typeof(Doc.LogCustomFramework.LogAttribute), typeof(DependencyAttribute))] 

namespace Doc.LogCustomFramework
{
    public class LoggerDependencyInjectionFramework : DefaultDependencyInjectionFramework
    {
        // Returns true if we want to handle this dependency, i.e. if is a dependency of type ILogger.
        public override bool CanHandleDependency( DependencyContext context )
        {
            return context.FieldOrProperty.Type.Is( typeof( ILogger ) );
        }

        // Return our own customized strategy.
        protected override DefaultDependencyInjectionStrategy GetStrategy( DependencyContext context )
        {
            return new InjectionStrategy( context );
        }

        // Our customized injection strategy. Decides how to create the field or property.
        // We actually have no customization except that we return a customized pull strategy instead of the default one.
        private class InjectionStrategy : DefaultDependencyInjectionStrategy
        {
            public InjectionStrategy( DependencyContext context ) : base( context )
            {
            }

            protected override IPullStrategy GetPullStrategy( IFieldOrProperty introducedFieldOrProperty )
            {
                return new LoggerPullStrategy( this.Context, introducedFieldOrProperty );
            }
        }

        // Our customized pull strategy. Decides how to assign the field or property from the constructor.
        private class LoggerPullStrategy : DefaultPullStrategy
        {
            public LoggerPullStrategy( DependencyContext context, IFieldOrProperty introducedFieldOrProperty ) : base( context, introducedFieldOrProperty )
            {
            }

            // Returns the type of the required or created constructor parameter. We return ILogger<T> where T is the declaring type
            // (The default behavior would return just ILogger).
            protected override IType ParameterType => 
                ((INamedType) TypeFactory.GetType( typeof(ILogger<>) )).ConstructGenericInstance( this.IntroducedFieldOrProperty.DeclaringType );
        }
    }

    // A project fabric that registers our framework.
    public class Fabric : ProjectFabric
    {
        public override void AmendProject( IProjectAmender amender )
        {
            amender.Project.DependencyInjectionOptions().RegisterFramework( new LoggerDependencyInjectionFramework() );
        }
    }

    // Our logging aspect.
    public class LogAttribute : OverrideMethodAspect
    {
        // Defines the dependency consumed by the aspect. It will be handled by LoggerDependencyInjectionFramework.
        // Note that the aspect does not need to know the implementation details of the dependency injection framework.
        [IntroduceDependency]
        private readonly ILogger _logger;

        public override dynamic? OverrideMethod()
        {
            try
            {
                this._logger.LogWarning( $"{meta.Target.Method} started." );

                return meta.Proceed();
            }
            finally
            {
                this._logger.LogWarning( $"{meta.Target.Method} completed." );
            }

        }
    }


}
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Doc.LogCustomFramework
{
    // The class using the Log aspect. This class is instantiated by the host builder and dependencies are automatically passed.
    public class Worker : BackgroundService
    {
        [Log]
        protected override Task ExecuteAsync( CancellationToken stoppingToken )
        {
            Console.WriteLine( "Hello, world." );
            return Task.CompletedTask;
        }
    }

}
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace Doc.LogCustomFramework
{
    // Program entry point. Creates the host, configure dependencies, and runs it.
    public static class Program
    {
        private static Task Main() =>
            CreateHostBuilder( Array.Empty<string>() ).Build().RunAsync();

        private static IHostBuilder CreateHostBuilder( string[] args ) =>
           Host.CreateDefaultBuilder( args )
               .ConfigureServices( ( _, services ) =>
                   services.AddHostedService<Worker>() )
                                       .ConfigureLogging( loggingBuilder => loggingBuilder.AddFilter( "Microsoft.Hosting.Lifetime", LogLevel.None )  );


    }

}
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Doc.LogCustomFramework
{
    // The class using the Log aspect. This class is instantiated by the host builder and dependencies are automatically passed.
    public class Worker : BackgroundService
    {
        [Log]
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            try
            {
                LoggerExtensions.LogWarning(this._logger, $"Worker.ExecuteAsync(CancellationToken) started.");
                Console.WriteLine("Hello, world.");
                return Task.CompletedTask;
            }
            finally
            {
                LoggerExtensions.LogWarning(this._logger, $"Worker.ExecuteAsync(CancellationToken) completed.");
            }
        }

        private ILogger _logger;

        public Worker(ILogger<Worker> logger = default)
        {
            this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
        }
    }

}
warn: Doc.LogCustomFramework.Worker[0]
      Worker.ExecuteAsync(CancellationToken) started.
Hello, world.
warn: Doc.LogCustomFramework.Worker[0]
      Worker.ExecuteAsync(CancellationToken) completed.