Skip to content

Provide conventions-based registrations in addition to [Service] attributes #116

@kzu

Description

@kzu

Sometimes you don't even have the source of the libraries you want to expose as services, or it would be cumbersome to annotate a large amount of them.

In this case, the typical solution is to implement a reflection-based approach where you inspect all types and register those that can be assigned to a given marker interface (i.e. IService) or by name (i.e. those ending in Service).

Since this project is all about compile-time registrations and zero run-time impact, it makes sense to also emit compile-time registrations that are more flexible than simply annotating all types.

After going through some alternatives (i.e. an assembly-level attribute), I decided that a much more discoverable approach is to add a "pseudo-reflection" overload to AddServices that can take a type and/or a regex. The source generator can evaluate both at compile-time and emit the registrations that the existing AddServices() will register, just as if they had been annotated with [Service].

Examples from tests:

public class ConventionsTests(ITestOutputHelper Output)
{
    [Fact]
    public void RegisterRepositoryServices()
    {
        var conventions = new ServiceCollection();
        conventions.AddSingleton(Output);
        conventions.AddServices(typeof(IRepository));
        var services = conventions.BuildServiceProvider();

        var instance = services.GetServices<IRepository>().ToList();

        Assert.Equal(2, instance.Count);
    }

    [Fact]
    public void RegisterServiceByRegex()
    {
        var conventions = new ServiceCollection();
        conventions.AddSingleton(Output);
        conventions.AddServices(nameof(ConventionsTests), ServiceLifetime.Transient);
        var services = conventions.BuildServiceProvider();

        var instance = services.GetRequiredService<ConventionsTests>();
        var instance2 = services.GetRequiredService<ConventionsTests>();

        Assert.NotSame(instance, instance2);
    }

    [Fact]
    public void RegisterGenericServices()
    {
        var conventions = new ServiceCollection();

        conventions.AddServices(typeof(IGenericRepository<>), ServiceLifetime.Scoped);

        var services = conventions.BuildServiceProvider();

        var scope = services.CreateScope();
        var instance = scope.ServiceProvider.GetRequiredService<IGenericRepository<string>>();
        var instance2 = scope.ServiceProvider.GetRequiredService<IGenericRepository<int>>();

        Assert.NotNull(instance);
        Assert.NotNull(instance2);

        Assert.Same(instance, scope.ServiceProvider.GetRequiredService<IGenericRepository<string>>());
        Assert.Same(instance2, scope.ServiceProvider.GetRequiredService<IGenericRepository<int>>());
    }
}

public interface IRepository { }
public class FooRepository : IRepository { }
public class BarRepository : IRepository { }

public interface IGenericRepository<T> { }
public class FooGenericRepository : IGenericRepository<string> { }
public class BarGenericRepository : IGenericRepository<int> { }

Back this issue
Back this issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions