From 8675db3598ee310dd69477e319f61a7a84524e07 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 11:30:09 +0200 Subject: [PATCH 01/16] Wip implementation for add installer --- .../Hosting/EndpointHostedInstallerService.cs | 15 +++++ .../Hosting/ServiceCollectionExtensions.cs | 59 +++++++++++++++++++ .../InstallerExternallyManaged.cs | 18 ++++++ 3 files changed, 92 insertions(+) create mode 100644 src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs create mode 100644 src/NServiceBus.Core/InstallerExternallyManaged.cs diff --git a/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs b/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs new file mode 100644 index 0000000000..38eea066dd --- /dev/null +++ b/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs @@ -0,0 +1,15 @@ +#nullable enable +namespace NServiceBus; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Installation; +using Microsoft.Extensions.Hosting; + +class EndpointHostedInstallerService(InstallerWithExternallyManagedContainer externallyManagedInstallerHost, IServiceProvider sp) : IHostedService +{ + public Task StartAsync(CancellationToken cancellationToken = default) => externallyManagedInstallerHost.Setup(sp, cancellationToken); + + public Task StopAsync(CancellationToken cancellationToken= default) => Task.CompletedTask; +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs index cc8295ee73..abbed9efe8 100644 --- a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ namespace NServiceBus; using System.Linq; using System.Runtime.CompilerServices; using Configuration.AdvancedExtensibility; +using Installation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Transport; @@ -16,6 +17,57 @@ namespace NServiceBus; /// public static class ServiceCollectionExtensions { + /// + /// + /// + /// + /// + /// + /// + public static void AddNServiceBusEndpointInstaller(this IServiceCollection services, + EndpointConfiguration endpointConfiguration, + object? endpointIdentifier = null) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(endpointConfiguration); + + var settings = endpointConfiguration.GetSettings(); + // Unfortunately we have to also check this here due to the multiple hosting variants as long as + // the old hosting is still supported. + settings.AssertNotReused(); + + var endpointName = settings.EndpointName(); + var transport = settings.Get().TransportDefinition; + var registrations = GetExistingInstallerRegistrations(services); + + // ValidateEndpointIdentifier(endpointIdentifier, registrations); + // ValidateAssemblyScanning(endpointConfiguration, endpointName, registrations); + // ValidateTransportReuse(transport, registrations); + + if (endpointIdentifier is null) + { + // Deliberately creating it here to make sure we are not accidentally doing it too late. + var externallyManagedInstallerHost = InstallerExternallyManaged.Create(endpointConfiguration, services); + + services.AddSingleton(externallyManagedInstallerHost); + services.AddSingleton(sp => new EndpointHostedInstallerService(externallyManagedInstallerHost, sp)); + } + else + { + // Backdoor for acceptance testing + var keyedServices = settings.GetOrDefault() ?? new KeyedServiceCollectionAdapter(services, endpointIdentifier); + var baseKey = keyedServices.ServiceKey.BaseKey; + + // Deliberately creating it here to make sure we are not accidentally doing it too late. + var externallyManagedInstallerHost = InstallerExternallyManaged.Create(endpointConfiguration, keyedServices); + + services.AddKeyedSingleton(baseKey, externallyManagedInstallerHost); + services.AddSingleton(sp => new EndpointHostedInstallerService(sp.GetRequiredKeyedService(baseKey), sp)); + } + + services.AddSingleton(new EndpointInstallerRegistration(endpointName, endpointIdentifier, endpointConfiguration.AssemblyScanner().Disable, RuntimeHelpers.GetHashCode(transport))); + } + /// /// Registers an NServiceBus endpoint with the dependency injection container, enabling the endpoint /// to resolve services from the application's service provider and participate in the hosted service lifecycle. @@ -174,5 +226,12 @@ [.. services .Where(d => d.ServiceType == typeof(EndpointRegistration) && d.ImplementationInstance is EndpointRegistration) .Select(d => (EndpointRegistration)d.ImplementationInstance!)]; + static List GetExistingInstallerRegistrations(IServiceCollection services) => + [.. services + .Where(d => d.ServiceType == typeof(EndpointInstallerRegistration) && d.ImplementationInstance is EndpointInstallerRegistration) + .Select(d => (EndpointInstallerRegistration)d.ImplementationInstance!)]; + + sealed record EndpointRegistration(string EndpointName, object? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode); + sealed record EndpointInstallerRegistration(string EndpointName, object? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode); } \ No newline at end of file diff --git a/src/NServiceBus.Core/InstallerExternallyManaged.cs b/src/NServiceBus.Core/InstallerExternallyManaged.cs new file mode 100644 index 0000000000..1fe6e36fe8 --- /dev/null +++ b/src/NServiceBus.Core/InstallerExternallyManaged.cs @@ -0,0 +1,18 @@ +namespace NServiceBus; + +using Installation; +using Microsoft.Extensions.DependencyInjection; + +static class InstallerExternallyManaged +{ + internal static InstallerWithExternallyManagedContainer Create(EndpointConfiguration configuration, + IServiceCollection serviceCollection) + { + // does not overwrite installer usernames configured by the user. + configuration.EnableInstallers(); + + var endpointCreator = EndpointCreator.Create(configuration, serviceCollection); + + return new InstallerWithExternallyManagedContainer(endpointCreator); + } +} \ No newline at end of file From 011287992d683d16461a65e4ea7b6544efdb03e6 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 14:23:58 +0200 Subject: [PATCH 02/16] Add tests for endpoint installer functionality --- ...CollectionExtensions_AddEndpoint_Tests.cs} | 18 ++- ...CollectionExtensions_AddInstaller_Tests.cs | 111 ++++++++++++++++++ 2 files changed, 126 insertions(+), 3 deletions(-) rename src/NServiceBus.Core.Tests/Hosting/{ServiceCollectionExtensionsTests.cs => ServiceCollectionExtensions_AddEndpoint_Tests.cs} (77%) create mode 100644 src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs diff --git a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensionsTests.cs b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs similarity index 77% rename from src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensionsTests.cs rename to src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs index 8aa0182d7f..7a505b6400 100644 --- a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensionsTests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs @@ -8,7 +8,7 @@ namespace NServiceBus.Core.Tests.Host; using NUnit.Framework; [TestFixture] -public class ServiceCollectionExtensionsTests +public partial class ServiceCollectionExtensions_AddEndpoint_Tests { [Test] public void Should_register_single_endpoint_without_identifier() @@ -88,12 +88,24 @@ public void Should_throw_when_transport_not_specified() Assert.That(ex!.Message, Does.Contain("A transport has not been configured. Use 'EndpointConfiguration.UseTransport()' to specify a transport")); } - static EndpointConfiguration CreateConfig(string endpointName) + [Test] + public void Should_throw_when_multiple_endpoints_assembly_scanning_enabled() + { + var services = new ServiceCollection(); + + services.AddNServiceBusEndpoint(CreateConfig("Sales"), "Sales"); + + var ex = Assert.Throws(() => services.AddNServiceBusEndpoint(CreateConfig("Billing", assemblyScanningEnabled: true), "Billing")); + + Assert.That(ex!.Message, Does.Contain("When multiple endpoints are registered, each endpoint must disable assembly scanning (cfg.AssemblyScanner().Disable = true) and explicitly register its handlers and sagas using the corresponding registrations methods like AddHandler(), AddSaga() etc. The following endpoints have assembly scanning enabled: 'Billing'.")); + } + + static EndpointConfiguration CreateConfig(string endpointName, bool assemblyScanningEnabled = false) { var config = new EndpointConfiguration(endpointName); config.UseSerialization(); config.UseTransport(new LearningTransport()); - config.AssemblyScanner().Disable = true; + config.AssemblyScanner().Disable = !assemblyScanningEnabled; return config; } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs new file mode 100644 index 0000000000..5ee3ce7982 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs @@ -0,0 +1,111 @@ +#nullable enable + +namespace NServiceBus.Core.Tests.Host; + +using System; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus; +using NUnit.Framework; + +[TestFixture] +public class ServiceCollectionExtensions_AddInstaller_Tests +{ + [Test] + public void Should_register_single_endpoint_without_identifier() + { + var services = new ServiceCollection(); + + Assert.DoesNotThrow(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Sales"))); + } + + [Test] + public void Should_register_single_endpoint_with_identifier() + { + var services = new ServiceCollection(); + + Assert.DoesNotThrow(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Sales"), "sales-key")); + } + + [Test] + public void Should_throw_when_first_endpoint_has_no_identifier_and_second_has_one() + { + var services = new ServiceCollection(); + + services.AddNServiceBusEndpointInstaller(CreateConfig("Sales")); + + var ex = Assert.Throws(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Billing"), "billing-key")); + + Assert.That(ex!.Message, Does.Contain("each endpoint must provide an endpointIdentifier")); + } + + [Test] + public void Should_throw_when_first_endpoint_has_identifier_and_second_has_none() + { + var services = new ServiceCollection(); + + services.AddNServiceBusEndpointInstaller(CreateConfig("Sales"), "sales-key"); + + var ex = Assert.Throws(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Billing"))); + + Assert.That(ex!.Message, Does.Contain("each endpoint must provide an endpointIdentifier")); + } + + [Test] + public void Should_register_multiple_endpoints_when_all_have_identifiers() + { + var services = new ServiceCollection(); + + services.AddNServiceBusEndpointInstaller(CreateConfig("Sales"), "sales-key"); + + Assert.DoesNotThrow(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Billing"), "billing-key")); + } + + [Test] + public void Should_throw_when_multiple_endpoints_have_duplicate_identifiers() + { + var services = new ServiceCollection(); + + services.AddNServiceBusEndpointInstaller(CreateConfig("Sales"), "shared-key"); + + var ex = Assert.Throws(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Billing"), "shared-key")); + + Assert.That(ex!.Message, Does.Contain("An endpoint with the identifier 'shared-key' has already been registered")); + } + + [Test] + public void Should_throw_when_transport_not_specified() + { + var services = new ServiceCollection(); + + var ex = Assert.Throws(() => + { + var endpointConfiguration = new EndpointConfiguration("Billing"); + endpointConfiguration.UseSerialization(); + + services.AddNServiceBusEndpointInstaller(endpointConfiguration); + }); + + Assert.That(ex!.Message, Does.Contain("A transport has not been configured. Use 'EndpointConfiguration.UseTransport()' to specify a transport")); + } + + [Test] + public void Should_throw_when_multiple_endpoints_assembly_scanning_enabled() + { + var services = new ServiceCollection(); + + services.AddNServiceBusEndpointInstaller(CreateConfig("Sales"), "Sales"); + + var ex = Assert.Throws(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Billing", assemblyScanningEnabled: true), "Billing")); + + Assert.That(ex!.Message, Does.Contain("When multiple endpoints are registered, each endpoint must disable assembly scanning (cfg.AssemblyScanner().Disable = true) and explicitly register its installers using the corresponding registrations methods like AddInstaller(). The following endpoints have assembly scanning enabled: 'Billing'.")); + } + + static EndpointConfiguration CreateConfig(string endpointName, bool assemblyScanningEnabled = false) + { + var config = new EndpointConfiguration(endpointName); + config.UseSerialization(); + config.UseTransport(new LearningTransport()); + config.AssemblyScanner().Disable = !assemblyScanningEnabled; + return config; + } +} \ No newline at end of file From 2320de81b4b396951291b49fd7b7c4c53a80d725 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 14:24:20 +0200 Subject: [PATCH 03/16] Refactor validation logic and enhance error messages for assembly scanning and transport reuse --- .../Hosting/ServiceCollectionExtensions.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs index abbed9efe8..e21df10644 100644 --- a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs @@ -40,9 +40,9 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi var transport = settings.Get().TransportDefinition; var registrations = GetExistingInstallerRegistrations(services); - // ValidateEndpointIdentifier(endpointIdentifier, registrations); - // ValidateAssemblyScanning(endpointConfiguration, endpointName, registrations); - // ValidateTransportReuse(transport, registrations); + ValidateEndpointIdentifier(endpointIdentifier, [..registrations]); + ValidateAssemblyScanning(endpointConfiguration, endpointName, [..registrations], message: "its installers using the corresponding registrations methods like AddInstaller()"); + ValidateTransportReuse(transport, [..registrations]); if (endpointIdentifier is null) { @@ -180,7 +180,7 @@ static void ValidateEndpointIdentifier(object? endpointIdentifier, List registrations) + static void ValidateAssemblyScanning(EndpointConfiguration endpointConfiguration, string endpointName, List registrations, string message ="its handlers and sagas using the corresponding registrations methods like AddHandler(), AddSaga() etc") { if (endpointConfiguration.GetSettings().HasSetting("NServiceBus.Hosting.DisableAssemblyScanningValidation")) { @@ -205,7 +205,7 @@ static void ValidateAssemblyScanning(EndpointConfiguration endpointConfiguration { throw new InvalidOperationException( $"When multiple endpoints are registered, each endpoint must disable assembly scanning " + - $"(cfg.AssemblyScanner().Disable = true) and explicitly register its handlers using AddHandler(). " + + $"(cfg.AssemblyScanner().Disable = true) and explicitly register {message}. " + $"The following endpoints have assembly scanning enabled: {string.Join(", ", endpointsWithScanning.Select(n => $"'{n}'"))}."); } } @@ -232,6 +232,7 @@ [.. services .Select(d => (EndpointInstallerRegistration)d.ImplementationInstance!)]; - sealed record EndpointRegistration(string EndpointName, object? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode); - sealed record EndpointInstallerRegistration(string EndpointName, object? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode); + record EndpointRegistration(string EndpointName, object? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode); + + record EndpointInstallerRegistration(string EndpointName, object? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode) : EndpointRegistration(EndpointName, EndpointIdentifier, ScanningDisabled, TransportHashCode); } \ No newline at end of file From a417d5a9d4200be24139706414bbf01af56070fb Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 14:31:43 +0200 Subject: [PATCH 04/16] Add tests to validate mutual exclusivity of `AddNServiceBusEndpoint` and `AddNServiceBusEndpointInstaller` --- .../ServiceCollectionExtensions_AddEndpoint_Tests.cs | 12 ++++++++++++ ...ServiceCollectionExtensions_AddInstaller_Tests.cs | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs index 7a505b6400..c82ecc44ec 100644 --- a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs @@ -100,6 +100,18 @@ public void Should_throw_when_multiple_endpoints_assembly_scanning_enabled() Assert.That(ex!.Message, Does.Contain("When multiple endpoints are registered, each endpoint must disable assembly scanning (cfg.AssemblyScanner().Disable = true) and explicitly register its handlers and sagas using the corresponding registrations methods like AddHandler(), AddSaga() etc. The following endpoints have assembly scanning enabled: 'Billing'.")); } + [Test] + public void Should_throw_when_used_with_add_installer() + { + var services = new ServiceCollection(); + + services.AddNServiceBusEndpoint(CreateConfig("Sales"), "Sales"); + + var ex = Assert.Throws(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Billing"), "Billing")); + + Assert.That(ex!.Message, Does.Contain("'AddNServiceBusEndpointInstaller' cannot be used together with 'AddNServiceBusEndpoint'.")); + } + static EndpointConfiguration CreateConfig(string endpointName, bool assemblyScanningEnabled = false) { var config = new EndpointConfiguration(endpointName); diff --git a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs index 5ee3ce7982..b1c1b37e44 100644 --- a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs @@ -100,6 +100,18 @@ public void Should_throw_when_multiple_endpoints_assembly_scanning_enabled() Assert.That(ex!.Message, Does.Contain("When multiple endpoints are registered, each endpoint must disable assembly scanning (cfg.AssemblyScanner().Disable = true) and explicitly register its installers using the corresponding registrations methods like AddInstaller(). The following endpoints have assembly scanning enabled: 'Billing'.")); } + [Test] + public void Should_throw_when_used_with_add_installer() + { + var services = new ServiceCollection(); + + services.AddNServiceBusEndpointInstaller(CreateConfig("Sales"), "Sales"); + + var ex = Assert.Throws(() => services.AddNServiceBusEndpoint(CreateConfig("Billing"), "Billing")); + + Assert.That(ex!.Message, Does.Contain("'AddNServiceBusEndpoint' cannot be used together with 'AddNServiceBusEndpointInstaller'.")); + } + static EndpointConfiguration CreateConfig(string endpointName, bool assemblyScanningEnabled = false) { var config = new EndpointConfiguration(endpointName); From 02731aae95d7e67bb3a247a6f1eae52bc9ced189 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 14:31:50 +0200 Subject: [PATCH 05/16] Ensure mutual exclusivity validation between `AddNServiceBusEndpoint` and `AddNServiceBusEndpointInstaller` --- .../Hosting/ServiceCollectionExtensions.cs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs index e21df10644..46bfac7168 100644 --- a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs @@ -38,11 +38,13 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi var endpointName = settings.EndpointName(); var transport = settings.Get().TransportDefinition; - var registrations = GetExistingInstallerRegistrations(services); + var installerRegistrations = GetExistingInstallerRegistrations(services); + var endpointRegistrations = GetExistingRegistrations(services); - ValidateEndpointIdentifier(endpointIdentifier, [..registrations]); - ValidateAssemblyScanning(endpointConfiguration, endpointName, [..registrations], message: "its installers using the corresponding registrations methods like AddInstaller()"); - ValidateTransportReuse(transport, [..registrations]); + ValidateNotUsedWithAddNServiceBusEndpoints(endpointRegistrations, $"'{nameof(AddNServiceBusEndpointInstaller)}' cannot be used together with '{nameof(AddNServiceBusEndpoint)}'."); + ValidateEndpointIdentifier(endpointIdentifier, [..installerRegistrations]); + ValidateAssemblyScanning(endpointConfiguration, endpointName, [..installerRegistrations], message: "its installers using the corresponding registrations methods like AddInstaller()"); + ValidateTransportReuse(transport, [..installerRegistrations]); if (endpointIdentifier is null) { @@ -68,6 +70,14 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi services.AddSingleton(new EndpointInstallerRegistration(endpointName, endpointIdentifier, endpointConfiguration.AssemblyScanner().Disable, RuntimeHelpers.GetHashCode(transport))); } + static void ValidateNotUsedWithAddNServiceBusEndpoints(List endpointRegistrations, string message) + { + if (endpointRegistrations.Count > 0) + { + throw new InvalidOperationException(message); + } + } + /// /// Registers an NServiceBus endpoint with the dependency injection container, enabling the endpoint /// to resolve services from the application's service provider and participate in the hosted service lifecycle. @@ -124,11 +134,13 @@ public static void AddNServiceBusEndpoint( var endpointName = settings.EndpointName(); var hostingSettings = settings.Get(); var transport = settings.Get().TransportDefinition; - var registrations = GetExistingRegistrations(services); + var endpointRegistrations = GetExistingRegistrations(services); + var installerRegistrations = GetExistingInstallerRegistrations(services); - ValidateEndpointIdentifier(endpointIdentifier, registrations); - ValidateAssemblyScanning(endpointConfiguration, endpointName, registrations); - ValidateTransportReuse(transport, registrations); + ValidateNotUsedWithAddNServiceBusEndpoints([..installerRegistrations], $"'{nameof(AddNServiceBusEndpoint)}' cannot be used together with '{nameof(AddNServiceBusEndpointInstaller)}'."); + ValidateEndpointIdentifier(endpointIdentifier, endpointRegistrations); + ValidateAssemblyScanning(endpointConfiguration, endpointName, endpointRegistrations); + ValidateTransportReuse(transport, endpointRegistrations); hostingSettings.ConfigureHostLogging(endpointIdentifier); From ee1b9a64af1c30f68d93b0fe5b8c73d8ffcd03b2 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 14:39:47 +0200 Subject: [PATCH 06/16] Refactor `ServiceCollectionExtensions` to improve validation logic, enhance documentation, and consolidate registration retrieval methods. --- .../Hosting/ServiceCollectionExtensions.cs | 84 +++++++++++++------ 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs index 46bfac7168..40297dcf59 100644 --- a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs @@ -18,12 +18,46 @@ namespace NServiceBus; public static class ServiceCollectionExtensions { /// - /// + /// Registers an NServiceBus endpoint installer with the dependency injection container, enabling the endpoint installers + /// to participate in the hosted service lifecycle. /// - /// - /// - /// - /// + /// Registering installers is mutually exclusive to registering endpoints. This is to force the installation API being used on a dedicated host that is only used to run installers + /// for example as part of a deployment. + /// The to add the endpoint to. + /// The defining how the endpoint should be configured. + /// + /// An optional identifier that uniquely identifies this endpoint within the dependency injection container. + /// When multiple endpoints are registered (by calling this method multiple times), this parameter is required + /// and must be a well-defined value that serves as a keyed service identifier. + /// + /// In most scenarios, using the endpoint name as the identifier is a good choice. + /// + /// + /// For more complex scenarios such as multi-tenant applications where endpoint infrastructure + /// per tenant is dynamically resolved, the identifier can be any object that implements + /// and in a way that conforms to Microsoft Dependency Injection keyed services assumptions. + /// The key is used with keyed service registration methods like AddKeyedSingleton and related methods, + /// and can be retrieved using keyed service resolution APIs like GetRequiredKeyedService or + /// the [FromKeyedServices] attribute on constructor parameters. + /// + /// + /// + /// + /// When using a keyed endpoint, all services resolved within NServiceBus extension points + /// (message handlers, sagas, features, installers, etc.) are automatically resolved as keyed services + /// for that endpoint and do not require the [FromKeyedServices] attribute. + /// Conversely, the [FromKeyedServices] attribute is required when accessing endpoint-specific services + /// (such as ) outside of NServiceBus extension points, for example in controllers + /// or background jobs. + /// + /// + /// By default, only endpoint-specific registrations are resolved when resolving all services of a given type + /// within an endpoint. However, for advanced scenarios where global services registered on the shared + /// service collection need to be resolved along with endpoint-specific ones, use + /// with the [FromKeyedServices] attribute (for example: [FromKeyedServices(KeyedServiceKey.Any)] IEnumerable<IMyService>). + /// This bypasses the default safeguards that isolate endpoints, allowing resolution of all services including + /// globally shared ones. + /// public static void AddNServiceBusEndpointInstaller(this IServiceCollection services, EndpointConfiguration endpointConfiguration, object? endpointIdentifier = null) @@ -38,13 +72,13 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi var endpointName = settings.EndpointName(); var transport = settings.Get().TransportDefinition; - var installerRegistrations = GetExistingInstallerRegistrations(services); - var endpointRegistrations = GetExistingRegistrations(services); + var installerRegistrations = GetExistingRegistrations(services); + var endpointRegistrations = GetExistingRegistrations(services); ValidateNotUsedWithAddNServiceBusEndpoints(endpointRegistrations, $"'{nameof(AddNServiceBusEndpointInstaller)}' cannot be used together with '{nameof(AddNServiceBusEndpoint)}'."); - ValidateEndpointIdentifier(endpointIdentifier, [..installerRegistrations]); - ValidateAssemblyScanning(endpointConfiguration, endpointName, [..installerRegistrations], message: "its installers using the corresponding registrations methods like AddInstaller()"); - ValidateTransportReuse(transport, [..installerRegistrations]); + ValidateEndpointIdentifier(endpointIdentifier, installerRegistrations); + ValidateAssemblyScanning(endpointConfiguration, endpointName, installerRegistrations, message: "its installers using the corresponding registrations methods like AddInstaller()"); + ValidateTransportReuse(transport, installerRegistrations); if (endpointIdentifier is null) { @@ -70,7 +104,7 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi services.AddSingleton(new EndpointInstallerRegistration(endpointName, endpointIdentifier, endpointConfiguration.AssemblyScanner().Disable, RuntimeHelpers.GetHashCode(transport))); } - static void ValidateNotUsedWithAddNServiceBusEndpoints(List endpointRegistrations, string message) + static void ValidateNotUsedWithAddNServiceBusEndpoints(List endpointRegistrations, string message) { if (endpointRegistrations.Count > 0) { @@ -134,10 +168,10 @@ public static void AddNServiceBusEndpoint( var endpointName = settings.EndpointName(); var hostingSettings = settings.Get(); var transport = settings.Get().TransportDefinition; - var endpointRegistrations = GetExistingRegistrations(services); - var installerRegistrations = GetExistingInstallerRegistrations(services); + var endpointRegistrations = GetExistingRegistrations(services); + var installerRegistrations = GetExistingRegistrations(services); - ValidateNotUsedWithAddNServiceBusEndpoints([..installerRegistrations], $"'{nameof(AddNServiceBusEndpoint)}' cannot be used together with '{nameof(AddNServiceBusEndpointInstaller)}'."); + ValidateNotUsedWithAddNServiceBusEndpoints(installerRegistrations, $"'{nameof(AddNServiceBusEndpoint)}' cannot be used together with '{nameof(AddNServiceBusEndpointInstaller)}'."); ValidateEndpointIdentifier(endpointIdentifier, endpointRegistrations); ValidateAssemblyScanning(endpointConfiguration, endpointName, endpointRegistrations); ValidateTransportReuse(transport, endpointRegistrations); @@ -172,7 +206,8 @@ public static void AddNServiceBusEndpoint( internal static IServiceCollection Unwrap(this IServiceCollection services) => (services as KeyedServiceCollectionAdapter)?.Inner ?? services; - static void ValidateEndpointIdentifier(object? endpointIdentifier, List registrations) + static void ValidateEndpointIdentifier(object? endpointIdentifier, List registrations) + where TRegistration : EndpointRegistration { if (registrations.Count == 0) { @@ -192,7 +227,8 @@ static void ValidateEndpointIdentifier(object? endpointIdentifier, List registrations, string message ="its handlers and sagas using the corresponding registrations methods like AddHandler(), AddSaga() etc") + static void ValidateAssemblyScanning(EndpointConfiguration endpointConfiguration, string endpointName, List registrations, string message ="its handlers and sagas using the corresponding registrations methods like AddHandler(), AddSaga() etc") + where TRegistration : EndpointRegistration { if (endpointConfiguration.GetSettings().HasSetting("NServiceBus.Hosting.DisableAssemblyScanningValidation")) { @@ -222,7 +258,8 @@ static void ValidateEndpointIdentifier(object? endpointIdentifier, List registrations) + static void ValidateTransportReuse(TransportDefinition transport, List registrations) + where TRegistration : EndpointRegistration { var transportHash = RuntimeHelpers.GetHashCode(transport); var existingRegistration = registrations.FirstOrDefault(r => r.TransportHashCode == transportHash); @@ -233,16 +270,11 @@ static void ValidateTransportReuse(TransportDefinition transport, List GetExistingRegistrations(IServiceCollection services) => + static List GetExistingRegistrations(IServiceCollection services) + where TRegistration : EndpointRegistration => [.. services - .Where(d => d.ServiceType == typeof(EndpointRegistration) && d.ImplementationInstance is EndpointRegistration) - .Select(d => (EndpointRegistration)d.ImplementationInstance!)]; - - static List GetExistingInstallerRegistrations(IServiceCollection services) => - [.. services - .Where(d => d.ServiceType == typeof(EndpointInstallerRegistration) && d.ImplementationInstance is EndpointInstallerRegistration) - .Select(d => (EndpointInstallerRegistration)d.ImplementationInstance!)]; - + .Where(d => d.ServiceType == typeof(TRegistration) && d.ImplementationInstance is TRegistration) + .Select(d => (TRegistration)d.ImplementationInstance!)]; record EndpointRegistration(string EndpointName, object? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode); From a1ad5ed4d355c9e0dd24780347269586f1257ba4 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 14:40:47 +0200 Subject: [PATCH 07/16] ApiApprovals --- .../ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 861e06ce49..2109edcdf7 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1042,6 +1042,7 @@ namespace NServiceBus public static class ServiceCollectionExtensions { public static void AddNServiceBusEndpoint(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, NServiceBus.EndpointConfiguration endpointConfiguration, object? endpointIdentifier = null) { } + public static void AddNServiceBusEndpointInstaller(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, NServiceBus.EndpointConfiguration endpointConfiguration, object? endpointIdentifier = null) { } } public static class SettingsExtensions { From 03ded5b1f8fdb64d00b206e4115f2e3f1302a72c Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 18:15:07 +0200 Subject: [PATCH 08/16] Delete InstallationOnlyComponent because it was only used in Core folder and therefore not shipped --- .../Installers/InstallationOnlyComponent.cs | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/NServiceBus.AcceptanceTests/Core/Installers/InstallationOnlyComponent.cs diff --git a/src/NServiceBus.AcceptanceTests/Core/Installers/InstallationOnlyComponent.cs b/src/NServiceBus.AcceptanceTests/Core/Installers/InstallationOnlyComponent.cs deleted file mode 100644 index 3530475c48..0000000000 --- a/src/NServiceBus.AcceptanceTests/Core/Installers/InstallationOnlyComponent.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace NServiceBus.AcceptanceTests.Core.Installers; - -using System.Threading; -using System.Threading.Tasks; -using AcceptanceTesting; -using AcceptanceTesting.Customization; -using AcceptanceTesting.Support; -using Configuration.AdvancedExtensibility; -using Installation; - -/// -/// Custom test component that uses the API instead of fully starting the endpoint. -/// -public class InstallationOnlyComponent : IComponentBehavior - where TConfigurationFactory : IEndpointConfigurationFactory, new() -{ - public async Task CreateRunner(RunDescriptor run) - { - var configurationFactory = new TConfigurationFactory(); - var customizationConfiguration = configurationFactory.Get(); - customizationConfiguration.EndpointName = Conventions.EndpointNamingConvention(configurationFactory.GetType()); - var endpointConfiguration = await customizationConfiguration.GetConfiguration(run); - RegisterScenarioContext(endpointConfiguration, run.ScenarioContext); - var installer = Installer.CreateInstallerWithExternallyManagedContainer(endpointConfiguration, run.Services); - return new InstallationRunner(installer, run); - } - - static void RegisterScenarioContext(EndpointConfiguration endpointConfiguration, ScenarioContext scenarioContext) - { - var type = scenarioContext.GetType(); - var settings = endpointConfiguration.GetSettings(); - - while (type != typeof(object)) - { - var currentType = type; - settings.Set(currentType.FullName, scenarioContext); - type = type.BaseType; - } - } - - public class InstallationRunner(InstallerWithExternallyManagedContainer installer, RunDescriptor run) : ComponentRunner - { - public override string Name => "Installation only runner"; - - public override Task Start(CancellationToken cancellationToken = default) => installer.Setup(run.ServiceProvider!, cancellationToken); - } -} \ No newline at end of file From ffe224f80b59d9f73735d03e31dfef0eea130073 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 18:15:29 +0200 Subject: [PATCH 09/16] Use the endpoint lifecycle with create since that only does a prepare and setup --- .../Hosting/EndpointHostedInstallerService.cs | 9 ++++++--- .../Hosting/ServiceCollectionExtensions.cs | 16 ++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs b/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs index 38eea066dd..19eb3ed1f1 100644 --- a/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs +++ b/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs @@ -4,12 +4,15 @@ namespace NServiceBus; using System; using System.Threading; using System.Threading.Tasks; -using Installation; using Microsoft.Extensions.Hosting; -class EndpointHostedInstallerService(InstallerWithExternallyManagedContainer externallyManagedInstallerHost, IServiceProvider sp) : IHostedService +class EndpointHostedInstallerService(IEndpointLifecycle endpointLifecycle) : IHostedService, IAsyncDisposable { - public Task StartAsync(CancellationToken cancellationToken = default) => externallyManagedInstallerHost.Setup(sp, cancellationToken); + public async Task StartAsync(CancellationToken cancellationToken = default) + // we only ever create but not start + => await endpointLifecycle.Create(cancellationToken).ConfigureAwait(false); public Task StopAsync(CancellationToken cancellationToken= default) => Task.CompletedTask; + + public ValueTask DisposeAsync() => endpointLifecycle.DisposeAsync(); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs index 40297dcf59..83386c3f75 100644 --- a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs @@ -80,13 +80,16 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi ValidateAssemblyScanning(endpointConfiguration, endpointName, installerRegistrations, message: "its installers using the corresponding registrations methods like AddInstaller()"); ValidateTransportReuse(transport, installerRegistrations); + endpointConfiguration.EnableInstallers(); + if (endpointIdentifier is null) { // Deliberately creating it here to make sure we are not accidentally doing it too late. - var externallyManagedInstallerHost = InstallerExternallyManaged.Create(endpointConfiguration, services); + var externallyManagedContainerHost = EndpointExternallyManaged.Create(endpointConfiguration, services); - services.AddSingleton(externallyManagedInstallerHost); - services.AddSingleton(sp => new EndpointHostedInstallerService(externallyManagedInstallerHost, sp)); + services.AddSingleton(externallyManagedContainerHost); + services.AddSingleton(sp => new BaseEndpointLifecycle(externallyManagedContainerHost, sp)); + services.AddSingleton(sp => new EndpointHostedInstallerService(sp.GetRequiredService())); } else { @@ -95,10 +98,11 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi var baseKey = keyedServices.ServiceKey.BaseKey; // Deliberately creating it here to make sure we are not accidentally doing it too late. - var externallyManagedInstallerHost = InstallerExternallyManaged.Create(endpointConfiguration, keyedServices); + var externallyManagedContainerHost = EndpointExternallyManaged.Create(endpointConfiguration, keyedServices); - services.AddKeyedSingleton(baseKey, externallyManagedInstallerHost); - services.AddSingleton(sp => new EndpointHostedInstallerService(sp.GetRequiredKeyedService(baseKey), sp)); + services.AddKeyedSingleton(baseKey, externallyManagedContainerHost); + services.AddKeyedSingleton(baseKey, (sp, _) => new EndpointLifecycle(externallyManagedContainerHost, sp, keyedServices.ServiceKey, keyedServices)); + services.AddSingleton(sp => new EndpointHostedInstallerService(sp.GetRequiredKeyedService(baseKey))); } services.AddSingleton(new EndpointInstallerRegistration(endpointName, endpointIdentifier, endpointConfiguration.AssemblyScanner().Disable, RuntimeHelpers.GetHashCode(transport))); From 8cf7963db0cabde7660fc50ff1dc552d7a4cffa7 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 18:18:58 +0200 Subject: [PATCH 10/16] Introduce a shared convenience method to run hosted services --- .../Installers/ServiceProviderExtensions.cs | 35 ++++++ .../Installers/When_installing_endpoint.cs | 84 ++++++------- .../Installers/When_installing_endpoints.cs | 114 ++++++++++++++++++ 3 files changed, 189 insertions(+), 44 deletions(-) create mode 100644 src/NServiceBus.AcceptanceTests/Core/Installers/ServiceProviderExtensions.cs create mode 100644 src/NServiceBus.AcceptanceTests/Core/Installers/When_installing_endpoints.cs diff --git a/src/NServiceBus.AcceptanceTests/Core/Installers/ServiceProviderExtensions.cs b/src/NServiceBus.AcceptanceTests/Core/Installers/ServiceProviderExtensions.cs new file mode 100644 index 0000000000..81e7469904 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/Installers/ServiceProviderExtensions.cs @@ -0,0 +1,35 @@ +namespace NServiceBus.AcceptanceTests.Core.Installers; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +static class ServiceProviderExtensions +{ + extension(IServiceProvider provider) + { + public async Task RunHostedServices(CancellationToken cancellationToken = default) + { + // We don't have host support in the acceptance tests, so we need to manually start/stop the services + var hostedServices = provider.GetServices().ToList(); + foreach (var hostedService in hostedServices) + { + await hostedService.StartAsync(cancellationToken); + } + + hostedServices.Reverse(); + foreach (var hostedService in hostedServices) + { + await hostedService.StopAsync(cancellationToken); + + if (hostedService is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Core/Installers/When_installing_endpoint.cs b/src/NServiceBus.AcceptanceTests/Core/Installers/When_installing_endpoint.cs index 0e042c3b51..a3410e4fdf 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Installers/When_installing_endpoint.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Installers/When_installing_endpoint.cs @@ -1,9 +1,10 @@ namespace NServiceBus.AcceptanceTests.Core.Installers; +using System; using System.Threading; using System.Threading.Tasks; using AcceptanceTesting; -using EndpointTemplates; +using Configuration.AdvancedExtensibility; using FakeTransport; using Features; using Installation; @@ -15,79 +16,74 @@ public class When_installing_endpoint : NServiceBusAcceptanceTest [Test] public async Task Should_only_execute_setup_and_complete() { - var context = await Scenario.Define() - .WithComponent(new InstallationOnlyComponent()) + var fakeTransport = new FakeTransport(); + var endpointConfiguration = new EndpointConfiguration("EndpointWithInstaller"); + endpointConfiguration.AssemblyScanner().Disable = true; + endpointConfiguration.UsePersistence(); + endpointConfiguration.UseTransport(fakeTransport); + endpointConfiguration.UseSerialization(); + + endpointConfiguration.EnableFeature(); + + var context = await Scenario.Define(ctx => + { + endpointConfiguration.GetSettings().Set(ctx); + }) + .WithServices(services => services.AddNServiceBusEndpointInstaller(endpointConfiguration)) + .WithServiceResolve(static async (provider, token) => await provider.RunHostedServices(token)) .Run(); using (Assert.EnterMultipleScope()) { Assert.That(context.InstallerCalled, Is.True, "Should run installers"); Assert.That(context.FeatureSetupCalled, Is.True, "Should initialize Features"); - Assert.That(context.FeatureStartupTaskCalled, Is.False, "Should not start FeatureStartupTasks"); } Assert.That(new[] { $"{nameof(TransportDefinition)}.{nameof(TransportDefinition.Initialize)}", $"{nameof(IMessageReceiver)}.{nameof(IMessageReceiver.Initialize)} for receiver Main", - }, Is.EqualTo(context.TransportStartupSequence).AsCollection, "Should not start the receivers"); + }, Is.EqualTo(fakeTransport.StartupSequence).AsCollection, "Should not start the receivers"); } class Context : ScenarioContext { public bool InstallerCalled { get; set; } public bool FeatureSetupCalled { get; set; } - public bool FeatureStartupTaskCalled { get; set; } - public FakeTransport.StartUpSequence TransportStartupSequence { get; set; } - public void MaybeCompleted() => MarkAsCompleted(InstallerCalled, FeatureSetupCalled, TransportStartupSequence != null); + public void MaybeCompleted() => MarkAsCompleted(InstallerCalled, FeatureSetupCalled); } - class EndpointWithInstaller : EndpointConfigurationBuilder + class CustomInstaller(Context testContext) : INeedToInstallSomething { - public EndpointWithInstaller() => - EndpointSetup((c, r) => - { - c.EnableFeature(); - - // Register FakeTransport to track transport seam usage during installation - var fakeTransport = new FakeTransport(); - c.UseTransport(fakeTransport); - ((Context)r.ScenarioContext).TransportStartupSequence = fakeTransport.StartupSequence; - }); - - class CustomInstaller(Context testContext) : INeedToInstallSomething + public Task Install(string identity, CancellationToken cancellationToken = default) { - public Task Install(string identity, CancellationToken cancellationToken = default) - { - testContext.InstallerCalled = true; - testContext.MaybeCompleted(); - return Task.CompletedTask; - } + testContext.InstallerCalled = true; + testContext.MaybeCompleted(); + return Task.CompletedTask; } + } - class CustomFeature : Feature + class CustomFeature : Feature + { + protected override void Setup(FeatureConfigurationContext context) { - protected override void Setup(FeatureConfigurationContext context) - { - context.AddInstaller(); + context.AddInstaller(); - var testContext = context.Settings.Get(); - testContext.FeatureSetupCalled = true; + var testContext = context.Settings.Get(); + testContext.FeatureSetupCalled = true; - context.RegisterStartupTask(new CustomFeatureStartupTask(testContext)); - } + context.RegisterStartupTask(new CustomFeatureStartupTask(testContext)); + } - class CustomFeatureStartupTask(Context testContext) : FeatureStartupTask + class CustomFeatureStartupTask(Context testContext) : FeatureStartupTask + { + protected override Task OnStart(IMessageSession session, CancellationToken cancellationToken = default) { - protected override Task OnStart(IMessageSession session, CancellationToken cancellationToken = default) - { - testContext.FeatureStartupTaskCalled = true; - testContext.MaybeCompleted(); - return Task.CompletedTask; - } - - protected override Task OnStop(IMessageSession session, CancellationToken cancellationToken = default) => Task.CompletedTask; + testContext.MarkAsFailed(new InvalidOperationException("FeatureStartupTask should not be called")); + return Task.CompletedTask; } + + protected override Task OnStop(IMessageSession session, CancellationToken cancellationToken = default) => Task.CompletedTask; } } } \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Core/Installers/When_installing_endpoints.cs b/src/NServiceBus.AcceptanceTests/Core/Installers/When_installing_endpoints.cs new file mode 100644 index 0000000000..7fb0e028ca --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/Installers/When_installing_endpoints.cs @@ -0,0 +1,114 @@ +namespace NServiceBus.AcceptanceTests.Core.Installers; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AcceptanceTesting; +using Configuration.AdvancedExtensibility; +using FakeTransport; +using Features; +using Installation; +using NUnit.Framework; +using Settings; +using Transport; + +public class When_installing_endpoints : NServiceBusAcceptanceTest +{ + [Test] + public async Task Should_only_execute_setup_and_complete() + { + var transport1 = new FakeTransport(); + var endpointConfiguration1 = CreateEndpointConfiguration(transport1, "EndpointWithInstaller1"); + + var transport2 = new FakeTransport(); + EndpointConfiguration endpointConfiguration2 = CreateEndpointConfiguration(transport2, "EndpointWithInstaller2"); + + var context = await Scenario.Define(ctx => + { + endpointConfiguration1.GetSettings().Set(ctx); + endpointConfiguration2.GetSettings().Set(ctx); + }) + .WithServices(services => + { + services.AddNServiceBusEndpointInstaller(endpointConfiguration1, "EndpointWithInstaller1"); + services.AddNServiceBusEndpointInstaller(endpointConfiguration2, "EndpointWithInstaller2"); + }) + .WithServiceResolve(static async (provider, token) => await provider.RunHostedServices(token)) + .Run(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.InstallerCalled.All(x => x.Value), Is.True, "Should run installers"); + Assert.That(context.FeatureSetupCalled.All(x => x.Value), Is.True, "Should initialize Features"); + } + + using (Assert.EnterMultipleScope()) + { + Assert.That(new[] + { + $"{nameof(TransportDefinition)}.{nameof(TransportDefinition.Initialize)}", $"{nameof(IMessageReceiver)}.{nameof(IMessageReceiver.Initialize)} for receiver Main", + }, Is.EqualTo(transport1.StartupSequence).AsCollection, "Should not start the receivers"); + Assert.That(new[] + { + $"{nameof(TransportDefinition)}.{nameof(TransportDefinition.Initialize)}", $"{nameof(IMessageReceiver)}.{nameof(IMessageReceiver.Initialize)} for receiver Main", + }, Is.EqualTo(transport2.StartupSequence).AsCollection, "Should not start the receivers"); + } + } + + static EndpointConfiguration CreateEndpointConfiguration(FakeTransport transport, string endpointName) + { + var endpointConfiguration = new EndpointConfiguration(endpointName); + endpointConfiguration.AssemblyScanner().Disable = true; + endpointConfiguration.UsePersistence(); + endpointConfiguration.UseTransport(transport); + endpointConfiguration.UseSerialization(); + endpointConfiguration.EnableFeature(); + return endpointConfiguration; + } + + class Context : ScenarioContext + { + public Dictionary InstallerCalled { get; set; } = []; + public Dictionary FeatureSetupCalled { get; set; } = []; + + public void MaybeCompleted() => MarkAsCompleted(InstallerCalled.Count == 2, FeatureSetupCalled.Count == 2); + } + + class CustomInstaller(Context testContext, IReadOnlySettings settings) : INeedToInstallSomething + { + public Task Install(string identity, CancellationToken cancellationToken = default) + { + testContext.InstallerCalled[settings.EndpointName()] = true; + testContext.MaybeCompleted(); + return Task.CompletedTask; + } + } + + class CustomFeature : Feature + { + protected override void Setup(FeatureConfigurationContext context) + { + context.AddInstaller(); + + var settings = context.Settings; + + var testContext = settings.Get(); + testContext.FeatureSetupCalled[settings.EndpointName()] = true; + + context.RegisterStartupTask(new CustomFeatureStartupTask(testContext)); + } + + class CustomFeatureStartupTask(Context testContext) : FeatureStartupTask + { + protected override Task OnStart(IMessageSession session, CancellationToken cancellationToken = default) + { + testContext.MarkAsFailed(new InvalidOperationException("FeatureStartupTask should not be called")); + return Task.CompletedTask; + } + + protected override Task OnStop(IMessageSession session, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + } +} \ No newline at end of file From 85d7a2da9603dce078743315fe9327ba82884623 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 18:24:00 +0200 Subject: [PATCH 11/16] Fix warnings --- .../Hosting/EndpointHostedInstallerService.cs | 2 +- src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs b/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs index 19eb3ed1f1..fdda25504b 100644 --- a/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs +++ b/src/NServiceBus.Core/Hosting/EndpointHostedInstallerService.cs @@ -12,7 +12,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default) // we only ever create but not start => await endpointLifecycle.Create(cancellationToken).ConfigureAwait(false); - public Task StopAsync(CancellationToken cancellationToken= default) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public ValueTask DisposeAsync() => endpointLifecycle.DisposeAsync(); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs index 83386c3f75..7e5b213cc8 100644 --- a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs @@ -7,7 +7,6 @@ namespace NServiceBus; using System.Linq; using System.Runtime.CompilerServices; using Configuration.AdvancedExtensibility; -using Installation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Transport; @@ -41,7 +40,6 @@ public static class ServiceCollectionExtensions /// the [FromKeyedServices] attribute on constructor parameters. /// /// - /// /// /// When using a keyed endpoint, all services resolved within NServiceBus extension points /// (message handlers, sagas, features, installers, etc.) are automatically resolved as keyed services @@ -231,7 +229,7 @@ static void ValidateEndpointIdentifier(object? endpointIdentifier } } - static void ValidateAssemblyScanning(EndpointConfiguration endpointConfiguration, string endpointName, List registrations, string message ="its handlers and sagas using the corresponding registrations methods like AddHandler(), AddSaga() etc") + static void ValidateAssemblyScanning(EndpointConfiguration endpointConfiguration, string endpointName, List registrations, string message = "its handlers and sagas using the corresponding registrations methods like AddHandler(), AddSaga() etc") where TRegistration : EndpointRegistration { if (endpointConfiguration.GetSettings().HasSetting("NServiceBus.Hosting.DisableAssemblyScanningValidation")) From d8a83c7dd9ada12a30fffd3a5c212dfb745878ad Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 18:24:41 +0200 Subject: [PATCH 12/16] Deprecate the installer APIs --- ...IApprovals.ApproveNServiceBus.approved.txt | 3 +++ .../Installation/Installer.cs | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 2109edcdf7..d84dd4cc79 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1574,9 +1574,12 @@ namespace NServiceBus.Installation { System.Threading.Tasks.Task Install(string identity, System.Threading.CancellationToken cancellationToken = default); } + [System.Obsolete(@"Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static class Installer { + [System.Obsolete(@"Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static NServiceBus.Installation.InstallerWithExternallyManagedContainer CreateInstallerWithExternallyManagedContainer(NServiceBus.EndpointConfiguration configuration, Microsoft.Extensions.DependencyInjection.IServiceCollection serviceCollection) { } + [System.Obsolete(@"Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static System.Threading.Tasks.Task Setup(NServiceBus.EndpointConfiguration configuration, System.Threading.CancellationToken cancellationToken = default) { } } public class InstallerWithExternallyManagedContainer diff --git a/src/NServiceBus.Core/Installation/Installer.cs b/src/NServiceBus.Core/Installation/Installer.cs index f98d032573..8c4e1bb6a8 100644 --- a/src/NServiceBus.Core/Installation/Installer.cs +++ b/src/NServiceBus.Core/Installation/Installer.cs @@ -6,16 +6,29 @@ namespace NServiceBus.Installation; using Microsoft.Extensions.DependencyInjection; using System.Threading.Tasks; using System.Threading; +using Particular.Obsoletes; /// /// Provides methods to setup an NServiceBus endpoint. /// +[ObsoleteMetadata( + Message = "Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12", + ReplacementTypeOrMember = "IServiceCollection.AddNServiceBusEndpointInstaller")] +[Obsolete("Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static class Installer { /// /// Executes all the installers and transport configuration without starting the endpoint. /// always runs installers, even if has not been configured. /// + [ObsoleteMetadata( + Message = "Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12", + ReplacementTypeOrMember = "IServiceCollection.AddNServiceBusEndpointInstaller")] + [Obsolete("Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static async Task Setup(EndpointConfiguration configuration, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(configuration); @@ -38,6 +51,12 @@ public static async Task Setup(EndpointConfiguration configuration, Cancellation /// /// Creates an instance of that can be used to setup an NServiceBus when access to externally registered container dependencies are required. /// + [ObsoleteMetadata( + Message = "Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12", + ReplacementTypeOrMember = "IServiceCollection.AddNServiceBusEndpointInstaller")] + [Obsolete("Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static InstallerWithExternallyManagedContainer CreateInstallerWithExternallyManagedContainer(EndpointConfiguration configuration, IServiceCollection serviceCollection) { ArgumentNullException.ThrowIfNull(configuration); From 2a9fab469e436217a4ed2992cfe9dcb5efe796d0 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 1 Apr 2026 18:25:32 +0200 Subject: [PATCH 13/16] Different wrapping --- .../Hosting/ServiceCollectionExtensions.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs index 7e5b213cc8..2510bf589d 100644 --- a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs @@ -56,9 +56,7 @@ public static class ServiceCollectionExtensions /// This bypasses the default safeguards that isolate endpoints, allowing resolution of all services including /// globally shared ones. /// - public static void AddNServiceBusEndpointInstaller(this IServiceCollection services, - EndpointConfiguration endpointConfiguration, - object? endpointIdentifier = null) + public static void AddNServiceBusEndpointInstaller(this IServiceCollection services, EndpointConfiguration endpointConfiguration, object? endpointIdentifier = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(endpointConfiguration); @@ -154,10 +152,7 @@ static void ValidateNotUsedWithAddNServiceBusEndpoints(List /// - public static void AddNServiceBusEndpoint( - this IServiceCollection services, - EndpointConfiguration endpointConfiguration, - object? endpointIdentifier = null) + public static void AddNServiceBusEndpoint(this IServiceCollection services, EndpointConfiguration endpointConfiguration, object? endpointIdentifier = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(endpointConfiguration); From d0576d5a1827cd735c80d6442941b8cd111da5e3 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Thu, 2 Apr 2026 12:06:29 -0400 Subject: [PATCH 14/16] Tweak obsolete message --- .../APIApprovals.ApproveNServiceBus.approved.txt | 6 +++--- src/NServiceBus.Core/Installation/Installer.cs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index d84dd4cc79..c0deb54667 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1574,12 +1574,12 @@ namespace NServiceBus.Installation { System.Threading.Tasks.Task Install(string identity, System.Threading.CancellationToken cancellationToken = default); } - [System.Obsolete(@"Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [System.Obsolete(@"Self-hosting installers is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static class Installer { - [System.Obsolete(@"Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [System.Obsolete(@"Self-hosting installers is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static NServiceBus.Installation.InstallerWithExternallyManagedContainer CreateInstallerWithExternallyManagedContainer(NServiceBus.EndpointConfiguration configuration, Microsoft.Extensions.DependencyInjection.IServiceCollection serviceCollection) { } - [System.Obsolete(@"Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [System.Obsolete(@"Self-hosting installers is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static System.Threading.Tasks.Task Setup(NServiceBus.EndpointConfiguration configuration, System.Threading.CancellationToken cancellationToken = default) { } } public class InstallerWithExternallyManagedContainer diff --git a/src/NServiceBus.Core/Installation/Installer.cs b/src/NServiceBus.Core/Installation/Installer.cs index 8c4e1bb6a8..cad0fb2544 100644 --- a/src/NServiceBus.Core/Installation/Installer.cs +++ b/src/NServiceBus.Core/Installation/Installer.cs @@ -3,20 +3,20 @@ namespace NServiceBus.Installation; using System; -using Microsoft.Extensions.DependencyInjection; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Particular.Obsoletes; /// /// Provides methods to setup an NServiceBus endpoint. /// [ObsoleteMetadata( - Message = "Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint", + Message = "Self-hosting installers is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint", TreatAsErrorFromVersion = "11", RemoveInVersion = "12", ReplacementTypeOrMember = "IServiceCollection.AddNServiceBusEndpointInstaller")] -[Obsolete("Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] +[Obsolete("Self-hosting installers is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static class Installer { /// @@ -24,11 +24,11 @@ public static class Installer /// always runs installers, even if has not been configured. /// [ObsoleteMetadata( - Message = "Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint", + Message = "Self-hosting installers is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint", TreatAsErrorFromVersion = "11", RemoveInVersion = "12", ReplacementTypeOrMember = "IServiceCollection.AddNServiceBusEndpointInstaller")] - [Obsolete("Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [Obsolete("Self-hosting installers is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static async Task Setup(EndpointConfiguration configuration, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(configuration); @@ -52,11 +52,11 @@ public static async Task Setup(EndpointConfiguration configuration, Cancellation /// Creates an instance of that can be used to setup an NServiceBus when access to externally registered container dependencies are required. /// [ObsoleteMetadata( - Message = "Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint", + Message = "Self-hosting installers is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint", TreatAsErrorFromVersion = "11", RemoveInVersion = "12", ReplacementTypeOrMember = "IServiceCollection.AddNServiceBusEndpointInstaller")] - [Obsolete("Self-hosting an installer is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [Obsolete("Self-hosting installers is no longer recommended. Instead, consider using a Microsoft IHostApplicationBuilder-based host to manage the installation lifecycle of your endpoint. Use 'IServiceCollection.AddNServiceBusEndpointInstaller' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public static InstallerWithExternallyManagedContainer CreateInstallerWithExternallyManagedContainer(EndpointConfiguration configuration, IServiceCollection serviceCollection) { ArgumentNullException.ThrowIfNull(configuration); From 9e70ae0bcc36393485e7bc3ef4715ccf395b8ee6 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Thu, 2 Apr 2026 12:07:26 -0400 Subject: [PATCH 15/16] Remove unused class --- .../InstallerExternallyManaged.cs | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/NServiceBus.Core/InstallerExternallyManaged.cs diff --git a/src/NServiceBus.Core/InstallerExternallyManaged.cs b/src/NServiceBus.Core/InstallerExternallyManaged.cs deleted file mode 100644 index 1fe6e36fe8..0000000000 --- a/src/NServiceBus.Core/InstallerExternallyManaged.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace NServiceBus; - -using Installation; -using Microsoft.Extensions.DependencyInjection; - -static class InstallerExternallyManaged -{ - internal static InstallerWithExternallyManagedContainer Create(EndpointConfiguration configuration, - IServiceCollection serviceCollection) - { - // does not overwrite installer usernames configured by the user. - configuration.EnableInstallers(); - - var endpointCreator = EndpointCreator.Create(configuration, serviceCollection); - - return new InstallerWithExternallyManagedContainer(endpointCreator); - } -} \ No newline at end of file From 84a80ef0c19334e6a870fab3b413777a1a0716d3 Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Thu, 2 Apr 2026 15:32:21 -0400 Subject: [PATCH 16/16] Tweaks --- .../Hosting/ServiceCollectionExtensions.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs index 2510bf589d..b7f625a390 100644 --- a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs @@ -68,8 +68,8 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi var endpointName = settings.EndpointName(); var transport = settings.Get().TransportDefinition; - var installerRegistrations = GetExistingRegistrations(services); var endpointRegistrations = GetExistingRegistrations(services); + var installerRegistrations = GetExistingRegistrations(services); ValidateNotUsedWithAddNServiceBusEndpoints(endpointRegistrations, $"'{nameof(AddNServiceBusEndpointInstaller)}' cannot be used together with '{nameof(AddNServiceBusEndpoint)}'."); ValidateEndpointIdentifier(endpointIdentifier, installerRegistrations); @@ -104,14 +104,6 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi services.AddSingleton(new EndpointInstallerRegistration(endpointName, endpointIdentifier, endpointConfiguration.AssemblyScanner().Disable, RuntimeHelpers.GetHashCode(transport))); } - static void ValidateNotUsedWithAddNServiceBusEndpoints(List endpointRegistrations, string message) - { - if (endpointRegistrations.Count > 0) - { - throw new InvalidOperationException(message); - } - } - /// /// Registers an NServiceBus endpoint with the dependency injection container, enabling the endpoint /// to resolve services from the application's service provider and participate in the hosted service lifecycle. @@ -163,10 +155,10 @@ public static void AddNServiceBusEndpoint(this IServiceCollection services, Endp settings.AssertNotReused(); var endpointName = settings.EndpointName(); - var hostingSettings = settings.Get(); var transport = settings.Get().TransportDefinition; var endpointRegistrations = GetExistingRegistrations(services); var installerRegistrations = GetExistingRegistrations(services); + var hostingSettings = settings.Get(); ValidateNotUsedWithAddNServiceBusEndpoints(installerRegistrations, $"'{nameof(AddNServiceBusEndpoint)}' cannot be used together with '{nameof(AddNServiceBusEndpointInstaller)}'."); ValidateEndpointIdentifier(endpointIdentifier, endpointRegistrations); @@ -203,6 +195,14 @@ public static void AddNServiceBusEndpoint(this IServiceCollection services, Endp internal static IServiceCollection Unwrap(this IServiceCollection services) => (services as KeyedServiceCollectionAdapter)?.Inner ?? services; + static void ValidateNotUsedWithAddNServiceBusEndpoints(List endpointRegistrations, string message) + { + if (endpointRegistrations.Count > 0) + { + throw new InvalidOperationException(message); + } + } + static void ValidateEndpointIdentifier(object? endpointIdentifier, List registrations) where TRegistration : EndpointRegistration {