Skip to content

Creating a multi tenant app

Jon P Smith edited this page Aug 14, 2021 · 10 revisions

This page gives an overview of what you need to do to use AuthP's multi-tenant to build a multi-tenant application.

1. Turn on AuthP's multi-tenant feature

This is explained in Multi tenant configuration.

2. Create AuthP Tenant

You need to create a AuthP Tenant (see Multi tenant admin service) for each group of data. Each tenant contains a method called GetTenantDataKey that returns a string used in EF Core's query filter to only show data in the specific AuthP Tenant.

NOTE: if you are using AuthP's HierarchicalTenant, then tenants can link to a 'parent' Tenant - see Multi tenant explained.

3. Assign a AuthP Tenant to a AuthUser

When a user logs in AuthP is set up to add a DataKey claim to the logged-in user. This is automatic if you are using the authentication cookie approach, or by AuthP's ITokenBuilder service that can create the JWT Token for you.

NOTE: an AuthP DataKey starts with dot and then a numeric value, e.g. ".123". Hierarchical tenants may have extra dot+number, e.g. ".123.333.231".

4. Extract the DataKey from the current user

You need to extract the DataKey claim from the ASP.NET Core user. The AuthP library provides a scoped service called GetDataKeyFilterFromUser that capture the ClaimsPrincipal of the currently logged in user and extract the DataKey claim. AuthP registers the against the IDataKeyFilter interface.

NOTE: You can create your own service to extract the DataKey if you want, just use a different interface so that it doesn't clash with the IDataKeyFilter interface service.

5. Inject the IDataKeyFilter service into your application DbContext

You need to provide a second parameter to the constructor in your application DbContext, as shown in the code below.

public class YourDbContext : DbContext, IDataKeyFilter
{
    public string DataKey { get; }

    public YourDbContext (DbContextOptions<YourDbContext > options, IDataKeyFilter dataKeyFilter)
        : base(options)
    {
        DataKey = dataKeyFilter?.DataKey ?? "."; 
    }

    // rest of code left out.
}

NOTE: You have two options on what to do if the DataKey is null (null means a) no logged in, b) background service call, or c) user hasn't got an assigned tenant):

  1. Set the DataKey to ".", which will means all the multi-data can be seen (good for admin, but watch out for 'no logged in' user).
  2. Set the DataKey to a string NOT starting with ".", e.g. "NoAccess". Then no multi-tenant data will be seen by any EF Core query.

6. Add EF Core global query filters to each entity

You need to configure a global query filter on all the entities that hold tenant's DataKey. You could do this by calling HasQueryFilter Fluent API method in your configuration, but I recommend you automate this.

  • If its a single-level multi-tenant system the user's DataKey much exactly match the DataKey in the multi-tenant entities.
  • If its a hierarchical multi-tenant system the DataKey in the multi-tenant entities must startwith the user's DataKey. (Have a look at code inside the OnModelCreating method of the RetailDbContext used in Example4's hierarchical multi-tenant system.)

7. Add an interface to all entities that hold tenant data

Assuming you are using the automated configuration, each entities that hold tenant data should have an interface to say they have a DayaKey. AuthP provides the IDataKeyFilter interface, which will work.

8. Setting the DataKey when you create a new tenant data entity

You have a few options:

  1. Your entity constructor uses the ITenantPartsToExport interface to add information from the user's Tenant when a new entity if created. See RetailOutlet entity class from the Example4 code as an example.
  2. You override the SaveChanges / SaveChangesAsync methods in your application's DbContext to set a newly created entity's DataKey from the current user's DataKey claim. An example of this approach is shown below.
public class YourDbContext : DbContext, IDataKeyFilter
{
    public string DataKey { get; }

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        this.MarkWithDataKeyIfNeeded(DataKey);
        return base.SaveChanges(acceptAllChangesOnSuccess);
    }

    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, 
        CancellationToken cancellationToken = default(CancellationToken))
    {
        this.MarkWithDataKeyIfNeeded(DataKey);
        return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }

    /// other code left out

Where the MarkWithDataKeyIfNeeded extension method will fill in the entity's DataKey property if

  1. The entity is being created
  2. The entity has the IDataKeyFilter interface assigned to it
  3. The entity's DataKey is null (this test allows a higher level to set a DataKey that is not its own, say to add stock to a shop lower in the hierarchy).

Here is an example the MarkWithDataKeyIfNeeded extension method

public static void MarkWithDataKeyIfNeeded(this DbContext context, string accessKey)
{
    foreach (var entityEntry in context.ChangeTracker.Entries()
        .Where(e => e.State == EntityState.Added))
    {
        if (entityEntry.Entity is IDataKeyFilter {DataKey: null} hasDataKey)
            hasDataKey = accessKey;
    }
}

Articles / Videos

Concepts

Setup

Usage

Admin

SupportCode

Clone this wiki locally