Attribute-based service registration for .NET dependency injection with support for keyed services
$ dotnet add package ServiceLocatorAttribute-based service registration for .NET dependency injection with support for keyed services. Automatically scan and register services decorated with the [Service] attribute.
[Service] and they're automatically registeredAddServiceLocator<T>()dotnet add package ServiceLocator
Requirements:
1. Decorate your services:
using Microsoft.Extensions.DependencyInjection;
using ServiceLocator;
public interface IUserService
{
User GetUser(int id);
}
[Service(ServiceLifetime.Scoped)]
public class UserService : IUserService
{
public User GetUser(int id) => new User { Id = id };
}2. Register services in your application:
var builder = WebApplication.CreateBuilder(args);
// Automatically register all services with [Service] attribute
// from the assembly containing Program
builder.Services.AddServiceLocator<Program>();
var app = builder.Build();3. Use dependency injection as normal:
app.MapGet("/user/{id}", (int id, IUserService userService) =>
{
return userService.GetUser(id);
});
app.Run();Keyed services allow you to register multiple implementations of the same interface and resolve them by key.
1. Register services with keys:
public interface ICache
{
string Get(string key);
}
[Service(ServiceLifetime.Singleton, Key = "redis")]
public class RedisCache : ICache
{
public string Get(string key) => $"Redis: {key}";
}
[Service(ServiceLifetime.Singleton, Key = "memory")]
public class MemoryCache : ICache
{
public string Get(string key) => $"Memory: {key}";
}2. Resolve services by key:
using Microsoft.AspNetCore.Mvc;
app.MapGet("/cache/redis/{key}", (
string key,
[FromKeyedServices("redis")] ICache cache) =>
{
return cache.Get(key);
});
app.MapGet("/cache/memory/{key}", (
string key,
[FromKeyedServices("memory")] ICache cache) =>
{
return cache.Get(key);
});3. Enumerate all services (including keyed):
app.MapGet("/cache/all/{key}", (
string key,
IEnumerable<ICache> caches) =>
{
return caches.Select(c => c.Get(key));
});
// Returns: ["Redis: test", "Memory: test"]Services are created once per request (HTTP request in web apps).
[Service(ServiceLifetime.Scoped)]
public class RequestService : IRequestService
{
// New instance per HTTP request
}Services are created once and shared across the entire application lifetime.
[Service(ServiceLifetime.Singleton)]
public class ConfigurationService : IConfigurationService
{
// Single instance for entire application
}Services are created each time they are requested.
[Service(ServiceLifetime.Transient)]
public class TemporaryService : ITemporaryService
{
// New instance every time it's injected
}Register multiple implementations of the same interface:
public interface INotificationService
{
void Notify(string message);
}
[Service(ServiceLifetime.Scoped)]
public class EmailNotificationService : INotificationService
{
public void Notify(string message) => SendEmail(message);
}
[Service(ServiceLifetime.Scoped)]
public class SmsNotificationService : INotificationService
{
public void Notify(string message) => SendSms(message);
}
// Inject all implementations
app.MapPost("/notify", (string message, IEnumerable<INotificationService> notifiers) =>
{
foreach (var notifier in notifiers)
{
notifier.Notify(message);
}
});Keys can be strings, integers, enums, or any object:
public enum CacheType
{
Primary,
Secondary,
Fallback
}
[Service(ServiceLifetime.Singleton, Key = CacheType.Primary)]
public class PrimaryCache : ICache
{
// ...
}
// Resolve by enum
app.MapGet("/cache/primary", ([FromKeyedServices(CacheType.Primary)] ICache cache) =>
{
return cache.Get("data");
});// Non-keyed service (traditional registration)
[Service(ServiceLifetime.Scoped)]
public class DefaultCache : ICache
{
public string Get(string key) => $"Default: {key}";
}
// Keyed services
[Service(ServiceLifetime.Scoped, Key = "fast")]
public class FastCache : ICache
{
public string Get(string key) => $"Fast: {key}";
}
// Resolve non-keyed service
app.MapGet("/cache/default/{key}", (string key, ICache cache) =>
{
return cache.Get(key); // Gets DefaultCache
});
// Resolve keyed service
app.MapGet("/cache/fast/{key}", (
string key,
[FromKeyedServices("fast")] ICache cache) =>
{
return cache.Get(key); // Gets FastCache
});
// Enumerate all (both keyed and non-keyed)
app.MapGet("/cache/all/{key}", (string key, IEnumerable<ICache> caches) =>
{
return caches.Select(c => c.Get(key));
// Returns: ["Default: test", "Fast: test"]
});ServiceLocator uses a unified reflection-based registration process:
[Service] attributeKey == null):
TryAdd*)TryAddEnumerable)Key != null):
AddKeyedScoped/Singleton/TransientIEnumerable<T>Important: Keyed services are accessible both by key AND via enumeration, giving you maximum flexibility.
[Service] attribute is backward compatible - existing code works without changesKey property added for keyed service supportUpdate your project to target .NET 9.0:
<TargetFramework>net9.0</TargetFramework>
Update the package:
dotnet add package ServiceLocator --version 2.0.3
(Optional) Start using keyed services for new scenarios
See the ServiceLocator.TestApi project for complete working examples of:
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
With love from Courland IT ❤️