From 58a2a0831f2df4248b0b33b9773f1e0d41afed50 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 30 Apr 2024 21:49:15 -0600 Subject: [PATCH] chore: update data isolation docs --- .github/workflows/copy-docs.yml | 35 +++++ docs/EFCore.md | 263 ++++++++++++++++++++------------ 2 files changed, 201 insertions(+), 97 deletions(-) create mode 100644 .github/workflows/copy-docs.yml diff --git a/.github/workflows/copy-docs.yml b/.github/workflows/copy-docs.yml new file mode 100644 index 00000000..5bb8ebe7 --- /dev/null +++ b/.github/workflows/copy-docs.yml @@ -0,0 +1,35 @@ +name: Copy docs to website + +on: [workflow_dispatch] + +jobs: + checkout-copy-checkin: + runs-on: ubuntu-latest + steps: + - name: Checkout Finbuckle.MultiTenant + uses: actions/checkout@v4 + with: + path: main + fetch-depth: 0 # Required due to the way Git works, without it this action won't be able to find any or the correct tags + - name: Get Current Version + id: currentversion + uses: WyriHaximus/github-action-get-previous-tag@v1 + with: + workingDirectory: main + prefix: v + - name: Checkout Website + uses: actions/checkout@v4 + with: + repository: Finbuckle/Website + token: ${{ secrets.workflow_pat }} + path: website + - name: Copy Docs + run: mkdir -p website/docs/${{ steps.currentversion.outputs.tag }} && cp main/docs/* website/docs/${{ steps.currentversion.outputs.tag }} + - name: Checkin Website + working-directory: website + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git add . + git commit -m "chore: docs generated" + git push diff --git a/docs/EFCore.md b/docs/EFCore.md index b9cc14bc..b7d0c451 100644 --- a/docs/EFCore.md +++ b/docs/EFCore.md @@ -1,75 +1,95 @@ # Data Isolation with Entity Framework Core ## Introduction -Data isolation is one of the most important considerations in a multi-tenant app. Whether each tenant has its own database, a shared database, or a hybrid approach can make a significant different in app design. Finbuckle.MultiTenant supports each of these models by associating a connection string with each tenant. + +Data isolation is one of the most important considerations in a multi-tenant app. Whether each tenant has its own +database, a shared database, or a hybrid approach can make a significant different in app design. Finbuckle.MultiTenant +supports each of these models by associating a connection string with each tenant. ## Separate Databases -If each tenant uses a separate database then the `ConnectionString` tenant info -property can be used directly in the `OnConfiguring` method of the database -context class to configure the connection. The `TenantInfo` instance can be -injected into the database context using either an `ITenantInfo` or custom -`ITenantInfo` implementation (as configured with `AddMultiTenant`) parameter -on the database context constructor. + +If each tenant uses a separate database then add a `ConnectionString` property to the app's `ITenantInfo` +implementation. and use it in the `OnConfiguring` method of the database context class. The tenant info can be obtained +by injecting a `IMultiTenantContextAccessor` into the database context class constructor. ```csharp +public class AppTenantInfo : ITenantInfo +{ + public string Id { get; set; } + public string Identifier { get; set; } + public string Name { get; set; } + public string ConnectionString { get; set; } +} + public class MyAppDbContext : DbContext { - private TTenantInfo TenantInfo { get; set; } + // AppTenantInfo is the app's custom implementation of ITenantInfo which + private AppTenantInfo TenantInfo { get; set; } - public MyAppDbContext(MyTenantInfo tenantInfo) + public MyAppDbContext(IMultiTenantContextAccessor multiTenantContextAccessor) { - // DI will pass in the tenant info for the current request. - // ITenantInfo is also injectable. - TenantInfo = tenantInfo; + // get the current tenant info at the time of construction + TenantInfo = multiTenantContextAccessor.tenantInfo; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { - // Use the connection string to connect to the per-tenant database. + // use the connection string to connect to the per-tenant database optionsBuilder.UseSqlServer(TenantInfo.ConnectionString); } ... } ``` -This approach does not require the added complexity described below for a shared -database approach, but does come with its own complexity in operating and -maintaining a larger number of database instances and infrastructure. +This approach does not require the added complexity described below for a shared database approach, but does come with +its own complexity in operating and maintaining a larger number of database instances and infrastructure. ## Shared Database -In shared database scenarios it is important to make sure that queries and commands for a tenant do not affect the data belonging to other tenants. Finbuckle.MultiTenant handles this automatically and removes the need to sprinkle "where" clauses all over an app. Internally a "shadow" tenant ID property is added (or used if already present) to multi-tenant entity types and managed as the database context is used. It also performs validation and related options for handling null or mismatched tenants. + +In shared database scenarios it is important to make sure that queries and commands for a tenant do not affect the data +belonging to other tenants. Finbuckle.MultiTenant handles this automatically and removes the need to sprinkle "where" +clauses all over an app. Internally a shadow `TenantId` property is added (or used if already present) to multi-tenant +entity types and managed as the database context is used. It also performs validation and related options for handling +null or mismatched tenants. Finbuckle.MultiTenant provides two different ways to utilize this behavior in a database context class: + 1. Implement `IMultiTenantDbContext` and used the helper methods as -[described below](#adding-multi-tenant-functionality-to-an-existing-dbcontext), or + [described below](#adding-multitenant-functionality-to-an-existing-dbcontext), or 2. Derive from `MultiTenantDbContext` which handles the details for you. -The first option is more complex, but provides enhanced flexibility and allows existing database context classes (which may derive from a base class) to utilize per-tenant data isolation. The second option is easier, but provides less flexibility. These approaches are both explained further below. +The first option is more complex, but provides enhanced flexibility and allows existing database context classes (which +may derive from a base class) to utilize per-tenant data isolation. The second option is easier, but provides less +flexibility. These approaches are both explained further below. + +Regardless of how the database context is configured, the context will need to know which entity types should be treated +as multi-tenant (i.e. which entity types are to be isolated per tenant) When the database context is initialized, a +shadow property named `TenantId` is added to the data model for designated entity types. This property is used +internally to filter all requests and commands. If there already is a defined string property named `TenantId` then it +will be used. -Regardless of how the db context is configured, the context will need to know which entity types should be treated as multi-tenant -(i.e. which entity types are to be isolated per tenant) When the db context is initialized, a shadow property named `TenantId` is added to the data model for designated entity types. This property is used internally to filter all requests and commands. If there already is a defined string property named "TenantId" then it will be used. +There are two ways to designate an entity type as multi-tenant: -There are two ways to designate an entity type as multi-tenant: +1. apply the `[MultiTenant]` data attribute +2. use the fluent API entity type builder extension method `IsMultiTenant` -1. the `[MultiTenant]` data attribute -2. the fluent API entity type builder extension method `IsMultiTenant`. +Entity types not designated via one of these methods are not isolated per-tenant all instances are shared across all +tenants. -Entity types not designated via one of these methods are not isolated per-tenant -all instances are shared across all tenants. +## Using the `[MultiTenant]` attribute -## Using the [MultiTenant] attribute -The `[MultiTenant]` attribute designates a class to be isolated per-tenant when -it is used as an entity type in a database context: +The `[MultiTenant]` attribute designates a class to be isolated per-tenant when it is used as an entity type in a +database context: ```csharp -// Tenants will only see their own blog posts. +// tenants will only see their own blog posts [MultiTenant] public class BlogPost { ... } -// Roles will be the same for all tenants. +// roles will be the same for all tenants public class Roles { ... @@ -77,26 +97,30 @@ public class Roles public class BloggingDbContext : MultiTenantDbContext { - public DbSet BlogPosts { get; set; } // This will be multi-tenant! - public DbSet Roles { get; set; } // Not multi-tenant! + public BloggingDbContext(IMultiTenantContextAccessor multiTenantContextAccessor) : base(multiTenantContextAccessor) + { + } + + public DbSet BlogPosts { get; set; } // this will be multi-tenant! + public DbSet Roles { get; set; } // not multi-tenant! } ``` -Database context classes derived from `MultiTenantDbContext` will automatically -respect the `[MultiTenant]` attribute. Otherwise a database context class can -be configured to respect the attribute by calling `ConfigureMultiTenant` in the +Database context classes derived from `MultiTenantDbContext` will automatically respect the `[MultiTenant]` attribute. +Otherwise, a database context class can be configured to respect the attribute by calling `ConfigureMultiTenant` in the `OnModelCreating` method. ```csharp protected override void OnModelCreating(ModelBuilder builder) { - // Not needed if db context derives from MultiTenantDbContext + // not needed if database context derives from MultiTenantDbContext builder.ConfigureMultiTenant(); } ``` ## Using the fluent API + The fluent API entity type builder extension method `IsMultiTenant` can be called in `OnModelCreating` to provide the multi-tenant functionality for entity types: @@ -108,13 +132,19 @@ protected override void OnModelCreating(ModelBuilder builder) } ``` -This approach is more flexible than using the `[MultiTenant]` attribute because -it can be used for types which do not have the attribute, e.g. from another assembly. +This approach is more flexible than using the `[MultiTenant]` attribute because it can be used for types which do not +have the attribute, e.g. from another assembly. -`IsMultiTenant()` returns an `MultiTenantEntityTypeBuilder` instance which enables further multi-tenant configuration of the entity type via `AdjustKey`,`AdjustIndex`, `AdjustIndexes`, and `AdjustUniqueIndexes`. See [Keys and Indexes] for more details. +`IsMultiTenant()` returns an `MultiTenantEntityTypeBuilder` instance which enables further multi-tenant configuration of +the entity type via `AdjustKey`,`AdjustIndex`, `AdjustIndexes`, and `AdjustUniqueIndexes`. See [Keys and Indexes] for +more details. ## Existing Query Filters -`IsMultiTenant` and the `[MultiTenant]` attribute use a query filter for data isolation and will automatically merge its query filter with an existing query filter is one is present. For that reason, if the type to be multi-tenant has a existing query filter, `IsMultiTenant` and `ConfigureMultiTenant` should be called *after* the existing query filter is configured: + +`IsMultiTenant` and the `[MultiTenant]` attribute use a query filter for data isolation and will automatically merge its +query filter with an existing query filter is one is present. For that reason, if the type to be multi-tenant has an +existing query filter, `IsMultiTenant` and `ConfigureMultiTenant` should be called *after* the existing query filter is +configured: ```csharp protected override void OnModelCreating(ModelBuilder builder) @@ -122,23 +152,25 @@ protected override void OnModelCreating(ModelBuilder builder) // set a global query filter, e.g. to support soft delete builder.Entity().HasQueryFilter(p => !p.IsDeleted); - // Configure an entity type to be multi-tenant (will merge with existing call to HasQueryFilter) + // configure an entity type to be multi-tenant (will merge with existing call to HasQueryFilter) builder.Entity().IsMultiTenant(); } ``` ## Adding MultiTenant functionality to an existing DbContext -This approach is more flexible than deriving from `MultiTenantDbContext`, but -needs more configuration. It requires implementing `IMultiTenantDbContext` and -following a strict convention of helper method calls. + +This approach is more flexible than deriving from `MultiTenantDbContext`, but needs more configuration. It requires +implementing `IMultiTenantDbContext` and following a strict convention of helper method calls. Start by adding the `Finbuckle.MultiTenant.EntityFrameworkCore` package to the project: + ```{.bash} dotnet add package Finbuckle.MultiTenant.EntityFrameworkCore ``` -Next, implement `IMultiTenantDbContext` on the context. These interface properties ensure that the extension methods will have the information needed to provide proper data isolation. +Next, implement `IMultiTenantDbContext` on the context. These interface properties ensure that the extension methods +will have the information needed to provide proper data isolation. ```csharp public class MyDbContext : DbContext, IMultiTenantDbContext @@ -150,11 +182,24 @@ public class MyDbContext : DbContext, IMultiTenantDbContext ... } ``` -The db context will need to ensure that these properties haves values, e.g. through constructors, setters, or default values. -Finally, call the library extension methods as described below. This requires overriding the `OnModelCreating`, `SaveChanges`, and `SaveChangesAsync` methods. +The database context will need to ensure that these properties haves values, either through constructors, setters, or +default values. + +> In earlier version of Finbuckle.MultiTenant `ITenantInfo` and the app implementation where available via dependency +> injection, but this was removed in v7.0.0 for consistency. Instead, inject the `IMultiTenantContextAccessor` and use +> it to set the `TenantInfo` property in the database context constructor. + +```csharp + +Finally, call the library extension methods as described below. This requires overriding +the `OnModelCreating`, `SaveChanges`, and `SaveChangesAsync` methods. -In `OnModelCreating` use the `EntityTypeBuilder` fluent API extension method `IsMultiTenant` to designate entity types as multi-tenant. Call `ConfigureMultiTenant` on the `ModelBuilder` to configure each entity type marked with the `[MultiTenant]` data attribute. This is only needed if using the attribute and internally uses the `IsMultiTenant` fluent API. Make sure to call the base class `OnModelCreating` method if necessary, such as if inheriting from `IdentityDbContext`. +In `OnModelCreating` use the `EntityTypeBuilder` fluent API extension method `IsMultiTenant` to designate entity types +as multi-tenant. Call `ConfigureMultiTenant` on the `ModelBuilder` to configure each entity type marked with +the `[MultiTenant]` data attribute. This is only needed if using the attribute and internally uses the `IsMultiTenant` +fluent API. Make sure to call the base class `OnModelCreating` method if necessary, such as if inheriting +from `IdentityDbContext`. ```csharp protected override void OnModelCreating(ModelBuilder builder) @@ -171,7 +216,9 @@ protected override void OnModelCreating(ModelBuilder builder) } ``` -In `SaveChanges` and `SaveChangesAsync` call the `IMultiTenantDbContext` extension method `EnforceMultiTenant` before calling the base class method. This ensures proper data isolation and behavior for `TenantMismatchMode` and `TenantNotSetMode`. +In `SaveChanges` and `SaveChangesAsync` call the `IMultiTenantDbContext` extension method `EnforceMultiTenant` before +calling the base class method. This ensures proper data isolation and behavior for `TenantMismatchMode` +and `TenantNotSetMode`. ```csharp public override int SaveChanges(bool acceptAllChangesOnSuccess) @@ -188,26 +235,39 @@ public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, } ``` -Now, whenever this db context is used it will only set and query records -for the current tenant. +Now, whenever this database context is used it will only set and query records for the current tenant. + +## Deriving from `MultiTenantDbContext` -## Deriving from MultiTenantDbContext -This approach is easier bit requires inheriting from `MultiTenantDbContext` which -may not always be possible. It is simply a pre-configured implementation of -`IMultiTenantDbContext` with the helper methods as described below in -(Adding MultiTenant Functionality to an Existing DbContext) -[#adding-multi-tenant-functionality-to-an-existing-dbcontext] +This approach is easier bit requires inheriting from `MultiTenantDbContext` which may not always be possible. It is +simply a pre-configured implementation of `IMultiTenantDbContext` with the helper methods as described above in +[Adding MultiTenant Functionality to an Existing DbContext](#adding-multitenant-functionality-to-an-existing-dbcontext) Start by adding the `Finbuckle.MultiTenant.EntityFrameworkCore` package to the project: + ```{.bash} dotnet add package Finbuckle.MultiTenant.EntityFrameworkCore ``` -The `MultiTenantDbContext` has two constructors which should be called from any derived db context. Make sure to forward the `ITenantInfo` and, if applicable the `DbContextOptions` into the base constructor. +The `MultiTenantDbContext` has two constructors which should be called from any derived database context. Make sure to +forward the `IMultiTenatContextAccessor` and, if applicable the `DbContextOptions` into the base constructor. +Variants of these constructors that pass `ITenantInfo` to the base constructor are also available, but these will not be +used for dependency injection. ```csharp public class BloggingDbContext : MultiTenantDbContext { + // these constructors are called when dependency injection is used + public BloggingDbContext(IMultiTenantContextAccessor multiTenantContextAccessor) : base(multiTenantContextAccessor) + { + } + + public BloggingDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : + base(multiTenantContextAccessor, options) + { + } + + // these constructors are useful for testing or other use cases where depdenency injection is not used public BloggingDbContext(ITenantInfo tenantInfo) : base(tenantInfo) { } public BloggingDbContext(ITenantInfo tenantInfo, DbContextOptions options) : @@ -218,21 +278,8 @@ public class BloggingDbContext : MultiTenantDbContext } ``` -If relying on the `ConnectionString` property of the `TenantInfo` then the db context will need to configures itself in its `OnConfiguring` method using its inherited `ConnectionString` property: - -```csharp -public class BloggingDbContext : MultiTenantDbContext -{ - ... - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseSqlServer(TenantInfo.ConnectionString);\ - } - ... -} -``` - -If the derived db context overrides `OnModelCreating` is it recommended that the base class `OnModelCreating` method is called last so that the multi-tenant query filters are not overwritten. +If the derived database context overrides `OnModelCreating` is it recommended that the base class `OnModelCreating` +method is called last so that the multi-tenant query filters are not overwritten. ```csharp public class BloggingDbContext : MultiTenantDbContext @@ -250,27 +297,33 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } ``` -And that's it. Whenever this db context is used it will only set and query records -for the current tenant. +Now, whenever this database context is used it will only set and query records for the current tenant. ## Hybrid Per-tenant and Shared Databases -When using a shared database context based on `IMultiTenantDbContext` it is -simple extend into a hybrid approach simply by assigning some tenants to a separate -shared database (or its own completely isolated database) via the tenant info + +When using a shared database context based on `IMultiTenantDbContext` it is simple extend into a hybrid approach simply +by assigning some tenants to a separate shared database (or its own completely isolated database) via the tenant info connection string property. ## Design Time Instantiation -Given that a multi-tenant db context usually requires a tenant to function, design time instantiation can be challenging. -By default for things like migrations and command line tools Entity Framework core attempts to create an instance of the context -using dependency injection, however usually no valid tenant exists in these cases and DI fails. -For this reason it is recommended to use a [design time factory](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dbcontext-creation#from-a-design-time-factory) wherein a dummy `ITenantInfo` is constructed with the desired connection string and passed to the db context constructor. + +Given that a multi-tenant database context usually requires a tenant to function, design time instantiation can be +challenging. By default, for things like migrations and command line tools Entity Framework core attempts to create an +instance of the context using dependency injection, however usually no valid tenant exists in these cases and DI fails. +For this reason it is recommended to use a [design time factory](https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dbcontext-creation#from-a-design-time-factory) wherein a dummy `ITenantInfo` is +constructed with the desired connection string and passed to the database context constructor. ## Registering with ASP.NET Core -When registering the db context as a service in ASP.NET Core it is important to take into account whether the connection string and/or provider will vary per-tenant. If so, it is recommended to set the connection string and provider in the `OnConfiguring` db context method as described above rather than in the `AddDbContext` service registration method. +When registering the database context as a service in ASP.NET Core it is important to take into account whether the +connection string and/or provider will vary per-tenant. If so, it is recommended to set the connection string and +provider in the `OnConfiguring` database context method as described above rather than in the `AddDbContext` service +registration method. ## Adding Data -Added entities are automatically associated with the current `TenantInfo`. If an entity is associated with a different `TenantInfo` then a `MultiTenantException` is thrown in `SaveChanges` or `SaveChangesAsync`. + +Added entities are automatically associated with the current `TenantInfo`. If an entity is associated with a +different `TenantInfo` then a `MultiTenantException` is thrown in `SaveChanges` or `SaveChangesAsync`. ```csharp // Add a blog for a tenant. @@ -287,6 +340,7 @@ await db.SaveChangesAsync(); // Throws MultiTenantException. ``` ## Querying Data + Queries only return results associated to the `TenantInfo`. ```csharp @@ -307,10 +361,14 @@ var db = new BloggingDbContext(myTenantInfo, null); var tenantBlogs = db.Blogs.IgnoreQueryFilters().ToList(); ``` -The query filter is applied only at the root level of a query. Any entity classes loaded via `Include` or `ThenInclude` are not filtered, but if all entity classes involved in a query have the `[MultiTenant]` attribute then all results are associated to the same tenant. +The query filter is applied only at the root level of a query. Any entity classes loaded via `Include` or `ThenInclude` +are not filtered, but if all entity classes involved in a query have the `[MultiTenant]` attribute then all results are +associated to the same tenant. ## Updating and Deleting Data -Updated or deleted entities are checked to make sure they are associated with the `TenantInfo`. If an entity is associated with a different `TenantInfo` then a `MultiTenantException` is thrown in `SaveChanges` or `SaveChangesAsync`. + +Updated or deleted entities are checked to make sure they are associated with the `TenantInfo`. If an entity is +associated with a different `TenantInfo` then a `MultiTenantException` is thrown in `SaveChanges` or `SaveChangesAsync`. ```csharp // Add a blog for a tenant. @@ -331,9 +389,12 @@ await db.SaveChangesAsync(); // Throws MultiTenantException. ## Keys and Indexes -When configuring a multi-tenant entity type it is often useful to include the implicit `TenantId` column in the primary key and/or indexes. The `MultiTenantEntityTypeBuilder` instance returned from `IsMultiTenant()` provides the following methods for this purpose: +When configuring a multi-tenant entity type it is often useful to include the implicit `TenantId` column in the primary +key and/or indexes. The `MultiTenantEntityTypeBuilder` instance returned from `IsMultiTenant()` provides the following +methods for this purpose: -* `AdjustKey(IMutableKey, ModelBuilder)` - Alters the existing defined key to add the implicit `TenantId`. Note that this will also impact entities with a dependent foreign key and may add an implicit `Tenant Id` there as well. +* `AdjustKey(IMutableKey, ModelBuilder)` - Alters the existing defined key to add the implicit `TenantId`. Note that + this will also impact entities with a dependent foreign key and may add an implicit `Tenant Id` there as well. * `AdjustIndex(IMutableIndex)` - Alters an existing index include the implicit `TenantId`. * `AdjustIndexes()` - Alters all existing indexes to include the implicit `TenantId`. * `AdjustUniqueIndexes()` - Alters only all existing unique indexes to include te implicit `TenantId`. @@ -349,17 +410,25 @@ protected override void OnModelCreating(ModelBuilder builder) ## Tenant Mismatch Mode -Normally Finbuckle.MultiTenant will automatically coordinate the `TenantId` property of each entity. However in certain situations the `TenantId` can be manually set. +Normally Finbuckle.MultiTenant will automatically coordinate the `TenantId` property of each entity. However, in certain +situations the `TenantId` can be manually set. -By default attempting to add or update an entity with a different `TenantId` property throws a `MultiTenantException` during a call to `SaveChanges` or `SaveChangesAsync`. This behavior can be changed by setting the `TenantMismatchMode` property on the database context: +By default, attempting to add or update an entity with a different `TenantId` property throws a `MultiTenantException` +during a call to `SaveChanges` or `SaveChangesAsync`. This behavior can be changed by setting the `TenantMismatchMode` +property on the database context: -* TenantMismatchMode.Throw - A `MultiTenantException` is thrown (default). -* TenantMismatchMode.Ignore - The entity is added or updated without modifying its `TenantId`. -* TenantMismatchMode.Overwrite - The entity's `TenantId` is overwritten to match the database context's current `TenantInfo`. +* `TenantMismatchMode.Throw` - A `MultiTenantException` is thrown (default). +* `TenantMismatchMode.Ignore` - The entity is added or updated without modifying its `TenantId`. +* `TenantMismatchMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's + current `TenantInfo`. ## Tenant Not Set Mode -If the `TenantId` on an entity is manually set to null the default behavior is to overwrite the `TenantId` for added entities or to throw a `MultiTenantException` for updated entities. This occurs during a call to `SaveChanges` or `SaveChangesAsync`. This behavior can be changed by setting the `TenantNotSetMode' property on the database context: +If the `TenantId` on an entity is manually set to null the default behavior is to overwrite the `TenantId` for added +entities or to throw a `MultiTenantException` for updated entities. This occurs during a call to `SaveChanges` +or `SaveChangesAsync`. This behavior can be changed by setting the `TenantNotSetMode` property on the database context: -* TenantNotSetMode.Throw - For added entities the null `TenantId` will be overwritten to match the database context's current `TenantInfo`. For updated entities a `MultiTenantException` is thrown (default). -* TenantNotSetMode.Overwrite - The entity's `TenantId` is overwritten to match the database context's current `TenantInfo`. +* `TenantNotSetMode.Throw` - For added entities the null `TenantId` will be overwritten to match the database context's + current `TenantInfo`. For updated entities a `MultiTenantException` is thrown (default). +* `TenantNotSetMode.Overwrite` - The entity's `TenantId` is overwritten to match the database context's + current `TenantInfo`.