Open sandboxFocusImprove this doc

Creating a custom DI framework adapter

The default dependency injection implementation in Metalama.Extensions.DependencyInjection pulls dependencies from constructor parameters, which works well with Microsoft.Extensions.DependencyInjection. However, you might need a different approach for:

  • Legacy DI frameworks that use attribute-based injection (like older versions of Unity, Ninject, or Autofac)
  • Service locator patterns where dependencies are resolved from a global provider
  • Custom resolution logic specific to your application

Architecture overview

The IDependencyInjectionFramework interface is the main abstraction of a DI framework adapter.

The default implementation of this interface, handling the Microsoft.Extensions.DependencyInjection injection pattern, is provided by the DefaultDependencyInjectionFramework class.

If you want to create a custom framework adapter, you can either implement IDependencyInjectionFramework or override DefaultDependencyInjectionFramework.

Implementing IDependencyInjectionFramework

For simple requirements, implement the IDependencyInjectionFramework interface directly.

To illustrate the process, we'll build a simple adapter that generates [Import] attributes on properties for use with an attribute-based DI container.

Step 1. Implement the framework interface

Create a class that implements IDependencyInjectionFramework. This interface has three methods:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Extensions.DependencyInjection.Implementation;
3using Metalama.Framework.Advising;
4using Metalama.Framework.Aspects;
5using Metalama.Framework.Code;
6using Metalama.Framework.Code.DeclarationBuilders;
7using Metalama.Framework.Diagnostics;
8
9namespace Doc.ImportAttributeFramework;
10
11// A DI framework adapter that adds [Import] attributes to properties
12// instead of pulling dependencies from constructor parameters.
13[CompileTime]
14public class ImportAttributeFramework : IDependencyInjectionFramework
15{
16    public bool CanHandleDependency( DependencyProperties properties, in ScopedDiagnosticSink diagnostics )
17    {
18        // Handle all non-static dependencies.
19        return !properties.IsStatic;
20    }
21
22    // Called when the programmatic IntroduceDependency adviser extension method is used.
23    public IntroduceDependencyResult IntroduceDependency( DependencyProperties properties, IAdviser<INamedType> adviser )
24    {
25        var importType = (INamedType) TypeFactory.GetType( typeof(ImportAttribute) );
26
27        // Introduce the property with the [Import] attribute.
28        var result = adviser.IntroduceAutomaticProperty(
29            properties.Name,
30            properties.DependencyType,
31            IntroductionScope.Instance,
32            OverrideStrategy.Ignore,
33            b =>
34            {
35                b.Accessibility = Accessibility.Public;
36                b.AddAttribute( AttributeConstruction.Create( importType ) );
37            } );
38
39        if ( result.Outcome != AdviceOutcome.Default )
40        {
41            return IntroduceDependencyResult.Ignore( result.Declaration );
42        }
43
44        return IntroduceDependencyResult.Success( result.Declaration );
45    }
46
47    // Called for the declarative scenario, i.e., when [Dependency] is applied to an existing field or property.
48    public bool TryImplementDependency( DependencyProperties properties, IAdviser<IFieldOrProperty> adviser )
49    {
50        // Add [Import] via the adviser.
51        var importType = (INamedType) TypeFactory.GetType( typeof(ImportAttribute) );
52
53        if ( !adviser.Target.Attributes.Any( importType ) )
54        {
55            adviser.IntroduceAttribute( AttributeConstruction.Create( importType ) );
56        }
57
58        return true;
59    }
60}
61

Step 2. Register the adapter

Register your adapter using a fabric. To automatically register when a package is referenced, use TransitiveProjectFabric. For project-specific registration, use ProjectFabric:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Fabrics;
3
4namespace Doc.ImportAttributeFramework;
5
6// Registers the custom framework with the DI system.
7internal class Fabric : ProjectFabric
8{
9    public override void AmendProject( IProjectAmender amender )
10    {
11        amender.ConfigureDependencyInjection(
12            builder => builder.RegisterFramework<ImportAttributeFramework>() );
13    }
14}
15

