Devlooped.Extensions.DependencyInjection
2.1.0-beta
Prefix Reserved
dotnet add package Devlooped.Extensions.DependencyInjection --version 2.1.0-beta
NuGet\Install-Package Devlooped.Extensions.DependencyInjection -Version 2.1.0-beta
<PackageReference Include="Devlooped.Extensions.DependencyInjection" Version="2.1.0-beta"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add Devlooped.Extensions.DependencyInjection --version 2.1.0-beta
#r "nuget: Devlooped.Extensions.DependencyInjection, 2.1.0-beta"
// Install Devlooped.Extensions.DependencyInjection as a Cake Addin #addin nuget:?package=Devlooped.Extensions.DependencyInjection&version=2.1.0-beta&prerelease // Install Devlooped.Extensions.DependencyInjection as a Cake Tool #tool nuget:?package=Devlooped.Extensions.DependencyInjection&version=2.1.0-beta&prerelease
Automatic compile-time service registrations for Microsoft.Extensions.DependencyInjection with no run-time dependencies.
Usage
After installing the nuget package,
a new [Service(ServiceLifetime)]
attribute will be available to annotate your types:
[Service(ServiceLifetime.Scoped)]
public class MyService : IMyService, IDisposable
{
public string Message => "Hello World";
public void Dispose() { }
}
public interface IMyService
{
string Message { get; }
}
The ServiceLifetime
argument is optional and defaults to ServiceLifetime.Singleton.
NOTE: The attribute is matched by simple name, so you can define your own attribute in your own assembly. It only has to provide a constructor receiving a ServiceLifetime argument.
A source generator will emit (at compile-time) an AddServices
extension method for
IServiceCollection
which you can call from your startup code that sets up your services, like:
var builder = WebApplication.CreateBuilder(args);
// NOTE: **Adds discovered services to the container**
builder.Services.AddServices();
// ...
var app = builder.Build();
// Configure the HTTP request pipeline.
app.MapGet("/", (IMyService service) => service.Message);
// ...
app.Run();
NOTE: the service is available automatically for the scoped request, because we called the generated
AddServices
that registers the discovered services.
And that's it. The source generator will discover annotated types in the current project and all its references too. Since the registration code is generated at compile-time, there is no run-time reflection (or dependencies) whatsoever.
You can also avoid attributes entirely by using a convention-based approach, which is nevertheless still compile-time checked and source-generated. This allows registering services for which you don't even have the source code to annotate:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServices(typeof(IRepository), ServiceLifetime.Scoped);
// ...
You can also use a regular expression to match services by name instead:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddServices(".*Service$"); // defaults to ServiceLifetime.Singleton
// ...
Or a combination of both, as needed. In all cases, NO run-time reflection is ever performed, and the compile-time source generator will evaluate the types that are assignable to the given type or matching full type names and emit the typed registrations as needed.
Keyed Services
Keyed services
are also supported by a separate generic [Service]
attribute, like:
public interface INotificationService
{
string Notify(string message);
}
[Service<string>("sms")]
public class SmsNotificationService : INotificationService
{
public string Notify(string message) => $"[SMS] {message}";
}
[Service<string>("email")]
[Service<string>("default")]
public class EmailNotificationService : INotificationService
{
public string Notify(string message) => $"[Email] {message}";
}
Services that want to consume a specific keyed service can use the
[FromKeyedServices(object key)]
attribute to specify the key, like:
[Service]
public class SmsService([FromKeyedServices("sms")] INotificationService sms)
{
public void DoSomething() => sms.Notify("Hello");
}
In this case, when resolving the SmsService
from the service provider, the
right INotificationService
will be injected, based on the key provided.
Note you can also register the same service using multiple keys, as shown in the
EmailNotificationService
above.
Keyed services are a feature of version 8.0+ of Microsoft.Extensions.DependencyInjection
How It Works
The generated code that implements the registration looks like the following:
static partial class AddServicesExtension
{
public static IServiceCollection AddServices(this IServiceCollection services)
{
services.AddScoped(s => new MyService());
services.AddScoped<IMyService>(s => s.GetRequiredService<MyService>());
services.AddScoped<IDisposable>(s => s.GetRequiredService<MyService>());
return services;
}
Note how the service is registered as scoped with its own type first, and the other two registrations just retrieve the same (according to its defined lifetime). This means the instance is reused and properly registered under all implemented interfaces automatically.
NOTE: you can inspect the generated code by setting
EmitCompilerGeneratedFiles=true
in your project file and browsing thegenerated
subfolder underobj
.
If the service type has dependencies, they will be resolved from the service provider by the implementation factory too, like:
services.AddScoped(s => new MyService(s.GetRequiredService<IMyDependency>(), ...));
MEF Compatibility
Given the (more or less broad?) adoption of
MEF attribute
(whether .NET MEF, NuGet MEF or VS MEF) in .NET,
the generator also supports the [Export]
attribute to denote a service (the
type argument as well as contract name are ignored, since those aren't supported
in the DI container).
In order to specify a singleton (shared) instance in MEF, you have to annotate the
type with an extra attribute: [Shared]
in NuGet MEF (from System.Composition)
or [PartCreationPolicy(CreationPolicy.Shared)]
in .NET MEF
(from System.ComponentModel.Composition).
Both [Export("contractName")]
and [Import("contractName")]
are supported and
will be used to register and resolve keyed services respectively, meaning you can
typically depend on just [Export]
and [Import]
attributes for all your DI
annotations and have them work automatically when composed in the DI container.
Advanced Scenarios
Lazy<T>
and Func<T>
Dependencies
A Lazy<T>
for each interface (and main implementation) is automatically provided
too, so you can take a lazy dependency out of the box too. In this case, the lifetime
of the dependency T
becomes tied to the lifetime of the component taking the lazy
dependency, for obvious reasons. The Lazy<T>
is merely a lazy resolving of the
dependency via the service provider. The lazy itself isn't costly to construct, and
since the lifetime of the underlying service, plus the lifetime of the consuming
service determine the ultimate lifetime of the lazy, no additional configuration is
necessary for it, as it's always registered as a transient component. Generated code
looks like the following:
services.AddTransient(s => new Lazy<IMyService>(s.GetRequiredService<MyService>));
A Func<T>
is also automatically registered, but it is just a delegate to the
actual IServiceProvider.GetRequiredService<T>
. Generated code looks like the
following:
services.AddTransient<Func<IMyService>>(s => s.GetRequiredService<MyService>);
Repeatedly invoking the function will result in an instance of the required
service that depends on the registered lifetime for it. If it was registered
as a singleton, for example, you would get the same value every time, just
as if you had used a dependency of Lazy<T>
instead, but invoking the
service provider each time, instead of only once. This makes this pattern
more useful for transient services that you intend to use for a short time
(and potentially dispose afterwards).
Your Own ServiceAttribute
If you want to declare your own ServiceAttribute
and reuse from your projects,
so as to avoid taking a (development-only, compile-time only) dependency on this
package from your library projects, you can just declare it like so:
[AttributeUsage(AttributeTargets.Class)]
public class ServiceAttribute : Attribute
{
public ServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Singleton) { }
}
Likewise for the keyed service version:
[AttributeUsage(AttributeTargets.Class)]
public class ServiceAttribute<TKey> : Attribute
{
public ServiceAttribute(TKey key, ServiceLifetime lifetime = ServiceLifetime.Singleton) { }
}
NOTE: since the constructor arguments are only used by the source generation to detemine the registration style (and key), but never at run-time, you don't even need to keep it around in a field or property!
With this in place, you only need to add this package to the top-level project that is adding the services to the collection!
The attribute is matched by simple name, so it can exist in any namespace.
If you want to avoid adding the attribute to the project referencing this package,
set the $(AddServiceAttribute)
to true
via MSBuild:
<PropertyGroup>
<AddServiceAttribute>false</AddServiceAttribute>
</PropertyGroup>
Choose Constructor
If you want to choose a specific constructor to be used for the service implementation
factory registration (instead of the default one which will be the one with the most
parameters), you can annotate it with [ImportingConstructor]
from either NuGet MEF
(System.Composition)
or .NET MEF (System.ComponentModel.Composition).
Sponsors
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Devlooped.Extensions.DependencyInjection:
Package | Downloads |
---|---|
Devlooped.Extensions.DependencyInjection.Attributed
Superseded by Devlooped.Extensions.DependencyInjection |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
2.1.0-beta | 70 | 11/12/2024 |