Attribute-based service registration for .NET dependency injection with support for keyed services. Automatically scan and register services decorated with the [Service] attribute.
- 🚀 Automatic Service Registration - Decorate classes with
[Service]and they're automatically registered - 🔑 Keyed Services Support - Register and resolve services by key
- ⚡ All Service Lifetimes - Scoped, Singleton, and Transient support
- 🎯 Interface Registration - Services registered as both concrete type and all interfaces
- 🔍 Assembly Scanning - Efficient reflection-based service discovery
- ✨ Clean API - Single extension method:
AddServiceLocator<T>() - 📦 Lightweight - Zero external dependencies (except Microsoft.Extensions.DependencyInjection)
dotnet add package ServiceLocatorRequirements:
- .NET 9.0 or later
- Microsoft.Extensions.DependencyInjection 9.0.0+
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:
- Scans the assembly for all classes decorated with
[Service]attribute - Non-Keyed Services (where
Key == null):- Registered as concrete type (first-wins behavior with
TryAdd*) - Registered for all implemented interfaces (allows multiple implementations with
TryAddEnumerable)
- Registered as concrete type (first-wins behavior with
- Keyed Services (where
Key != null):- Registered with the specified key using
AddKeyedScoped/Singleton/Transient - Also registered as non-keyed for enumeration support via
IEnumerable<T>
- Registered with the specified key using
Important: Keyed services are accessible both by key AND via enumeration, giving you maximum flexibility.
- Target Framework: Now requires .NET 9.0 (previously .NET 7.0)
- Dependency Removed: Scrutor is no longer a dependency (reduces package size)
[Service]attribute is backward compatible - existing code works without changes- New optional
Keyproperty added for keyed service support - All existing tests pass without modification
-
Update 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:
- Scoped, Singleton, and Transient services
- Multiple implementations of the same interface
- Keyed services with string keys
- Enumerating all services including keyed ones
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 ❤️