Step 3. Use the aspect

Now you can create aspects that use IntroduceDependencyAttribute and they will use your custom framework:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3using System;
4
5namespace Doc.ImportAttributeFramework;
6
7// A logging aspect that introduces a logger dependency using our custom framework.
8public class LogAttribute : OverrideMethodAspect
9{
10    [IntroduceDependency]
11    private readonly ILogger _logger;
12
13    public override dynamic? OverrideMethod()
14    {
15        _logger.Log( $"Entering {meta.Target.Method}" );
16
17        try
18        {
19            return meta.Proceed();
20        }
21        finally
22        {
23            _logger.Log( $"Leaving {meta.Target.Method}" );
24        }
25    }
26}
27

Result (simple IDependencyInjectionFramework)

When you apply the aspect to a class:

Source Code
1using System;
2
3namespace Doc.ImportAttributeFramework;
4
5// A class using the aspect.
6public partial class Greeter
7{
8    [Log]
9    public void SayHello() => Console.WriteLine( "Hello!" );
10}
11
Transformed Code
1using System;
2
3namespace Doc.ImportAttributeFramework;
4
5// A class using the aspect.
6public partial class Greeter
7{
8    [Log]
9    public void SayHello()
10    {
11        _logger.Log("Entering Greeter.SayHello()");
12        try
13        {
14            Console.WriteLine("Hello!");
15            return;
16        }
17        finally
18        {
19            _logger.Log("Leaving Greeter.SayHello()");
20        }
21    }
22
23    private ILogger _logger1 = default!;
24
25    [Import]
26    public ILogger _logger
27    {
28        get
29        {
30            return _logger1;
31        }
32
33        set
34        {
35            _logger1 = value;
36        }
37    }
38}
39

Extending DefaultDependencyInjectionFramework

If your DI framework also uses constructor injection but requires customizations to how dependencies are resolved, extend DefaultDependencyInjectionFramework and override the GetStrategy method to return custom strategy implementations.

Example: Factory-based injection

This example shows a custom adapter for services requiring factory-based creation. Instead of injecting the service directly, it injects IServiceFactory<T> and calls Create() to instantiate the service.

First, define the factory interfaces:

1using System;
2
3namespace Doc.FactoryFramework;
4
5// Base interface for services that are created by factories.
6public interface IFactoredService { }
7
8// Factory interface that creates instances of factored services.
9public interface IServiceFactory<out T>
10    where T : IFactoredService
11{
12    T Create();
13}
14
15// Example factored service interface.
16public interface ILogger : IFactoredService
17{
18    void Log( string message );
19}
20
21// Example factory implementation (would be registered in DI container).
22public class LoggerFactory : IServiceFactory<ILogger>
23{
24    public ILogger Create() => new ConsoleLogger();
25}
26
27// Example logger implementation.
28public class ConsoleLogger : ILogger
29{
30    public void Log( string message ) => Console.WriteLine( message );
31}
32

Step 1. Implement the framework

Extend DefaultDependencyInjectionFramework and override CanHandleDependency to handle only types implementing IFactoredService. Override GetStrategy to return a custom strategy:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Extensions.DependencyInjection.Implementation;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Code;
5using Metalama.Framework.Diagnostics;
6
7namespace Doc.FactoryFramework;
8
9// A DI framework adapter that handles IFactoredService dependencies
10// by pulling the factory from the constructor and calling Create().
11[CompileTime]
12public class FactoryDependencyInjectionFramework : DefaultDependencyInjectionFramework
13{
14    // Only handle types that implement IFactoredService.
15    public override bool CanHandleDependency( DependencyProperties properties, in ScopedDiagnosticSink diagnostics )
16    {
17        return properties.DependencyType is INamedType namedType
18               && namedType.IsConvertibleTo( typeof(IFactoredService) );
19    }
20
21    protected override DefaultDependencyInjectionStrategy GetStrategy( DependencyProperties properties )
22        => properties.IsLazy
23            ? new FactoryLazyDependencyInjectionStrategy( properties )
24            : new FactoryEagerDependencyInjectionStrategy( properties );
25}
26

