diff --git a/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs b/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs index e54de7b8d1..8179e75665 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs @@ -34,8 +34,16 @@ public EndpointBehavior(IEndpointConfigurationFactory endpointBuilder, int insta } var serviceKey = collectionAdapter.ServiceKey; + var endpointKeyObject = serviceKey.ServiceKey ?? serviceKey.BaseKey; - collectionAdapter.Inner.AddNServiceBusEndpoint(config, serviceKey); + if (endpointKeyObject is string serviceKeyString) + { + collectionAdapter.Inner.AddNServiceBusEndpoint(config, serviceKeyString); + } + else + { + throw new InvalidOperationException($"ServiceKey of type {serviceKey.ServiceKey?.GetType().FullName ?? "(null)"} cannot be cast to an endpoint identifier string."); + } return Task.FromResult(new StartableEndpointInstance(serviceKey)); }, static (startableEndpoint, provider, cancellationToken) => startableEndpoint.Start(provider, cancellationToken)); diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index abaf82cd0f..af4630bd3d 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1041,8 +1041,8 @@ 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 void AddNServiceBusEndpoint(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, NServiceBus.EndpointConfiguration endpointConfiguration, string? endpointIdentifierOverride = null) { } + public static void AddNServiceBusEndpointInstaller(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, NServiceBus.EndpointConfiguration endpointConfiguration, string? endpointIdentifierOverride = null) { } } public static class SettingsExtensions { diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index 613add74e0..9116a220dd 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -64,7 +64,6 @@ NServiceBus.SerializationContextExtensions NServiceBus.SerializationExtensionsExtensions NServiceBus.Settings.IReadOnlySettings NServiceBus.Settings.SettingsHolder -NServiceBus.SettingsExtensions NServiceBus.StaticHeadersConfigExtensions NServiceBus.SubscriptionMigrationModeSettings NServiceBus.Support.RuntimeEnvironment diff --git a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs index c82ecc44ec..dfb4e0f9d0 100644 --- a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddEndpoint_Tests.cs @@ -27,27 +27,23 @@ public void Should_register_single_endpoint_with_identifier() } [Test] - public void Should_throw_when_first_endpoint_has_no_identifier_and_second_has_one() + public void Should_register_multiple_endpoints_when_only_last_has_identifier() { var services = new ServiceCollection(); services.AddNServiceBusEndpoint(CreateConfig("Sales")); - var ex = Assert.Throws(() => services.AddNServiceBusEndpoint(CreateConfig("Billing"), "billing-key")); - - Assert.That(ex!.Message, Does.Contain("each endpoint must provide an endpointIdentifier")); + Assert.DoesNotThrow(() => services.AddNServiceBusEndpoint(CreateConfig("Billing"), "billing-key")); } [Test] - public void Should_throw_when_first_endpoint_has_identifier_and_second_has_none() + public void Should_register_multiple_endpoints_when_only_first_has_identifier() { var services = new ServiceCollection(); services.AddNServiceBusEndpoint(CreateConfig("Sales"), "sales-key"); - var ex = Assert.Throws(() => services.AddNServiceBusEndpoint(CreateConfig("Billing"))); - - Assert.That(ex!.Message, Does.Contain("each endpoint must provide an endpointIdentifier")); + Assert.DoesNotThrow(() => services.AddNServiceBusEndpoint(CreateConfig("Billing"))); } [Test] diff --git a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs index b1c1b37e44..ab4f105ecf 100644 --- a/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/ServiceCollectionExtensions_AddInstaller_Tests.cs @@ -27,27 +27,23 @@ public void Should_register_single_endpoint_with_identifier() } [Test] - public void Should_throw_when_first_endpoint_has_no_identifier_and_second_has_one() + public void Should_register_multiple_endpoints_when_only_last_has_identifier() { 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")); + Assert.DoesNotThrow(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Billing"), "billing-key")); } [Test] - public void Should_throw_when_first_endpoint_has_identifier_and_second_has_none() + public void Should_register_multiple_endpoints_when_only_first_has_identifier() { 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")); + Assert.DoesNotThrow(() => services.AddNServiceBusEndpointInstaller(CreateConfig("Billing"))); } [Test] diff --git a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs index b7f625a390..158d42563b 100644 --- a/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/NServiceBus.Core/Hosting/ServiceCollectionExtensions.cs @@ -24,19 +24,16 @@ public static class ServiceCollectionExtensions /// 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. + /// + /// An optional identifier uniquely identifies this endpoint within the dependency injection container, instead of the + /// endpoint name which is used by default. The value will serve as a keyed service identifier within the dependency + /// injection container. /// - /// In most scenarios, using the endpoint name as the identifier is a good choice. + /// In most scenarios, using the default endpoint name as the identifier is a good choice, and this value is not required. /// /// - /// 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 + /// and can be used with keyed service resolution APIs like GetRequiredKeyedService or /// the [FromKeyedServices] attribute on constructor parameters. /// /// @@ -56,7 +53,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, string? endpointIdentifierOverride = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(endpointConfiguration); @@ -70,6 +67,7 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi var transport = settings.Get().TransportDefinition; var endpointRegistrations = GetExistingRegistrations(services); var installerRegistrations = GetExistingRegistrations(services); + var endpointIdentifier = endpointIdentifierOverride ?? endpointName; ValidateNotUsedWithAddNServiceBusEndpoints(endpointRegistrations, $"'{nameof(AddNServiceBusEndpointInstaller)}' cannot be used together with '{nameof(AddNServiceBusEndpoint)}'."); ValidateEndpointIdentifier(endpointIdentifier, installerRegistrations); @@ -78,28 +76,16 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi endpointConfiguration.EnableInstallers(); - if (endpointIdentifier is null) - { - // Deliberately creating it here to make sure we are not accidentally doing it too late. - var externallyManagedContainerHost = EndpointExternallyManaged.Create(endpointConfiguration, services); + // Backdoor for acceptance testing + var keyedServices = settings.GetOrDefault() ?? new KeyedServiceCollectionAdapter(services, endpointIdentifier); + var baseKey = keyedServices.ServiceKey.BaseKey; - services.AddSingleton(externallyManagedContainerHost); - services.AddSingleton(sp => new BaseEndpointLifecycle(externallyManagedContainerHost, sp)); - services.AddSingleton(sp => new EndpointHostedInstallerService(sp.GetRequiredService())); - } - 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 externallyManagedContainerHost = EndpointExternallyManaged.Create(endpointConfiguration, keyedServices); - // Deliberately creating it here to make sure we are not accidentally doing it too late. - var externallyManagedContainerHost = EndpointExternallyManaged.Create(endpointConfiguration, keyedServices); - - services.AddKeyedSingleton(baseKey, externallyManagedContainerHost); - services.AddKeyedSingleton(baseKey, (sp, _) => new EndpointLifecycle(externallyManagedContainerHost, sp, keyedServices.ServiceKey, keyedServices)); - services.AddSingleton(sp => new EndpointHostedInstallerService(sp.GetRequiredKeyedService(baseKey))); - } + 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))); } @@ -110,19 +96,16 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi /// /// 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. + /// + /// An optional identifier uniquely identifies this endpoint within the dependency injection container, instead of the + /// endpoint name which is used by default. The value will serve as a keyed service identifier within the dependency + /// injection container. /// - /// In most scenarios, using the endpoint name as the identifier is a good choice. + /// In most scenarios, using the default endpoint name as the identifier is a good choice, and this value is not required. /// /// - /// 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 + /// and can be used with keyed service resolution APIs like GetRequiredKeyedService or /// the [FromKeyedServices] attribute on constructor parameters. /// /// @@ -144,7 +127,7 @@ public static void AddNServiceBusEndpointInstaller(this IServiceCollection servi /// globally shared ones. /// /// - public static void AddNServiceBusEndpoint(this IServiceCollection services, EndpointConfiguration endpointConfiguration, object? endpointIdentifier = null) + public static void AddNServiceBusEndpoint(this IServiceCollection services, EndpointConfiguration endpointConfiguration, string? endpointIdentifierOverride = null) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(endpointConfiguration); @@ -159,6 +142,7 @@ public static void AddNServiceBusEndpoint(this IServiceCollection services, Endp var endpointRegistrations = GetExistingRegistrations(services); var installerRegistrations = GetExistingRegistrations(services); var hostingSettings = settings.Get(); + var endpointIdentifier = endpointIdentifierOverride ?? endpointName; ValidateNotUsedWithAddNServiceBusEndpoints(installerRegistrations, $"'{nameof(AddNServiceBusEndpoint)}' cannot be used together with '{nameof(AddNServiceBusEndpointInstaller)}'."); ValidateEndpointIdentifier(endpointIdentifier, endpointRegistrations); @@ -167,28 +151,16 @@ public static void AddNServiceBusEndpoint(this IServiceCollection services, Endp hostingSettings.ConfigureHostLogging(endpointIdentifier); - if (endpointIdentifier is null) - { - // Deliberately creating it here to make sure we are not accidentally doing it too late. - var externallyManagedContainerHost = EndpointExternallyManaged.Create(endpointConfiguration, services); + // Backdoor for acceptance testing + var keyedServices = settings.GetOrDefault() ?? new KeyedServiceCollectionAdapter(services, endpointIdentifier); + var baseKey = keyedServices.ServiceKey.BaseKey; - services.AddSingleton(externallyManagedContainerHost); - services.AddSingleton(sp => new BaseEndpointLifecycle(externallyManagedContainerHost, sp)); - services.AddSingleton(sp => new EndpointHostedService(sp.GetRequiredService())); - } - 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 externallyManagedContainerHost = EndpointExternallyManaged.Create(endpointConfiguration, keyedServices); - // Deliberately creating it here to make sure we are not accidentally doing it too late. - var externallyManagedContainerHost = EndpointExternallyManaged.Create(endpointConfiguration, keyedServices); - - services.AddKeyedSingleton(baseKey, externallyManagedContainerHost); - services.AddKeyedSingleton(baseKey, (sp, _) => new EndpointLifecycle(externallyManagedContainerHost, sp, keyedServices.ServiceKey, keyedServices)); - services.AddSingleton(sp => new EndpointHostedService(sp.GetRequiredKeyedService(baseKey))); - } + services.AddKeyedSingleton(baseKey, externallyManagedContainerHost); + services.AddKeyedSingleton(baseKey, (sp, _) => new EndpointLifecycle(externallyManagedContainerHost, sp, keyedServices.ServiceKey, keyedServices)); + services.AddSingleton(sp => new EndpointHostedService(sp.GetRequiredKeyedService(baseKey))); services.AddSingleton(new EndpointRegistration(endpointName, endpointIdentifier, endpointConfiguration.AssemblyScanner().Disable, RuntimeHelpers.GetHashCode(transport))); } @@ -203,7 +175,7 @@ static void ValidateNotUsedWithAddNServiceBusEndpoints(List(object? endpointIdentifier, List registrations) + static void ValidateEndpointIdentifier(string endpointIdentifier, List registrations) where TRegistration : EndpointRegistration { if (registrations.Count == 0) @@ -211,7 +183,7 @@ static void ValidateEndpointIdentifier(object? endpointIdentifier return; } - if (endpointIdentifier is null || registrations.Any(r => r.EndpointIdentifier is null)) + if (registrations.Any(r => r.EndpointIdentifier is null)) { throw new InvalidOperationException( "When multiple endpoints are registered, each endpoint must provide an endpointIdentifier."); @@ -273,7 +245,7 @@ [.. services .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); + record EndpointRegistration(string EndpointName, string? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode); - record EndpointInstallerRegistration(string EndpointName, object? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode) : EndpointRegistration(EndpointName, EndpointIdentifier, ScanningDisabled, TransportHashCode); + record EndpointInstallerRegistration(string EndpointName, string? EndpointIdentifier, bool ScanningDisabled, int TransportHashCode) : EndpointRegistration(EndpointName, EndpointIdentifier, ScanningDisabled, TransportHashCode); } \ No newline at end of file diff --git a/src/NServiceBus.Core/SettingsExtensions.cs b/src/NServiceBus.Core/SettingsExtensions.cs index 18bb1afab3..7e7c671016 100644 --- a/src/NServiceBus.Core/SettingsExtensions.cs +++ b/src/NServiceBus.Core/SettingsExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System;