Multi-Tenancy in ASP.NET (SaaS Architecture) Application
Multi-tenancy allows a single application instance to be used by multiple organizations. It is the key feature of SaaS architecture; keeping data isolated between tenants and can be done using shared or separate databases.
Running a single instance of an application keeps cloud infrastructure costs low and reduces the amount of deployment effort needed by the developers. ASP.NET and Entity Framework provide all the tools needed to implement robust multi-tenant architecture with ease.
In the Nano boilerplate, multi-tenancy is implemented using query filters and override methods in the DB contexts.
Domain
In Domain/Multitenancy, the Tenant table contains the tenant data. You can use any data type you prefer for the tenant ID, but strings are used by default. In a multi-database setup, if a null value is provided for ConnectionString, the tenant will use the default database.
The IMustHaveTenant interface contains one field, TenantId. Any table that implements this interface will have tenant-isolated data. TenantBaseEntity and AuditableEntity are common base classes that implement IMustHaveTenant.
Current Tenant User Service
CurrentTenantUserService is a scoped service that will ‘always’ contain a value for tenant ID. It is triggered on every request in middleware, TenantResolver. TenantResolver will look for the tenant ID as a claim in the JWT token, or as a request header in an unauthenticated request, like login. In a single-tenant setup, this service is called the CurrentUserService and only contains a value for the current user ID.
DB Contexts
In a multi-database setup, two DB contexts are used for actual data persistence, BaseDbContext and ApplicationDbContext. The TenantDbContext is auxiliary, and only used for reading tenant info.
The TenandDbContext is used in the CurrentTenantUserService to look up a tenant when a request comes in. The reason this needs to be a separate context is that BaseDbContext and ApplicationDbContext use CurrentTenantUserService when they construct and have query filters that rely on the tenant Id being present. Having a separate context avoids a circular dependency.
BaseDbContext inherits from IdentityDbContext and contains the ASP Identity-related tables and the Tenant table. In a multiple database scenario, these tables will not be created on every tenant database, only the main database. ApplicationDbContext contains all other tables related to your application. In a multiple database scenario, these tables will be created on every tenant table. The products entity, for example, is managed within the ApplicationDbContext.
In a single database or single tenant setup, there is no need for a separate BaseDbContext. Instead, the ApplicationDbContext is the main context. Refer to the Persistence & Infrastructure guide on how to create migrations.
Query Filters & On Configuring
Queries on tenant data are isolated by the use of global query filters provided by Entity Framework core. The OnModelCreating method dynamically applies query filters to any entity implementing the IMustHaveTenant interface, with help from the ApplyGlobalQueryFilter extension in Infrastructure/Persistence/Extensions.
ApplicationDbContext contains an OnConfiguring method to dynamically switch the connection string per each request.
A single tenant setup will not contain a tenant table, tenant management service, or any fields and query filters with tenant ID.
Save Changes
Whenever tenant-isolated data changes, the Tenant Id and audit fields are handled by the TenantAndAuditFields method in Infrastructure/Persistence/Extensions.
Tenant Management Service (Multi-Tenancy Manager )
The TenantManagementService found in Infrastructure/Multitenancy is responsible for tenant CRUD operations. The GetTenants method returns a list of tenants with full details for the client-side tables.
GetTenantOptions returns a list of tenants with limited details, (only name and id) used for the tenant selection dropdowns. These dropdowns are not intended to be used in production but rather to make development and testing easier.
The SaveTenant method creates a new tenant. When a new tenant is created, an admin user for that tenant is also created. When creating a tenant with an isolated database, the default database name and tenant id together will form the name of the new database.
New tenant databases will use any pending migrations from ApplicationDbContext to generate an initial schema.
Handling Migrations
Any migrations are applied automatically when the app starts up and are handled by the extension AddAndMigrateTenantDatabases. This method will apply any pending migrations to the BaseDbContext and then read the list of tenants. Any pending ApplicationDbContext migrations are then applied to tenants in that list if they contain a unique connection string.
The BaseDbContextFactory is necessary to guide the Entity Framework design-time tools when scaffolding new migrations locally. You’ll find quick reference EF commands for scaffolding migrations commented in each of the DB contexts. In a single database setup, ApplicationDbContextFactory serves the same purpose.
Next Steps
That covers multi-tenancy. Next, we’ll explore persistence and other infrastructure.