Step 2. Implement the strategy

The strategy extends DefaultDependencyInjectionStrategy and returns a custom pull strategy that changes how constructor parameters are generated:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Extensions.DependencyInjection.Implementation;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Code;
5using Metalama.Framework.Code.SyntaxBuilders;
6
7namespace Doc.FactoryFramework;
8
9// Eager strategy: calls factory.Create() immediately in the constructor.
10[CompileTime]
11internal class FactoryEagerDependencyInjectionStrategy : DefaultDependencyInjectionStrategy
12{
13    public FactoryEagerDependencyInjectionStrategy( DependencyProperties properties ) : base( properties ) { }
14
15    protected override IDependencyPullStrategy GetDependencyPullStrategy( IFieldOrProperty introducedFieldOrProperty )
16        => new FactoryEagerPullStrategy( this.Properties, introducedFieldOrProperty );
17}
18
19// Pull strategy for eager initialization: pulls IServiceFactory<T> and calls Create() in constructor.
20[CompileTime]
21internal class FactoryEagerPullStrategy : DefaultDependencyPullStrategy
22{
23    private readonly INamedType _factoryType;
24    private readonly IFieldOrProperty _fieldOrProperty;
25
26    public FactoryEagerPullStrategy( DependencyProperties properties, IFieldOrProperty fieldOrProperty )
27        : base( properties, fieldOrProperty )
28    {
29        _fieldOrProperty = fieldOrProperty;
30        _factoryType = TypeFactory.GetNamedType( typeof(IServiceFactory<>) )
31            .WithTypeArguments( properties.DependencyType );
32    }
33
34    // Use IServiceFactory<T> as the parameter type instead of T.
35    protected override IType ParameterType => _factoryType;
36
37    // Call factory.Create() in constructor.
38    public override IStatement GetAssignmentStatement( IParameter existingParameter )
39    {
40        var assignmentCode = $"this.{_fieldOrProperty.Name} = {existingParameter.Name}.Create();";
41
42        return StatementFactory.Parse( assignmentCode );
43    }
44}
45

The key customizations are:

  • ParameterType: Returns IServiceFactory<T> instead of T directly, so the constructor receives the factory.
  • GetAssignmentStatement: Generates this._logger = factory.Create(); instead of direct assignment.

Step 3. Register and use

Register the framework in a fabric:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Fabrics;
3
4namespace Doc.FactoryFramework;
5
6internal class Fabric : ProjectFabric
7{
8    public override void AmendProject( IProjectAmender amender )
9    {
10        amender.ConfigureDependencyInjection(
11            builder => builder.RegisterFramework<FactoryDependencyInjectionFramework>() );
12    }
13}
14

Use IntroduceDependencyAttribute in an aspect:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3
4namespace Doc.FactoryFramework;
5
6// A logging aspect that introduces a logger dependency.
7// The custom framework will pull IServiceFactory<ILogger> and call Create().
8public class LogAttribute : OverrideMethodAspect
9{
10    [IntroduceDependency]
11    private readonly ILogger _logger;
12
13    public override dynamic? OverrideMethod()
14    {
15        _logger.Log( $"Entering {meta.Target.Method}" );
16
17        try
18        {
19            return meta.Proceed();
20        }
21        finally
22        {
23            _logger.Log( $"Leaving {meta.Target.Method}" );
24        }
25    }
26}
27

Result (eager loading)

When applied to a class, the framework injects the factory and calls Create():

Source Code
1using System;
2
3namespace Doc.FactoryFramework;

4
5// A class using the logging aspect.
6// The factory framework will pull IServiceFactory<ILogger> and call Create().
7public partial class Greeter
8{
9    [Log]
10    public void SayHello() => Console.WriteLine( "Hello!" );
11}
12
Transformed Code
1using System;
2using Metalama.Framework.RunTime;
3
4namespace Doc.FactoryFramework;
5
6// A class using the logging aspect.
7// The factory framework will pull IServiceFactory<ILogger> and call Create().
8public partial class Greeter
9{
10    [Log]
11    public void SayHello()
12    {
13        _logger.Log("Entering Greeter.SayHello()");
14        try
15        {
16            Console.WriteLine("Hello!");
17            return;
18        }
19        finally
20        {
21            _logger.Log("Leaving Greeter.SayHello()");
22        }
23    }
24
25    private ILogger _logger;
26
27    public Greeter([AspectGenerated] IServiceFactory<ILogger> logger = null)
28    {
29        this._logger = logger.Create();
30    }
31}
32

Lazy initialization

For lazy initialization where Create() is called on first access instead of in the constructor, you need to:

  1. Check the IsLazy property in your framework's GetStrategy method
  2. Implement a custom strategy that introduces a property with a lazy getter

The lazy strategy is more complex because it needs to:

  • Introduce a Func<IServiceFactory<T>> field to store the factory getter
  • Introduce a cache field for the created instance
  • Introduce a property that calls factory().Create() on first access

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Extensions.DependencyInjection.Implementation;
3using Metalama.Framework.Advising;
4using Metalama.Framework.Aspects;
5using Metalama.Framework.Code;
6using System;
7
8namespace Doc.FactoryFrameworkLazy;
9
10// Lazy strategy: stores Func<IServiceFactory<T>> and calls factory.Create() on first access.
11[CompileTime]
12internal partial class FactoryLazyDependencyInjectionStrategy : DefaultDependencyInjectionStrategy, ITemplateProvider
13{
14    private readonly INamedType _factoryType;
15
16    public FactoryLazyDependencyInjectionStrategy( DependencyProperties properties ) : base( properties )
17    {
18        _factoryType = TypeFactory.GetNamedType( typeof(IServiceFactory<>) )
19            .WithTypeArguments( properties.DependencyType );
20    }
21
22    public override IntroduceDependencyResult IntroduceDependency( IAdviser<INamedType> adviser )
23    {
24        // Introduce a field that stores Func<IServiceFactory<T>>.
25        var funcType = TypeFactory.GetNamedType( typeof(Func<>) ).WithTypeArguments( _factoryType );
26
27        var funcFieldResult = adviser.IntroduceField(
28            this.Properties.Name + "Func",
29            funcType );
30
31        if ( funcFieldResult.Outcome == AdviceOutcome.Error )
32        {
33            return IntroduceDependencyResult.Error;
34        }
35
36        // Introduce a field that caches the created service.
37        var cacheFieldResult = adviser.IntroduceField(
38            this.Properties.Name + "Cache",
39            this.Properties.DependencyType.ToNullable() );
40
41        if ( cacheFieldResult.Outcome == AdviceOutcome.Error )
42        {
43            return IntroduceDependencyResult.Error;
44        }
45
46        // Introduce the property with a getter that calls Create() on first access.
47        var introducePropertyResult = adviser
48            .WithTemplateProvider( this )
49            .IntroduceProperty(
50                this.Properties.Name,
51                nameof(GetDependencyTemplate),
52                null,
53                IntroductionScope.Instance,
54                OverrideStrategy.Ignore,
55                propertyBuilder =>
56                {
57                    propertyBuilder.Type = this.Properties.DependencyType;
58                    propertyBuilder.Name = this.Properties.Name;
59                },
60                args: new { cacheField = cacheFieldResult.Declaration, factoryFuncField = funcFieldResult.Declaration } );
61
62        switch ( introducePropertyResult.Outcome )
63        {
64            case AdviceOutcome.Ignore:
65                return IntroduceDependencyResult.Ignore( introducePropertyResult.Declaration );
66
67            case AdviceOutcome.Error:
68                return IntroduceDependencyResult.Error;
69        }
70
71        var pullStrategy = new InjectionStrategy( this.Properties, introducePropertyResult.Declaration, funcFieldResult.Declaration, _factoryType );
72
73        if ( !this.TryPullDependency( adviser, funcFieldResult.Declaration, pullStrategy ) )
74        {
75            return IntroduceDependencyResult.Error;
76        }
77
78        return IntroduceDependencyResult.Success( introducePropertyResult.Declaration );
79    }
80
81    // Template: returns cached value or calls factory.Create() and caches.
82    [Template]
83    private static dynamic? GetDependencyTemplate( IField cacheField, IField factoryFuncField )
84        => cacheField.Value ??= factoryFuncField.Value!.Invoke().Create();
85
86    // Pull strategy that uses Func<IServiceFactory<T>> as the parameter type.
87    private sealed class InjectionStrategy : DefaultDependencyPullStrategy
88    {
89        private readonly IField _funcField;
90        private readonly INamedType _funcType;
91
92        protected override IType ParameterType => this._funcType;
93
94        public InjectionStrategy( DependencyProperties properties, IProperty mainProperty, IField funcField, INamedType factoryType )
95            : base( properties, mainProperty )
96        {
97            this._funcField = funcField;
98            this._funcType = TypeFactory.GetNamedType( typeof(Func<>) ).WithTypeArguments( factoryType ).ToNullable();
99        }
100
101        protected override IFieldOrProperty AssignedFieldOrProperty => this._funcField;
102    }
103}
104

