Pipeline when IMessageSession is created in a Scope

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>();

Is this expected?

@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;
}

Source: Manipulate pipeline with behaviors • NServiceBus • Particular Docs

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.

@Stig_Christensen

I tried this and that worked:

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);
}

Registrations:

builder.Services.AddScoped<TenantProvider>();
builder.Services.AddSingleton<InjectTenantBehavior>();

// ...

endpointConfiguration.Pipeline.Register<InjectTenantBehavior>(b=> b.GetRequiredService<InjectTanentBehavior>(), nameof(InjectTenantBehavior));

That works.. but I think your problem is related by using IMessageSession.

IMessageSession is a singleton and it isn’t context aware. THere is no way to pass the current scope to any method of that interface.

In that case you likely want to use Transactional Session:

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;
}

– Ramon