DependencyInjection
Provides compile-time discovery and code generation of service registrations from attributed types and conventions
Install / Use
/learn @devlooped/DependencyInjectionREADME
.NET DependencyInjection via conventions or [Service] attributes
Automatic compile-time service registrations for Microsoft.Extensions.DependencyInjection with no run-time dependencies, from conventions or attributes.
<!-- include https://github.com/devlooped/.github/raw/main/osmf.md -->Open Source Maintenance Fee
To ensure the long-term sustainability of this project, users of this package who generate revenue must pay an Open Source Maintenance Fee. While the source code is freely available under the terms of the License, this package and other aspects of the project require adherence to the Maintenance Fee.
To pay the Maintenance Fee, become a Sponsor at the proper OSMF tier. A single fee covers all of Devlooped packages.
<!-- https://github.com/devlooped/.github/raw/main/osmf.md --> <!-- #content -->Usage
The package supports two complementary ways to register services in the DI container, both of which are source-generated at compile-time and therefore have no run-time dependencies or reflection overhead:
- Attribute-based: annotate your services with
[Service]or[Service<TKey>]attributes to register them in the DI container. - Convention-based: register services by type or name using a convention-based approach.
Attribute-based
The [Service(ServiceLifetime)] attribute is available to explicitly annotate types for registration:
[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, and optionally an overload receiving an
object keyfor keyed services.
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
AddServicesthat 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.
If the service implements many interfaces and you want to register it only for a specific one, you can specify that as the generic argument:
[Service<IMyService>(ServiceLifetime.Scoped)]
public class MyService : IMyService, IDisposable
[!TIP] If no specific interface is provided, all implemented interfaces are registered for the same service implementation (and they all resolve to the same instance, except for transient lifetime).
Convention-based
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);
// ...
This will register all types in the current project and its references that are
assignable to IRepository, with the specified lifetime.
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
// ...
You can use 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 providing a key with the [Service] attribute. For example:
public interface INotificationService
{
string Notify(string message);
}
[Service("sms")]
public class SmsNotificationService : INotificationService
{
public string Notify(string message) => $"[SMS] {message}";
}
[Service("email")]
[Service("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.
[!IMPORTANT] Keyed services are a feature of version 8.0+ of Microsoft.Extensions.DependencyInjection
How It Works
In all cases, the generated code that implements the registration looks like the following:
static partial class AddServicesExtension
{
public static IServiceCollection AddServices(this IServiceCollection services)
{
services.TryAddScoped(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 service (according to its defined lifetime). This means the instance is reused and properly registered under all implemented interfaces automatically.
[!TIP] You can inspect the generated code by setting
EmitCompilerGeneratedFiles=truein your project file and browsing thegeneratedsubfolder underobj.
If the service type has dependencies, they will be resolved from the service provider by the implementation factory too, like:
services.TryAddScoped(s => new MyService(s.GetRequiredService<IMyDependency>(), ...));
Keyed services will emit TryAddKeyedXXX methods instead.
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 additi
Related Skills
node-connect
335.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
82.5kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
335.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
82.5kCommit, push, and open a PR
