Skip to content

Introduce .Services on EndpointConfiguration as a replacement for RegisterComponents#7643

Draft
danielmarbach wants to merge 1 commit intomasterfrom
service_collection
Draft

Introduce .Services on EndpointConfiguration as a replacement for RegisterComponents#7643
danielmarbach wants to merge 1 commit intomasterfrom
service_collection

Conversation

@danielmarbach
Copy link
Contributor

@danielmarbach danielmarbach commented Mar 6, 2026

This PR introduces a new EndpointConfiguration.Services property as the preferred way to register custom services with the dependency injection container, and deprecates the existing RegisterComponents(Action<IServiceCollection>) method.

Motivation

We want to deprecate RegisterComponents on endpoint configuration because:

  1. Discoverability: The lambda-based API is less discoverable than a direct property
  2. Consistency: Using a property aligns with modern .NET DI patterns (Aspire, Akka.Hosting, etc.)
  3. Simplicity: Direct service registration is more intuitive than wrapping in a lambda

Tradeoff Analysis

Option 1: Lambda on AddNServiceBusEndpoint(IServiceCollection services, Action configure)

Rejected

This would have users configure services when calling AddNServiceBusEndpoint:

services.AddNServiceBusEndpoint(config, endpointServices => {
    endpointServices.AddSingleton<IMyService, MyService>();
});

Why rejected:

  • Different APIs for different modes: Internally managed mode (Endpoint.Create) would still need the old API
  • Less discoverable: Forces external registration when the endpoint configuration is the natural composition root
  • Multi-hosting complexity: Needs special handling for keyed services scenario
  • EndpointConfiguration is the composition root of all configuration

Option 2: EndpointConfiguration.Services Property (Selected)

This exposes an IServiceCollection directly on the configuration:

var config = new EndpointConfiguration("MyEndpoint");
config.Services.AddSingleton<IMyService, MyService>();

Why selected:

  • Consistent API: Same pattern for both internally and externally managed modes
  • Natural composition root: Users configure the endpoint, then start/host it
  • Aligns with ecosystem: Matches patterns from Aspire, Akka.Hosting, etc.
  • Clear migration path: Simple find-and-replace from lambda to property access
    Implementation challenge: Since EndpointConfiguration is created before the actual service collection exists, we use a staging collection pattern:
  1. EndpointConfiguration.Services returns a staging ServiceCollection that records registrations
  2. ApplyUserServicesTo(target) is called at a deterministic point after NServiceBus registers its defaults
  3. User registrations are applied last, preserving "user overrides NServiceBus" semantics

Changes

New API:

  • EndpointConfiguration.Services - Returns IServiceCollection for registering custom services
  • EndpointConfiguration.ApplyUserServicesTo(IServiceCollection) - Internal method to apply staged registrations
    Deprecated:
  • RegisterComponents(Action) - Obsolete with warning, becomes error in v11, removed in v12
    Behavior:
  • NServiceBus registers its defaults first
  • User registrations from .Services property are applied second
  • Obsolete RegisterComponents actions are applied last (allowing overrides)
  • Works for both internally managed (Endpoint.Create) and externally managed (AddNServiceBusEndpoint) modes

Migration Guide

Before (obsolete):

var config = new EndpointConfiguration("MyEndpoint");
config.RegisterComponents(services => {
    services.AddSingleton<IMyService, MyService>();
});

After:

var config = new EndpointConfiguration("MyEndpoint");
config.Services.AddSingleton<IMyService, MyService>();

@danielmarbach
Copy link
Contributor Author

We are keeping this as draft and rediscussing it.

s.AddSingleton<SingletonAsyncDisposable>();
});
// We have to take control over re-registering the context because we have taken control over the instance creation
c.Services.AddSingleton((Context)r.ScenarioContext);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unique to this test, not unique to moving from RegisterComponents to Services.Add… right?

Comment on lines +89 to +92
[ObsoleteMetadata(Message = "Use the Services property instead",
ReplacementTypeOrMember = "EndpointConfiguration.Services",
TreatAsErrorFromVersion = "11",
RemoveInVersion = "12")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReplacementTypeOrMember is only a shortcut for when you don't specify a more specific string. Note in the generated message you essentially get duplicate sentences when you provide both properties. Also, an edit suggestion:

Suggested change
[ObsoleteMetadata(Message = "Use the Services property instead",
ReplacementTypeOrMember = "EndpointConfiguration.Services",
TreatAsErrorFromVersion = "11",
RemoveInVersion = "12")]
[ObsoleteMetadata(Message = "Use the Services property for direct access to the endpoint-specific IServiceCollection instead",
TreatAsErrorFromVersion = "11",
RemoveInVersion = "12")]

Will require running the fixer (I don't trust myself to guess) and then also updating the API approval.

Copy link
Member

@andreasohlund andreasohlund left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming Davids remark about the obsolete message gets addressed

@danielmarbach
Copy link
Contributor Author

We'll do it differently. I will resurrect some things from here, but we are not proceeding. I'll keep you posted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants