1

I'm developing a multi-teant SaaS application in ASP.NET core mvc and I was wondering what the general approach is to applying tenant logic in a shared database scenario (TenantId for each entity). And more specificly, where this logic should be applied; In the Context, UnitOfWork, Repository or Service?

My current architecture looks like this:

EF DbContext -> UnitOfWork -> Repository -> Service

Right now, I'm applying all of my logic in the EF DbContext. For example applying the TenantId to a entity:

public override int SaveChanges( )
{
    foreach ( var entityEntry in ChangeTracker.Entries( ) )
    {
        if ( entityEntry.Entity is ITenantEntity entity )
        {
            if ( entityEntry.State == EntityState.Added ||
                 entityEntry.State == EntityState.Modified )
            {
                entity.TenantId = _tenantProvider.GetTenantId( );
            }
        }
    }

    return base.SaveChanges( );
}

And filtering out data based on tenant:

protected override void OnModelCreating( ModelBuilder builder )
{
    base.OnModelCreating( builder );

    ... omitted ....

    builder.Entity<Person>( )
        .HasQueryFilter( p => p.TenantId == _tenantProvider.GetTenantId( ) );

    builder.Entity<Address>( )
        .HasQueryFilter( p => p.TenantId == _tenantProvider.GetTenantId( ) );

    builder.Entity<Company>( )
        .HasQueryFilter( p => p.TenantId == _tenantProvider.GetTenantId( ) );

    ... omitted ....
}

3 Answers 3

1

I'm not exactly sure of your architecture from your description. But just to be clear, your dbcontexts, repo and UoW should be instantiated per request, which should also allow for them to be per tenant. You can either instantiate one of those objects with the tenant information for the request, or you can pass the tenantId down from the service, either on a model or as a separate parameter. You shouldn't be keeping a instance of each tenant dbcontext/repo in memory.

0

And more specifically, where this logic should be applied; In the Context, UnitOfWork, Repository or Service?

Where should it be applied? As close to the source as is reasonably possible. That generally means the db context, but I wouldn't be surprised if some people were to argue that this is a business logic decision and therefore shouldn't reside in the persistence layer. I disagree with that, but unsurprisingly not everyone agrees.

The thing is, unless you have specific concerns about e.g. the persistence layer project being reused without the business logic project, it doesn't really matter where you put it, as long as the tenant-filtering is a blanketed, automated and OCP-compliant solution.


One improvement you can make is having your tenant-scoped entities to use a reusable ancestor, so that you can configure EF once for this type.

public abstract class TenantedEntity
{
    public Guid TenantId { get; set; }
}

public class Person : TenantedEntity
{
    // Other Person properties
}

public class Address : TenantedEntity
{
    // Other Address properties
}

public class Company : TenantedEntity
{
    // Other Company properties
}

Because this allows you to configure EF once:

builder.Entity<TenantedEntity>( )
    .HasQueryFilter( p => p.TenantId == _tenantProvider.GetTenantId( ) );
-1

Basically you only have two options

  1. Add tenantId onto your root objects and just have a universal repository

  2. Hide tenantId from the service by having a repository per tenant

1 is far easier, but it looks like you are doing 2.

The key thing is to work out what you persist between requests and make sure you can reuse connections and objects without running out of memory because you have an array of a million repositories

3
  • similar summary in this answer on SO May 12, 2019 at 0:06
  • 2
    I not sure what "make sure you can reuse connections and objects without running out of memory because you have an array of a million repositories" is about. You should be instantiating your repo/dbcontext per request. And you can instantiate that single repository with the single tenantid associated with that request. Sep 12, 2019 at 13:18
  • if you can do that then fine, but you also have to think about caching and cases where you cant instantiate the connection each time such as http
    – Ewan
    Dec 7, 2020 at 10:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.