Skip to content

alzuma/ServiceLocator

Repository files navigation

ServiceLocator

NuGet .NET License

Attribute-based service registration for .NET dependency injection with support for keyed services. Automatically scan and register services decorated with the [Service] attribute.

Features

  • 🚀 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)

Installation

dotnet add package ServiceLocator

Requirements:

  • .NET 9.0 or later
  • Microsoft.Extensions.DependencyInjection 9.0.0+

Quick Start

Basic Usage (Non-Keyed Services)

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 (New in v2.0.0)

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"]

Service Lifetimes

Scoped

Services are created once per request (HTTP request in web apps).

[Service(ServiceLifetime.Scoped)]
public class RequestService : IRequestService
{
    // New instance per HTTP request
}

Singleton

Services are created once and shared across the entire application lifetime.

[Service(ServiceLifetime.Singleton)]
public class ConfigurationService : IConfigurationService
{
    // Single instance for entire application
}

Transient

Services are created each time they are requested.

[Service(ServiceLifetime.Transient)]
public class TemporaryService : ITemporaryService
{
    // New instance every time it's injected
}

Advanced Scenarios

Multiple Implementations

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);
    }
});

Enum Keys

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");
});

Mixed Keyed and Non-Keyed Services

// 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"]
});

How It Works

ServiceLocator uses a unified reflection-based registration process:

  1. Scans the assembly for all classes decorated with [Service] attribute
  2. 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)
  3. 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>

Important: Keyed services are accessible both by key AND via enumeration, giving you maximum flexibility.

Migration from v1.x to v2.0.0

Breaking Changes

  • Target Framework: Now requires .NET 9.0 (previously .NET 7.0)
  • Dependency Removed: Scrutor is no longer a dependency (reduces package size)

Non-Breaking Changes

  • [Service] attribute is backward compatible - existing code works without changes
  • New optional Key property added for keyed service support
  • All existing tests pass without modification

Upgrading

  1. Update your project to target .NET 9.0:

    <TargetFramework>net9.0</TargetFramework>
  2. Update the package:

    dotnet add package ServiceLocator --version 2.0.3
  3. (Optional) Start using keyed services for new scenarios

Dependencies

Examples

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

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.


With love from Courland IT ❤️

About

Service injection by attributes

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages