Exceptions
TenantGuardModelException
Section titled “TenantGuardModelException”TenantGuardModelException : InvalidOperationException, thrown when EF Core builds a model
that breaks a rule with error severity, under Enforcement.Throw. That happens the first
time a context is used, or when the host starts with AddTenantGuardStartupValidation.
Report holds every finding; the message lists every error:
TenantGuard found 1 model error:- TGM001: Customer has tenant property 'TenantId' but no query filter references it. Add HasQueryFilter(e => e.TenantId == <current tenant>) on the root type.The model rules give each message and its fix.
TenantIsolationViolationException
Section titled “TenantIsolationViolationException”TenantIsolationViolationException : InvalidOperationException, thrown by SaveChanges
and SaveChangesAsync before anything reaches the database, when a write adds, changes,
moves or deletes a row outside the current tenant:
| State | Blocked when |
|---|---|
| Added | The row’s tenant isn’t the current tenant (an unset one is stamped under StampOnAdd) |
| Modified | The row’s original tenant isn’t the current tenant, or the update changes it |
| Deleted | The row’s original tenant isn’t the current tenant |
Tenants are compared with the tenant property’s ValueComparer. Entries of owned types
are skipped, so a save that changes only the owned part of another tenant’s row isn’t
blocked yet.
Violations lists every blocked write as a TenantViolation:
| Member | What it holds |
|---|---|
Entity |
The entity type’s display name |
State |
Added, Modified or Deleted |
Keys |
The primary key values by property name; empty under IncludeKeysInErrors(false), or for a key EF Core hasn’t generated yet |
Expected |
The current tenant, or null when there is none |
Actual |
The tenant on the row: the new value for an insert, the stored or changed value for an update, the stored value for a delete |
The message lists up to ten violations and the total:
TenantGuard blocked 12 writes outside the current tenant "acme":- Added Invoice (Id: 7): TenantId is "globex"- Modified Invoice (Id: 3): TenantId changes from "acme" to "globex"...- and 2 moreWrite only rows of the current tenant, or wrap a write meant to cross tenants in context.TenantGuard().Bypass(reason).With no current tenant, under NoTenantPolicy.Reject:
TenantGuard blocked 1 write because the context has no current tenant:- Added Invoice (Id: 1): TenantId is "acme"Set the current tenant before saving, or wrap a write meant to cross tenants in context.TenantGuard().Bypass(reason).When the context’s model was built for another tenant, under
AllowCapturedValues(), Violations is empty and the
message names both tenants:
TenantGuard blocked a save to AppDbContext: its model was built for tenant "acme", so its query filters read "acme"'s rows, but the current tenant is "globex". Key the model cache by tenant in IModelCacheKeyFactory, so each tenant gets a model of its own.TenantIsolationProbeException
Section titled “TenantIsolationProbeException”TenantIsolationProbeException : Exception, thrown by ProbeReport.AssertAllPassed()
when a check failed or was skipped. The message is a table of those rows:
TenantGuard isolation probe: 3 of 12 checks did not pass.
Entity Check Outcome Detail-------- ------------- ------- ------Customer ReadIsolation Failed As acme, Set<Customer>() returned globex's row (Id: 2). ...Customer FindIsolation Failed As acme, FindAsync(Id: 2) returned globex's row. ...Customer BulkIsolation Failed As acme, ExecuteDelete removed globex's row (Id: 2). ...Configuration errors
Section titled “Configuration errors”These are mistakes in the setup rather than in the data, so they say what to change. The
message of an argument exception ends with the parameter’s name, as in
(Parameter 'tenants').
| Thrown by | Exception and message |
|---|---|
TenantPropertyNames() with no names |
ArgumentException: Pass at least one tenant property name. A null or blank name throws ArgumentNullException or ArgumentException. |
TenantProperty<TEntity>(...), with anything but a property or field of TEntity itself |
ArgumentException: Pass a property of {Entity} itself, such as e => e.TenantId. |
Enforcement(...) or WhenNoCurrentTenant(...), with a value the enum doesn’t define |
ArgumentOutOfRangeException: Unknown enforcement. or Unknown policy. |
SaveChanges that writes a tenant-scoped row, with no CurrentTenant configured |
InvalidOperationException: TenantGuard checks the writes of {Context} against its current tenant, but no CurrentTenant is configured. Add CurrentTenant(ctx => ...) to UseTenantGuard. |
SaveChanges, when CurrentTenant returns another type |
InvalidOperationException: CurrentTenant returned a {ReturnedType}, but {Entity}.{Property} is a {PropertyType}. Return a value of the tenant property's type. |
context.TenantGuard(), without UseTenantGuard |
InvalidOperationException: {Context} does not use TenantGuard. Add UseTenantGuard to its options. |
Bypass(reason), with a null, empty or whitespace reason |
ArgumentNullException for null, ArgumentException otherwise |
Host start, from AddTenantGuardStartupValidation, when the context isn’t registered |
OptionsValidationException: AddTenantGuardStartupValidation<{Context}>() found neither {Context} nor IDbContextFactory<{Context}>. Register the context before the host starts. |
Host start, from AddTenantGuardStartupValidation, without UseTenantGuard |
OptionsValidationException: AddTenantGuardStartupValidation<{Context}>() needs {Context} configured with UseTenantGuard; without it there are no rules to run. |
WithTenants, with fewer than two tenants, a repeat, or a null, empty or whitespace one |
ArgumentException: Pass at least two tenants, distinct and not empty. |
RunAsync, before WithTenants |
InvalidOperationException: Call WithTenants with at least two tenants before RunAsync. |
RunAsync, when Seed<T> names a type the model doesn’t have |
InvalidOperationException: Seed<{Type}> names a type that is not an entity type of {Context}. |
RunAsync, when a tenant doesn’t fit a tenant property |
InvalidOperationException: The tenant "{Tenant}" is not a {Type}, the type of {Entity}.{Property}. Pass WithTenants values the tenant property can hold. |
RunAsync, when the write guard blocks the probe’s own seeding |
InvalidOperationException: The write guard blocked seeding {Entity} as {Tenant}. Does the context factory make {Tenant} the context's current tenant? |
RunAsync, when the write guard blocks seeding because the context’s model was built for another tenant (per-tenant models) |
InvalidOperationException: The write guard blocked seeding {Entity} as {Tenant}, because the context's model was built for another tenant. Key the model cache by tenant in IModelCacheKeyFactory, so each tenant gets a model of its own. |
A null argument to UseTenantGuard, CurrentTenant, TenantGuardModel.Validate, the
probe’s constructor, WithTenants or Seed<T> throws ArgumentNullException.