Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use TenantInfo in a background job? #808

Open
michaelhachen opened this issue Apr 16, 2024 · 4 comments
Open

How to use TenantInfo in a background job? #808

michaelhachen opened this issue Apr 16, 2024 · 4 comments
Labels

Comments

@michaelhachen
Copy link
Contributor

michaelhachen commented Apr 16, 2024

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

@AndrewTriesToCode
Copy link
Sponsor Contributor

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.

@vigouredelaruse
Copy link

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

@Blackburn29
Copy link

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

@AndrewTriesToCode
Copy link
Sponsor Contributor

Thanks @Blackburn29 that looks like a good approach

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

4 participants