Search code examples
scopeasp.net-core-webapidbcontextsignalr-hubasp.net-core-signalr

Requesting a DbContext from a scope in a SignalR Core hub, passing it a connection string


I have an ASP .Net Core 2.2 Web API with a SignalR hub. When the API receives a message form the client, it needs to save this message to the database. It does this as follows:

The SignalR Hub:

public class ChatHub : Hub
{
    public async Task SendMessageToGroup(int clientId, int groupName, string message)
    {
        await SaveMessage(clientId, groupName, message);
        await Clients.Group(groupName).SendAsync("ReceiveMessage", message);
    }

    private async Task<bool> SaveMessage(int clientId, string groupName, string message)
    {
        using (var scope = _serviceProvider.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<TenantContext>();

            Message newMessage = new Message()
            {
                Message = message,
                GroupName = groupName,
                Timestamp = DateTime.Now
            };

            dbContext.Messages.Add(pwMessage);
            dbContext.SaveChanges();
       } 
       return true;
   }
}

All would be well except for the fact that this is a multi-tenant application. Normally, when the client calls the API's controller methods using HTTP requests, the client sends through a "TenantId" header with each request. I then have middleware that intercepts this request, grabs the TenantId from the header, calls a service to retrieve this Tenant using the tenantId, and saves the Tenant object in the HttpContext. Then, on the DbContext's OnConfiguring() method, I use this Tenant Object (stored in the HttpContext) to set the connectionString of the dbContext to whatever database this tenant uses. So:

Middleware:

public class TenantIdentifier
{
    private readonly RequestDelegate _next;

    public TenantIdentifier(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        string tenantId = httpContext.Request.Headers["tenantId"].FirstOrDefault();
        Tenant tenant = await GetTenant(tenantId);
        httpContext.Items["Tenant"] = tenant;
        await _next.Invoke(httpContext);
    }
}

DbContext.cs:

public TenantContext(DbContextOptions<TenantContext> options) : base(options)
{
}

public TenantContext(DbContextOptions<TenantContext> options, IHttpContextAccessor httpContextAccessor) : base(options)
{
    _httpContextAccessor = httpContextAccessor;
}

protected override async void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    Tenant tenant = (Tenant)_httpContextAccessor.HttpContext.Items["Tenant"];
    string connectionString = $"server={tenant.DbUrl};user id={tenant.DbUserName};Pwd={tenant.DbPassword};database={tenant.DbName};persistsecurityinfo=True;TreatTinyAsBoolean=false";
    optionsBuilder.UseMySql(connectionString);
}

Now, when the client calls the SignalR hub, and I create a new scope in the hub and request the DbContext, it's connection string is null. This appears to be because, unlike an HTTP request, calling a SignalR hub doesn't trigger the middleware (which is responsible fro identifying the tenant)

How can I, when requesting a DbContext from the scope, manually pass it the connection string, instead of relying on it to try and generate the connectionString in the OnConfiguring() event (which won't work)

Hope this makes sense :/ Thank you


Solution

  • If you add the IHttpContextAccessor to your Hub class constructor - are you able to access the current context (and headers) there?

    public class ChatHub : Hub
    {
        private IHttpContextAccessor currentContext;
    
        public ChatHub(IHttpContextAccessor currentContext)
        {
            this.currentContext = currentContext;
        }
    }
    

    Of course, remembering to register the HttpContextAccessor in the DI too:

    public void ConfigureServices(IServiceCollection services)
    {
         services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
         services.AddHttpContextAccessor();
         services.AddTransient<IUserRepository, UserRepository>();
    }