Finbuckle.MultiTenant
Finbuckle.MultiTenant copied to clipboard
How to use TenantInfo in a background job?
Hello
I am delighted with the library, it simplifies the handling of several tenants enormously. Many thanks for that!
But now I have a problem and somehow can't find the solution. The DbContext of EF Core is initialized with the help of the TenantInfo object, which is passed to the DbContext via DI.
public DocuScanDbContext(DbContextOptions options, IDateTimeProvider dateTimeProvider, TenantInfo? tenantInfo) : base(options)
{
_dateTimeProvider = dateTimeProvider;
_tenantInfo = tenantInfo;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (_tenantInfo is not null)
{
optionsBuilder.UseSqlServer(_tenantInfo.ConnectionString, providerOptions =>
{
providerOptions.EnableRetryOnFailure();
});
}
base.OnConfiguring(optionsBuilder);
}
So far everything works, the data is written or read into the correct database. But now the following situation arises: When a new object is created, a domain / integration event is registered and when this object is saved via DbContext, the event is persisted in a table (outbox pattern).
public static DocumentMetaData Create(Guid tenantId, DocumentType documentType, string documentName, string? notes, DocumentMetaDataOrigin origin)
{
var metaData = new DocumentMetaData
{
DocumentMetaDataId = NewId.NextGuid(),
TenantId = tenantId,
DocumentType = documentType,
DocumentName = documentName,
Notes = notes ?? string.Empty,
Origin = origin,
ProcessStatus = DocumentProcessStatus.New
};
metaData.RaiseDomainEvent(new DocumentMetaDataCreatedEvent(metaData.DocumentMetaDataId, metaData.TenantId, metaData.DocumentType, metaData.DocumentName, metaData.Notes, metaData.Origin, metaData.ProcessStatus));
return metaData;
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new())
{
try
{
await AddDomainEventsAsOutboxMessages(cancellationToken);
var result = await base.SaveChangesAsync(cancellationToken);
return result;
}
catch (DbUpdateConcurrencyException ex)
{
throw new ConcurrencyException("Concurrency exception occurred.", ex);
}
}
A background job regularly checks whether it has events to process and executes them if necessary. However, these events must now be executed in the correct database. So how can I set the TenantInfo correctly for the background job so that the DbContext can be initialized? The background job does not have an HttpContext, so I cannot set the TenantInfo in the HttpContext. According to the Issue #485 the TenantInfos are not stored in the HttpContext. However, I cannot find a way to "set" the TenantInfos so that the DbContext is initialized correctly if it is executed in a background job. What do I miss?
Thank you very much for your help! Michael
Hi, I can respond later today with a solution for you but I am working on some dbcontext factory like functions which will help spin up new db contexts for a a given tenant much easier.
i'm also interested in this for non-http context scenarios where tenant databases
- are administered (created/migrated/deleted) by people or orchestrations that are not within the same organizational trust boundary, but may be running on different pods in the same kubernetes cluster
- share a db context but not necessarily a schema (the db context discovers its model by assembly scanning)
- must come with automatable installers (docker init containers) that respond to network requests to perform superuser operations against arbitrary connection strings
thanks
This is how I do it:
public MyBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public MyBackgroundService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public Task ExecuteAsync(...)
{
using var scope = _scopeFactory.CreateScope();
var resolvedTenant = scope.ServiceProvider.GetRequiredService<IMultiTenantStore<TenantConfig>>().TryGetByIdentifierAsync("myTenantId");
scope.ServiceProvider.GetRequiredService<IMultiTenantContextSetter>().MultiTenantContext = new
{
TenantInfo = resolvedTenant
};
var database = scope.ServiceProvider.GetRequiredService<MyDbContext>();
// Use context
}
}
Thanks @Blackburn29 that looks like a good approach