Skip to content

Manual registration

Hook hand-written registration code into the generated AddXxxServices method with [RegisterServices].

3 min read

Not every registration fits on an attribute. Options binding, AddHttpClient, AddDbContext, third-party extension methods — these are ordinary code. [RegisterServices] marks a method to be called from the generated AddXxxServices, so the whole assembly still registers through one call.

Declaring a method

ManualRegistrations.cs
using Immediate.Injections.Shared;
using Microsoft.Extensions.DependencyInjection;

public static class ManualRegistrations
{
	[RegisterServices]
	public static void Register(IServiceCollection services)
	{
		_ = services.AddHttpClient<TodoApiClient>();
		_ = services.AddDbContext<TodoDbContext>();
	}
}

Nothing else is needed. AddTodoServices() now calls ManualRegistrations.Register(services) after applying every attribute-driven registration.

The accepted signatures

Exactly two shapes are accepted:

static void Method(IServiceCollection services);
static void Method(IServiceCollection services, ReadOnlySpan<string> tags);

Everything else is INJ0001, an error. In particular:

WrittenAccepted
static void M(IServiceCollection)Yes
static void M(IServiceCollection, ReadOnlySpan<string>)Yes
static async void M(IServiceCollection)No — async is rejected
static IServiceCollection M(IServiceCollection)No — must return void
static void M()No — the collection parameter is required
static void M(IServiceCollection, ReadOnlySpan<string>, int)No — no third parameter
void M(IServiceCollection)No — must be static

The parameters are positional: IServiceCollection first, ReadOnlySpan<string> second. The containing type is up to you — a static method on a non-static class works just as well as one on a static class.

The generated code calls the method by its fully-qualified name from a class in the project’s root namespace, so the method and its containing type must be accessible from there. A private or nested-private method passes the analyzer and then fails to compile in the generated file; public or internal is what you want.

Receiving tags

The second parameter receives the exact tag list passed to AddXxxServices, so hand-written registrations can apply the same filter as attribute-driven ones:

ManualRegistrations.cs
using Immediate.Injections.Shared;
using Microsoft.Extensions.DependencyInjection;

public static class ManualRegistrations
{
	[RegisterServices]
	public static void RegisterWorkers(IServiceCollection services, ReadOnlySpan<string> tags)
	{
		if (tags.Length != 0 && !tags.Contains("worker"))
			return;

		_ = services.AddHostedService<OutboxWorker>();
	}
}

Note the guard shape. The generator does no filtering for you here — it hands you the raw list, including the empty list that means “register everything”. Mirroring the built-in rule means treating an empty span as a match. See Tags and conditional registration.

Several methods

Any number of [RegisterServices] methods may exist across the assembly, on any number of types, and all of them are called — one after another, in the order the generator collected them. There is no “one wins” rule and no way to declare an ordering, so do not write two methods that both register the same service type unless you have set a DuplicateStrategy that makes the outcome deterministic.

Two methods, both called
public static class ManualRegistrations
{
	[RegisterServices]
	public static void RegisterInfrastructure(IServiceCollection services) { /* … */ }

	[RegisterServices]
	public static void RegisterOptions(IServiceCollection services) { /* … */ }
}

The generated hook is a single method that calls each in turn:

II.RegisterServicesMethods.g.cs
static partial void RegisterServices(this IServiceCollection services, ReadOnlySpan<string> tags)
{
	ManualRegistrations.RegisterInfrastructure(services);
	ManualRegistrations.RegisterOptions(services);
}

That call happens last inside AddXxxServices, after all attribute-driven registrations — which matters if your manual code uses TryAdd or Replace.

Where to go next