⚠ Deprecated: Legacy
A featherweight dependency injector.
$ dotnet add package Injector.NETA featherweight dependency injector written in C#.
| Package name | NuGet link |
|---|---|
| Injector.NET | |
| Injector.NET.Interfaces |
This documentation refers the version 10.X of the library (which is a complete reimplementation of the library)
Dependency Injection is a design pattern that helps you separate the dependencies of your code from its behavior. Additionaly it makes the code easy to test by let you mock the dependencies in your unit tests.
Singleton, Transient, Scoped, Pooled, InstanceThey are class instances (most probably imlementing a particular interface).
They are declared in a IServiceCollection.
They are provided by an IInjector.
Every service can be requested multiple times.
Producible services are instantiated only when they are requested.
Every producible service has its own lifetime, which can be:
Singleton:
Scoped:
Transient:
Pooled:
As you can see you should never free producible services manually.
Since Singleton and Instance services may be accessed parallelly they (and their dependencies) have to be thread safe.
Points from 1 to 3 are done in initialization time (typically at application startup), points from 4 to 6 are executed multiple times, parallelly (e.g. per WEB request). Point 7 is done in finalization time (mostly at termination).
The first step before we'd start is creating a scope factory:
using Solti.Utils.DI.Interfaces;
using Solti.Utils.DI;
using(IScopeFactory scopeFactory = ScopeFactory.Create(svcs => /*registering services*/))
{
...
}
In most of the cases you should use only one factory although you can have as much as you want.
Registering a service can be done via several patterns (I name them recipes):
Service() generic method with the desired interface, implementation and lifetime:
svcs.Service<IMyService, MyService>(Lifetime.Transient);
You can register generic services as well:
svcs.Service(typeof(IMyGenericService<>), typeof(MyGenericService<>), Lifetime.Singleton);
Remarks:
ServiceActivatorAttribute)!public class MyService: IMyService
{
public MyService(IInjector injector, IService_1 dep1, IService_2 dep2) {...}
[Inject]
publiuc IService3 Dep3 {get; set; /*or init*/}
}
public class MyService: IMyService
{
public MyService(ILazy<IService_1> /*or Lazy<IService_1>*/ dep1, ILazy<IService_2> dep2) {...}
}
public class MyService: IMyService
{
public MyService(IService_1 dep1, [Options(Optional = true)]IService_2 dep2) {...}
}
svcs.Service<IMyService, MyService>(new Dictionary<string, object?>{["paramName"] = someValue}, Lifetime.Transient);
// or
svcs.Service<IMyService, MyService>(new {paramName = someValue}, Lifetime.Transient);
svcs.Factory<IMyService>
(
factoryExpr: injector => new MyService(injector, injector.Get<IService_1>(), injector.Get<IService_2>())
{
Dep3 = injector.Get<IService_3>()
},
Lifetime.Singleton
);
// or
svcs.Factory<IMyService>
(
factory: injector => { ...complex logic... },
Lifetime.Singleton
);
It can be useful e.g. if a service has more than one public constructor. In case of generic services the factory function is called with the concrete id:
svcs.Factory(typeof(IMyGenericService<>), (injector, serviceInterface) =>
{
Assert.That(serviceInterface.IsGenericTypeDefinition, Is.False);
Assert.That(serviceInterface.GetGenericTypeDefinition(), Is.EqualTo(typeof(IMyGenericService<>)));
...
});
using System;
using ServiceStack.Data;
using ServiceStack.OrmLite;
namespace Services
{
using API;
public class MySqlDbConnectionFactoryProvider : IServiceProvider
{
public IConfig Config { get; }
public MySqlDbConnectionFactoryProvider(IConfig config) => Config = config ?? throw new ArgumentNullException(nameof(config));
public object GetService(Type serviceType)
{
if (serviceType != typeof(IDbConnectionFactory))
throw new NotSupportedException();
return new OrmLiteConnectionFactory(Config.ConnectionString, MySqlDialect.Provider)
{
AutoDisposeConnection = true
};
}
}
}
...
svcs.Provider<IDbConnectionFactory, MySqlDbConnectionFactoryProvider>(Lifetime.Singleton);
svcs.Instance<IMyService>(service);
Instances are NEVER disposed by the system, you have to do it manually.Remarks:
svcs.Service<IMyService, MyServiceImeplementation_1>("svc1", Lifetime.Transient);
svcs.Factory<IMyService>("svc2", i => ..., Lifetime.Singleton);
...
Later you can request them individually:
class MyOtherService: IMyOtherService
{
public MyOtherService([Options(Name = "svc2")]IMyService dep) {...}
...
}
or in a batched form using the magic IEnumerable<> service:
class MyOtherService: IMyOtherService
{
public MyOtherService(IEnumerable<IMyService> deps)
{
Assert.That(deps.Count(), Is.EqualTo(2));
...
}
...
}
In practice, it's useful to separate common functionality (e.g. parameter validation) from the implementation. In this library this can be achieved by proxy pattern. In a brief example:
using Solti.Utils.DI.Interfaces;
using Solti.Utils.DI;
...
public interfce IMyModule
{
// When using the Decorate() function, only the interface members can be annotated
void DoSomethig([NotNull] string param);
}
...
svcs
.Service<IMyModule, MyModule>(Lifetime.Scoped).Decorate<ParameterValidatorProxy>();
Where the ParameterValidatorProxy is an IInterfaceInterceptor implementation containing the parameter validation logic:
using Solti.Utils.DI.Interfaces;
...
// Base class of all the validator attributes
public abstract class ParameterValidatorAttribute: Attribute
{
public abstract void Validate(ParameterInfo param, object value);
}
// Sample validator
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class NotNullAttribute : ParameterValidatorAttribute
{
public override void Validate(ParameterInfo param, object value)
{
if (value is null)
throw new ArgumentNullException(param.Name);
}
}
...
public class ParameterValidatorProxy : IInterfaceInterceptor
{
public ParameterValidator(IDependency dependency)
{
...
}
public object? Invoke(IInvocationContext context, CallNextDelegate<IInvocationContext, object?> callNext)
{
foreach(var descr in context.TargetMethod.GetParameters().Select(
(p, i) => new
{
Parameter = p,
Value = context.Args[i],
Validators = p.GetCustomAttributes<ParameterValidatorAttribute>()
}))
{
foreach (var validator in descr.Validators)
{
validator.Validate(descr.Parameter, descr.Value);
}
}
return callNext(context);
}
}
Remarks:
Decorating services can be done using attributes as well. In this case we declare an attribute (derived from the AspectAttribute) that instructs the system which interceptors should be used. Doing so we introduce AOP in our code:
...
// Define an aspect for the ParameterValidatorProxy (see above)
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, AllowMultiple = false)]
public sealed class ParameterValidatorAspect : AspectAttribute
{
public ParameterValidatorAspect(): base(typeof(ParameterValidatorProxy)) { }
}
...
// Then annotate the desired interface ...
[ParameterValidatorAspect]
public interface IService
{
void DoSomething([NotNull] object arg);
}
// ... OR class (recommended)
[ParameterValidatorAspect]
public class Service: IService
{
// Only methods implementing the above declared interface can be annoteted
void DoSomething([NotNull] object arg) {...}
}
Notes:
ParameterValidatorProxy manually.// On service invocations the system first validates the user then the method parameters.
[UserValidatorAspect, ParameterValidatorAspect]
public interface MyService {...}
using (IInjector injector = scopeFactory.CreateScope()) // creating a scope is a thread-safe operation
{
...
}
or
await using (IInjector injector = scopeFactory.CreateScope())
{
...
}
Remarks:
IInjector instances are NOT thread-safe so every session / worker must have its own scope.IInjector instance.IMyService svc = injector.Get<IMyService>();
or
IMyService svc = injector.Get<IMyService>("servicename");
Remarks:
injector.TryGet() method.injector.Get() method.using(IScopeFactory scopeFactory = ScopeFactory.Create(svcs => svcs.Service(typeof(IMyGenericService<>), ...)))
{
...
using(IInjector injector = scopeFactory.CreateScope())
{
IMyGenericService<string> svc = injector.Get<IMyGenericService<string>>();
}
}
IEnumerable<> service:
// Returns all the IMyService instances regardless their names
IEnumerable<IMyService> svcs = injector.Get<IEnumerable<IMyService>>();
// Service instantiation is done during enumeration
A service can request its owner IInjector as a regular dependency (via constructor parameter). Doing this makes it possible to get services anywhere in the implementation (by invoking the IInjector.Get() method). This is we call inline dependencies.
Notes:
Consider the following registration:
...
svcs
.Service<IMyService, MyService>(Lifetime.Transient)
.Service<IMyOtherService, MyOtherServiceDependsOnIMyService>(Lifetime.Singleton);
...
using (IInjector injector = scopeFactory.CreateScope())
{
var svc = injector.Get<IMyOtherService>();
...
}
Leaving the using block the IMyService instance requested by MyOtherServiceDependsOnIMyService won't be released because the requester still alives (until the root scope is disposed). This situation is called captive dependency. To avoid it you have two options:
Lifetime less than or equal to the Lifetime of consumed service.using(IScopeFactory scopeFactory = ScopeFactory.Create(svcs => ..., new ScopeOptions {StrictDI = true}))
{
...
}
Microsoft also defines its own interface for dependency resolution. This library has built in support for it:
using System;
using Solti.Utils.DI;
using Solti.utils.DI.Interfaces;
...
using(IScopeFactory scopeFactory = ScopeFactory.Create(svcs => ..., new ScopeOptions {SupportsServiceProvider = true}))
{
...
using(scopeFactory.CreateScope(out IServiceProvider sp))
{
}
}
Differences compared to IInjector:
IServiceProvider.GetService() won't throw if a service cannot be found). This rule applies to services requested via constructor parameters, too.OptionsAttribute (IServiceProvider.GetService() has no name parameter).IServiceProvider interface is not an IDisposable descendant you should release the disposable returned by the CreateScope() to end the scope lifetime.You can also visualize the dependency graph belongs to a particular service using the Solti.Utils.DI.Diagnostics namespace and a Graphviz renderer:
using Solti.Utils.DI.Diagnostics;
using Solti.utils.DI.Interfaces;
...
using(IScopeFactory scopeFactory = ScopeFactory.Create(svcs => ...))
{
// The returned graph descriptor is supposed to be compatible with any Graphviz rendrer
string dotGraph = scopeFactory.GetDependencyGraph<IMyService>(newLine: "\n");
...
}
The rendered graph should look like this:
You can browse the detailed API docs or the benchmark results.
This project currently targets .NET Standard 2.0 and 2.1.
Solti.Utils.DI namespace so you just have to remove the Solti.Utils.DI.Annotations usings.Lazy recipe has completely been removed. To preserve this functionality you can implement your own deferring logic in a Factory function.Solti.Utils.DI.Interfaces) in files where you use the general interfaces.Solti.Utils.DI.Interfaces assembly directly.runtimeconfig.json (and got a new layout). See this as a referenceIServiceGraph to IServicePath.ServiceContainer class has been dropped. You can register services when creating the (newly introduced) scope factory:
using IScopeFactory scopeFactory = ScopeFactory.Create(svcs => svcs
.Service(...)
.Factory(...)
.Provider(...));
or
using IScopeFactory scopeFactory = ScopeFactory.Create(new ServiceCollection()
.Service(...)
.Factory(...)
.Provider(...));
runtimeconfig configuration has been removed. Scopes can be tweaked via the ScopeOptions class:
ScopeFactory.Create(svcs => ..., new ScopeOptions {...})
IScopeFactory is responsible for creating scopes:
/*await*/ using IInjector scope = scopeFactory.CreateScope();
Note that IInjector instance MUST be freed at the end of the session (scope factory doesn't maintain the lifetime of the created scopes)InterfaceInterceptor<>.Invoke() method has been changed: Now it has only a single parameter (InvocationContext) that contains all the invocation related attributes.Factory and Provider recipe now takes Expression<> instead of Func<>. Compile time created expressions have their limitations (for e.g. they cannot contain if or any complex statements). To work this around:
svcs.Factory<IMyService>(injector =>
{
IDependency dep = injector.Get<IDependency>();
...DO SOME ELABORATE STUFFS...
return result;
}, ...);
needs to be refactored like:
private static IMyService MyServiceFactory(IDependency dep)
{
...DO SOME ELABORATE STUFFS...
return result;
}
...
svcs.Factory<IMyService>(injector => MyServiceFactory(injector.Get<IDependency>()), ...);
IServiceCollection(Basic|Advanced)Extensions.WithProxy() has been renamed to Decorate()IInterfaceInterceptor interfacce (instead of descending from InterfaceInterceptor<> class directly)AspectAttribute got a new layout. Now it returns the interceptor type via property.AspectAttribute got a new layout. Interceptors now shall be passed as constructor parameters. So refactor this
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, AllowMultiple = false)]
public sealed class ParameterValidatorAspect : AspectAttribute
{
public override Type UnderlyingInterceptor { get; } = typeof(ParameterValidatorProxy);
}
to
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, AllowMultiple = false)]
public sealed class ParameterValidatorAspect : AspectAttribute
{
public ParameterValidatorAspect(): base(typeof(ParameterValidatorProxy)) { }
}
IInterfaceInterceptor.Invoke() layout has been altered (next() now requires the context to be passed), so refactor this
public class LoggerInterceptor : IInterfaceInterceptor
{
public object Invoke(IInvocationContext context, Next<object> callNext)
{
Console.WriteLine(context.InterfaceMethod);
return callNext();
}
}
to
public class LoggerInterceptor : IInterfaceInterceptor
{
public object Invoke(IInvocationContext context, Next<IInvocationContext, object> callNext)
{
Console.WriteLine(context.InterfaceMethod);
return callNext(context);
}
}
Factory recipe now accepts concrete FactoryDelegate alongside the Expression<FactoryDelegate>. In case the compiler can't decide which overload to utilize, use named arguments:
svcs.Factory<IMyService>(factory: injector =>
{
IDependency dep = injector.Get<IDependency>();
...DO SOME ELABORATE STUFFS...
return result;
}, ...);
or
svcs.Factory<IMyService>(factoryExpr: injector => /*simple expression*/, ...);
name parameter has been changed to key in all named service related methods.Next delegate has been renamed to CallNextDelegateCan be found here