Then use the aspect with IsLazy set to true:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3
4namespace Doc.FactoryFrameworkLazy;
5
6// A logging aspect that introduces a lazy logger dependency.
7// The custom framework will store Func<IServiceFactory<ILogger>> and call Create() on first access.
8public class LogLazyAttribute : OverrideMethodAspect
9{
10    [IntroduceDependency( IsLazy = true )]
11    private readonly ILogger _logger;
12
13    public override dynamic? OverrideMethod()
14    {
15        _logger.Log( $"Entering {meta.Target.Method}" );
16
17        try
18        {
19            return meta.Proceed();
20        }
21        finally
22        {
23            _logger.Log( $"Leaving {meta.Target.Method}" );
24        }
25    }
26}
27

Result (lazy loading)

When applied to a class, the framework defers Create() until first property access:

Source Code
1using System;
2
3namespace Doc.FactoryFrameworkLazy;

4
5// A class using the lazy logging aspect.
6// The factory framework will lazily call Create() on first property access.
7public partial class LazyGreeter
8{
9    [LogLazy]
10    public void SayHello() => Console.WriteLine( "Hello!" );
11}
12
Transformed Code
1using System;
2using Metalama.Framework.RunTime;
3
4namespace Doc.FactoryFrameworkLazy;
5
6// A class using the lazy logging aspect.
7// The factory framework will lazily call Create() on first property access.
8public partial class LazyGreeter
9{
10    [LogLazy]
11    public void SayHello()
12    {
13        _logger.Log("Entering LazyGreeter.SayHello()");
14        try
15        {
16            Console.WriteLine("Hello!");
17            return;
18        }
19        finally
20        {
21            _logger.Log("Leaving LazyGreeter.SayHello()");
22        }
23    }
24
25    private ILogger? _loggerCache;
26    private Func<IServiceFactory<ILogger>> _loggerFunc;
27
28    public LazyGreeter([AspectGenerated] Func<IServiceFactory<ILogger>>? logger = null)
29    {
30        this._loggerFunc = logger ?? throw new System.ArgumentNullException(nameof(logger));
31    }
32
33    private ILogger _logger
34    {
35        get
36        {
37            return _loggerCache ??= _loggerFunc.Invoke().Create();
38        }
39    }
40}
41

Framework priority

When multiple adapters are registered, Metalama selects one based on priority, where lower numbers indicate higher priority. Built-in frameworks use priorities 100–101, while user-registered frameworks default to priority 0 (highest priority). To set a specific priority, use RegisterFramework and supply a value for the priority parameter.