Skip to content

Commit 4308439

Browse files
Expose dependency injection on the transport seam (#7570)
* Expose the service provider on the transport seam * Move up and improve documentation * Remove using * Improve service collection and dependency injection access (#7571) * Improve service collection and dependency injection access * IsRawMode --------- Co-authored-by: Daniel Marbach <[email protected]> * Remove no longer needed assert --------- Co-authored-by: Daniel Marbach <[email protected]> Co-authored-by: Daniel Marbach <[email protected]>
1 parent 6bd6323 commit 4308439

File tree

9 files changed

+171
-17
lines changed

9 files changed

+171
-17
lines changed

src/NServiceBus.AcceptanceTesting/AcceptanceTestingTransport/AcceptanceTestingTransport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ public string? StorageLocation
4242
field = value;
4343
}
4444
}
45-
}
45+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace NServiceBus.AcceptanceTests.Core.TransportSeam;
2+
3+
using System.Threading.Tasks;
4+
using AcceptanceTesting;
5+
using EndpointTemplates;
6+
using Features;
7+
using NUnit.Framework;
8+
using Transport;
9+
10+
public class When_transport_provides_feature : NServiceBusAcceptanceTest
11+
{
12+
[Test]
13+
public async Task Should_setup_feature()
14+
{
15+
var context = await Scenario.Define<Context>()
16+
.WithEndpoint<EndpointWithFeature>()
17+
.Done(c => c.EndpointsStarted)
18+
.Run();
19+
20+
Assert.That(context.TransportFeatureRan, Is.True);
21+
}
22+
23+
class Context : ScenarioContext
24+
{
25+
public bool TransportFeatureRan { get; set; }
26+
}
27+
28+
class EndpointWithFeature : EndpointConfigurationBuilder
29+
{
30+
public EndpointWithFeature() => EndpointSetup<DefaultServer>(c => c.UseTransport(new CustomAcceptanceTestingTransport()));
31+
}
32+
33+
class CustomAcceptanceTestingTransport : AcceptanceTestingTransport
34+
{
35+
public CustomAcceptanceTestingTransport() => EnableEndpointFeature<FeatureAccessingInfrastructure>();
36+
37+
class FeatureAccessingInfrastructure : Feature
38+
{
39+
protected override void Setup(FeatureConfigurationContext context)
40+
{
41+
var testContext = (Context)context.Settings.Get<ScenarioContext>();
42+
testContext.TransportFeatureRan = true;
43+
}
44+
}
45+
}
46+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace NServiceBus.AcceptanceTests.Core.TransportSeam;
2+
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using AcceptanceTesting;
6+
using EndpointTemplates;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using NUnit.Framework;
9+
using Transport;
10+
11+
public class When_transport_needs_access_to_di : NServiceBusAcceptanceTest
12+
{
13+
[Test]
14+
public async Task Should_be_able_to_resolve_dependencies()
15+
{
16+
var context = await Scenario.Define<Context>()
17+
.WithEndpoint<EndpointWithFeature>()
18+
.Done(c => c.EndpointsStarted)
19+
.Run();
20+
21+
Assert.That(context.TransportDependencyResolved, Is.True);
22+
}
23+
24+
class Context : ScenarioContext
25+
{
26+
public bool TransportDependencyResolved { get; set; }
27+
}
28+
29+
class EndpointWithFeature : EndpointConfigurationBuilder
30+
{
31+
public EndpointWithFeature() => EndpointSetup<DefaultServer>(c => c.UseTransport(new CustomAcceptanceTestingTransport()));
32+
}
33+
34+
class CustomAcceptanceTestingTransport : AcceptanceTestingTransport
35+
{
36+
protected override void ConfigureServicesCore(IServiceCollection services) => services.AddSingleton<TransportService>();
37+
38+
public override Task<TransportInfrastructure> Initialize(HostSettings hostSettings, ReceiveSettings[] receivers, string[] sendingAddresses, CancellationToken cancellationToken = default)
39+
{
40+
if (hostSettings.SupportsDependencyInjection)
41+
{
42+
var serviceProvider = hostSettings.ServiceProvider;
43+
var context = serviceProvider.GetRequiredService<Context>();
44+
context.TransportDependencyResolved = serviceProvider.GetService<TransportService>() is not null;
45+
}
46+
return base.Initialize(hostSettings, receivers, sendingAddresses, cancellationToken);
47+
}
48+
49+
class TransportService;
50+
}
51+
}

src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,9 +2317,16 @@ namespace NServiceBus.Transport
23172317
public NServiceBus.Settings.IReadOnlySettings? CoreSettings { get; }
23182318
public System.Action<string, System.Exception, System.Threading.CancellationToken> CriticalErrorAction { get; }
23192319
public string HostDisplayName { get; }
2320+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "CoreSettings")]
2321+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, "CoreSettings")]
2322+
public bool IsRawMode { get; }
23202323
public string Name { get; }
2324+
public System.IServiceProvider? ServiceProvider { get; set; }
23212325
public bool SetupInfrastructure { get; }
23222326
public NServiceBus.StartupDiagnosticEntries StartupDiagnostic { get; }
2327+
[System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ServiceProvider")]
2328+
[get: System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, "ServiceProvider")]
2329+
public bool SupportsDependencyInjection { get; }
23232330
}
23242331
public interface IMessageDispatcher
23252332
{
@@ -2429,6 +2436,8 @@ namespace NServiceBus.Transport
24292436
public bool SupportsPublishSubscribe { get; }
24302437
public bool SupportsTTBR { get; }
24312438
public virtual NServiceBus.TransportTransactionMode TransportTransactionMode { get; set; }
2439+
public void ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection services) { }
2440+
protected virtual void ConfigureServicesCore(Microsoft.Extensions.DependencyInjection.IServiceCollection services) { }
24322441
protected void EnableEndpointFeature<T>()
24332442
where T : NServiceBus.Features.Feature, new () { }
24342443
public abstract System.Collections.Generic.IReadOnlyCollection<NServiceBus.TransportTransactionMode> GetSupportedTransactionModes();

