In a Web API, I have a service that depends on IMessageSession. However, the pipeline used to send messages does not have access to the scoped ServiceProvider.
I have implemented Behavior<IOutgoingLogicalMessageContext> and want to access our scoped TenantProvider to set a tenant header on outgoing messages. The problem is that the ServiceProvider available in the pipeline is the root container; hence, trying to resolve a scoped service results in an exception.
var scopedProvider = context.Extensions.Get<IServiceProvider>();
var tenantProvider = scopedProvider.GetRequiredService<TenantProvider>();
@Stig_Christensen To resolve scoped services use context.Builder.GetRequiredService<ScopedDependency>(); as shown in the below snippet:
public class BehaviorUsingDependencyInjection :
Behavior<IIncomingLogicalMessageContext>
{
// Dependencies injected into the constructor are singletons and cached for the lifetime
// of the endpoint
public BehaviorUsingDependencyInjection(SingletonDependency singletonDependency)
{
this.singletonDependency = singletonDependency;
}
public override async Task Invoke(IIncomingLogicalMessageContext context, Func<Task> next)
{
var scopedDependency = context.Builder.GetRequiredService<ScopedDependency>();
// do something with the scoped dependency before
await next();
// do something with the scoped dependency after
}
SingletonDependency singletonDependency;
}
I see there is a builder that is convenient. But my problem is that this is an IOutgoingLogicalMessageContextbehaviour, and the container is not scoped. Even though the IMessageSession(that sends the message) is a dependency in a scoped API controller, I still get:
System.InvalidOperationException: Cannot resolve scoped service 'TenantProvider' from root provider.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NServiceBus.Pipeline;
public sealed class InjectTenantBehavior(ILogger<InjectTenantBehavior> logger) :
Behavior<IOutgoingLogicalMessageContext>
{
public override async Task Invoke(IOutgoingLogicalMessageContext context, Func<Task> next)
{
var tenantProvider = context.Builder.GetRequiredService<TenantProvider>();
logger.LogInformation("Injected scoped dependency: {TenantId}", tenantProvider.TenantId);
context.Headers["TenantId"] = tenantProvider.TenantId.ToString();
await next();
}
}
class TenantProvider
{
static long counter;
public long TenantId { get; } = Interlocked.Increment(ref counter);
}
As an alternative workaround you could use something like:
public sealed class TenantContext
{
static readonly AsyncLocal<string?> _tenantId = new();
public string TenantId
{
get => _tenantId.Value ?? throw new InvalidOperationException(
"Tenant context not set. Did you forget to call SetTenantId?");
set => _tenantId.Value = value;
}
public bool IsSet => _tenantId.Value is not null;
}