src/NServiceBus.Core/EndpointCreator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ void ConfigureMessageTypes(IEnumerable<Type> messageTypesHandled)
169169
});
170170
}
171171

172-
public StartableEndpoint CreateStartableEndpoint(IServiceProvider builder, bool serviceProviderIsExternallyManaged)
172+
public StartableEndpoint CreateStartableEndpoint(IServiceProvider serviceProvider, bool serviceProviderIsExternallyManaged)
173173
{
174174
hostingConfiguration.AddStartupDiagnosticsSection("Container", new
175175
{
@@ -184,7 +184,7 @@ public StartableEndpoint CreateStartableEndpoint(IServiceProvider builder, bool
184184
recoverabilityComponent,
185185
hostingComponent,
186186
sendComponent,
187-
builder,
187+
serviceProvider,
188188
serviceProviderIsExternallyManaged);
189189
}
190190

src/NServiceBus.Core/StartableEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class StartableEndpoint(
2525

2626
public async Task Setup(CancellationToken cancellationToken = default)
2727
{
28-
transportInfrastructure = await transportSeam.CreateTransportInfrastructure(cancellationToken).ConfigureAwait(false);
28+
transportInfrastructure = await transportSeam.CreateTransportInfrastructure(serviceProvider, cancellationToken).ConfigureAwait(false);
2929

3030
var pipelineCache = pipelineComponent.BuildPipelineCache(serviceProvider);
3131
var messageOperations = sendComponent.CreateMessageOperations(serviceProvider, pipelineComponent);

src/NServiceBus.Core/Transports/HostSettings.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace NServiceBus.Transport;
44

55
using System;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Threading;
78
using Settings;
89

@@ -14,12 +15,38 @@ namespace NServiceBus.Transport;
1415
/// </remarks>
1516
public class HostSettings(string name, string hostDisplayName, StartupDiagnosticEntries startupDiagnostic, Action<string, Exception, CancellationToken> criticalErrorAction, bool setupInfrastructure, IReadOnlySettings? coreSettings = null)
1617
{
18+
/// <summary>
19+
/// Indicates whether the transport is running in the raw mode not hosted inside an NServiceBus endpoint.
20+
/// </summary>
21+
/// <remarks>
22+
/// This property returns <c>true</c> if the transport is running in the raw mode not hosted inside an NServiceBus endpoint; otherwise, <c>false</c> indicating the transports is running inside an NServiceBus endpoint.</remarks>
23+
[MemberNotNullWhen(false, nameof(CoreSettings))]
24+
public bool IsRawMode => CoreSettings is null;
25+
26+
/// <summary>
27+
/// Indicates whether the transport supports dependency injection.
28+
/// </summary>
29+
/// <remarks>
30+
/// This property returns <c>true</c> if the associated service provider is initialized; otherwise, <c>false</c>. When the endpoint is running in an NServiceBus endpoint (<see cref="IsRawMode"/> set to <c>false</c>),
31+
/// this property will always return <c>true</c>.
32+
/// In raw mode (<see cref="IsRawMode"/> set to <c>true</c>), this property will return <c>false</c> unless the hosting infrastructure has initialized the service provider and assigned it to the <see cref="ServiceProvider"/> property before the transport definition is initialized.
33+
/// Transport implementations can use this property to determine whether they can resolve dependencies from the service provider during initialization.
34+
/// </remarks>
35+
[MemberNotNullWhen(true, nameof(ServiceProvider))]
36+
public bool SupportsDependencyInjection => ServiceProvider is not null;
37+
1738
/// <summary>
1839
/// Settings available only when running hosted in an NServiceBus endpoint; Otherwise, <c>null</c>.
1940
/// Transports can use these settings to validate the hosting endpoint settings.
2041
/// </summary>
2142
public IReadOnlySettings? CoreSettings { get; } = coreSettings;
2243

44+
/// <summary>
45+
/// Service provider available only when running hosted in an NServiceBus endpoint; Otherwise, <c>null</c>.
46+
/// Transports can use the provider in hosted scenarios to resolve dependencies from the provider.
47+
/// </summary>
48+
public IServiceProvider? ServiceProvider { get; set; }
49+
2350
/// <summary>
2451
/// A name that describes the host (e.g. the endpoint name).
2552
/// </summary>
@@ -44,4 +71,4 @@ public class HostSettings(string name, string hostDisplayName, StartupDiagnostic
4471
/// A flag that indicates whether the transport should automatically setup necessary infrastructure.
4572
/// </summary>
4673
public bool SetupInfrastructure { get; } = setupInfrastructure;
47-
}
74+
}

src/NServiceBus.Core/Transports/TransportDefinition.cs

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace NServiceBus.Transport;
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using Features;
11+
using Microsoft.Extensions.DependencyInjection;
1112
using Settings;
1213

1314
/// <summary>
@@ -16,7 +17,7 @@ namespace NServiceBus.Transport;
1617
public abstract class TransportDefinition
1718
{
1819
TransportTransactionMode transportTransactionMode;
19-
List<IEnabled>? featuresToEnable;
20+
readonly List<IEnabled> featuresToEnable = [Enabled<TransportServiceCollectionProviderFeature>.Instance];
2021

2122
/// <summary>
2223
/// Creates a new transport definition.
@@ -35,11 +36,7 @@ protected TransportDefinition(TransportTransactionMode defaultTransactionMode, b
3536
/// </summary>
3637
/// <remarks>This method needs to be called within the constructor(s) of the transport definition.</remarks>
3738
/// <typeparam name="T">The feature to enable.</typeparam>
38-
protected void EnableEndpointFeature<T>() where T : Feature, new()
39-
{
40-
featuresToEnable ??= [];
41-
featuresToEnable.Add(Enabled<T>.Instance);
42-
}
39+
protected void EnableEndpointFeature<T>() where T : Feature, new() => featuresToEnable.Add(Enabled<T>.Instance);
4340

4441
/// <summary>
4542
/// Initializes transport factories and transport-specific behavior.
@@ -87,13 +84,31 @@ public virtual TransportTransactionMode TransportTransactionMode
8784
/// </summary>
8885
public bool SupportsTTBR { get; }
8986

90-
internal void Configure(SettingsHolder settings)
87+
/// <summary>
88+
/// Allows the transport to register required services into the service collection.
89+
/// </summary>
90+
/// <remarks>During hosted scenarios, this method is called by the hosting infrastructure to register transport-specific services
91+
/// and the service provider containing the registered services is passed over the <see cref="HostSettings"/> into the <see cref="Initialize"/> method.
92+
/// In raw transport scenarios it is the responsibility of the custom hosting infrastructure to call this method during service registration time
93+
/// and <see cref="Initialize"/> when the service provider is available. Transports should be designed to work in both modes and only every attempt to resolve
94+
/// dependencies when the corresponding service provider is available in the <see cref="HostSettings"/> indicated by <see cref="HostSettings.SupportsDependencyInjection"/>.</remarks>
95+
/// <param name="services">The service collection to register services into.</param>
96+
public void ConfigureServices(IServiceCollection services)
9197
{
92-
if (featuresToEnable is null)
93-
{
94-
return;
95-
}
98+
ArgumentNullException.ThrowIfNull(services);
99+
ConfigureServicesCore(services);
100+
}
101+
102+
/// <summary>
103+
/// Allows the transport to register required services into the service collection.
104+
/// </summary>
105+
/// <param name="services">The service collection to register services into.</param>
106+
protected virtual void ConfigureServicesCore(IServiceCollection services)
107+
{
108+
}
96109

110+
internal void Configure(SettingsHolder settings)
111+
{
97112
foreach (var featureToEnable in featuresToEnable)
98113
{
99114
featureToEnable.Apply(settings);
@@ -116,4 +131,9 @@ sealed class Enabled<TFeature> : IEnabled
116131

117132
public void Apply(SettingsHolder settingsHolder) => settingsHolder.EnableFeature<TFeature>();
118133
}
134+
135+
sealed class TransportServiceCollectionProviderFeature : Feature
136+
{
137+
protected override void Setup(FeatureConfigurationContext context) => context.Settings.Get<TransportDefinition>().ConfigureServices(context.Services);
138+
}
119139
}

src/NServiceBus.Core/Transports/TransportSeam.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ class TransportSeam(TransportDefinition transportDefinition, HostSettings hostSe
1515
// The dependency in IServiceProvider ensures that the TransportInfrastructure can't be resolved too early.
1616
public TransportInfrastructure GetTransportInfrastructure(IServiceProvider _) => transportInfrastructure;
1717

18-
public async Task<TransportInfrastructure> CreateTransportInfrastructure(CancellationToken cancellationToken = default)
18+
public async Task<TransportInfrastructure> CreateTransportInfrastructure(IServiceProvider serviceProvider, CancellationToken cancellationToken = default)
1919
{
2020
if (OperatingSystem.IsWindows() && TransportDefinition.TransportTransactionMode == TransportTransactionMode.TransactionScope)
2121
{
2222
TransactionManager.ImplicitDistributedTransactions = true;
2323
}
2424

25+
hostSettings.ServiceProvider = serviceProvider;
2526
transportInfrastructure = await TransportDefinition.Initialize(hostSettings, receiverSettings, [.. QueueBindings.SendingAddresses], cancellationToken)
2627
.ConfigureAwait(false);
2728

0 commit comments

Comments
 (0)