Docs
Tenantry Pro

Changelog

The changes in each release of Tenantry Pro 0.5, newest first.

0.5.0 - 2026-10-03

Upgrading from 0.4

0.5 reshapes the public API once, before 1.0, and runs on Tenantry Core 0.5: update the Core calls first, with Core's changelog ("Upgrading from 0.4"). Registration code needs no Tenantry.Pro using directive any more; code that uses Pro's types needs using Tenantry.Pro; (and using Tenantry.Pro.EfCore; or using Tenantry.Pro.AspNetCore; for theirs). The Breaking entries below give the details.

0.40.5
packages Tenantry.Pro.EfCore.SqlServer, .Npgsql and .MySqlTenantry.Pro.EfCore, which works through your context's own EF Core provider
package Tenantry.Pro.HealthChecksTenantry.Pro.EfCore
namespaces Tenantry.Pro.BackgroundServices, .Exceptions, .Licensing, .Lifecycle and .Strategies.*Tenantry.Pro
namespaces Tenantry.Pro.EfCore.Audit, .Migrations and .Strategies.*Tenantry.Pro.EfCore
namespaces Tenantry.Pro.AspNetCore.Telemetry and .Telemetry.ExtensionsTenantry.Pro.AspNetCore
pro.WithLicence(key), the Tenantry:Licence settingpro.UseLicenseKey(key), the Tenantry:License setting (Tenantry__License)
pro.UseDatabasePerTenant(…) and DatabasePerTenantOptionsCore's tenant.UseConnectionStrings(…) and tenant.AddDbContextPerTenantDatabase<TContext>(…)
DatabasePerTenantOptions.CacheConnectionStrings and CacheDurationpro.CacheConnectionStrings(o => o.Duration = …)
connection-string encryption (UseDataProtectionEncryption(), ConnectionStringEncryptionOptions)removed
ITenantLifecycleManager<TKey>, pro.AddLifecycleManagement(…), TenantLifecycleOptionsITenantProvisioner<TKey> (always registered), pro.ConfigureProvisioning(o => …), TenantProvisioningOptions
a seeder registered in the service collection, with SeedAsync(tenant, scopedProvider, ct)pro.AddSeeder<T>(), with SeedAsync(tenant, ct) and its services in the constructor; a seeder that is only in the service collection is no longer run
pro.UseMixedMode(o => o.GetStrategyForTenant = …), TenantStrategypro.UseMixedMode(o => o.GetIsolation = …), TenantIsolation
pro.AddDatabaseProvisioning(), pro.AddSchemaProvisioning(o => o.ConnectionString = …), DatabaseProvisioningService<TKey>, SchemaProvisioningService<TKey>pro.AddDatabaseProvisioning<TContext>(), pro.AddSchemaProvisioning<TContext>(); provision with ITenantProvisioner<TKey>
pro.AddSchemaPerTenantCaching(), options.AddSchemaPerTenantCaching<TKey>(sp), ISchemaNameResolver<TKey>, HasDefaultSchema(…) in OnModelCreatingpro.UseSchemaPerTenant(o => o.GetSchemaName = …) and options.UseTenantry()
pro.WithMigrationOrchestration<TKey, TContext>(factory, runAtStartup, failStartupOnMigrationError), MigrationOrchestratorService<TKey, TContext>, MigrationStatusTracker<TKey, TContext>pro.AddMigrations<TContext>(o => o.OnStartup = …), ITenantMigrationRunner<TKey>
AddTenantryDatabaseCheck<TKey>(…), AddTenantryMigrationCheck<TKey, TContext>(factory), TenantHealthCheckOptions.ConnectionFactoryAddTenantDatabaseCheck<TContext>(), AddTenantMigrationCheck<TContext>(), through the application's context
options.UseAuditLogging(sp), AuditOptions.ExcludeTypespro.AddAuditLogging() for every context with options.UseTenantry(); Exclude<T>(), ExcludeProperty<T>(…), ShouldAudit
TenantBackgroundService<TKey>.ExecuteForTenantAsync(tenant, scopedProvider, ct), the constructor's storeAccessorExecuteForTenantAsync(scope, ct), the constructor's ITenantLookup<TKey> tenantLookup
pro.AddTenantMetrics() and the tenantry.requests.* instrumentsthe tenant.id tag on ASP.NET Core's http.server.request.duration (see Changed for dashboards)
AddHangfireTenantFilter(), app.UseTenantryHangfire()pro.AddHangfirePropagation(), config.UseTenantry(sp) in AddHangfire
AddMassTransitTenantFilters(), x.AddTenantryConsumeFilter(), cfg.UseTenantryPro(ctx)pro.AddMassTransitPropagation(), cfg.UseTenantry(context)
AddQuartzTenantScope()pro.AddQuartzPropagation(), q.UseTenantry() in AddQuartz
AddRebusTenantSteps(), o.UseTenantryPro(sp)pro.AddRebusPropagation(), o.UseTenantry(sp)
MissingTenantBehavior in the integrations' propagation optionsTenantPropagationBehavior (Allow, Warn, Skip, Reject)

Added

  • Audit entries say who made the change and what made it: AuditEntry.Actor, CorrelationId and Data, from an IAuditContextProvider (by default no actor, and the current trace's id; register your own to name the user, as the audit logging guide shows for ASP.NET Core), and EF Core's name for the entity type, EntityType. With AuditOptions.OnStoreFailure = AuditStoreFailureBehavior.Throw, a store that fails to write the entries of changes already saved makes SaveChanges, or the commit, throw an AuditStoreException holding the entries (by default the failure is logged, as before).
  • Migrating several databases or schemas at once: pro.AddMigrations<TContext>(o => o.MaxConcurrency = 8) (1 by default), which also bounds GetStatusAsync. ITenantMigrationRunner<TKey>.MigrateAllAsync(IProgress<MigrationResult<TKey>>, …) reports each one's result as it completes, with every tenant whose data is there. To know which tenants share a database or schema before migrating it, a run now creates each tenant's context first, so the first tenant's context of each is created twice.
  • Migrations for schema per tenant: pro.AddMigrations<TContext>() applies migrations generated without a schema (as dotnet ef migrations add generates them, with no tenant) to each tenant's schema, with a migration history table in that schema, and gives the snapshot the tenant's schema for EF Core 9+'s pending-changes check. Tested on SQL Server and PostgreSQL; SQL in migrationBuilder.Sql(…) is applied as written. See Changed for AddMigrations.
  • app.RunTenantMigrationsIfRequestedAsync(args) (an IHost extension): with the argument migrate-tenants, migrates every tenant and returns the exit code (1 if any database or schema failed), so a deployment step is one line.
  • Provisioning steps of your own: ITenantProvisioningStep<TKey>, added with pro.AddProvisioningStep<T>(), runs for each new tenant in a scope for it, after Tenantry.Pro's own steps; its AppliesTo can skip a tenant, for example by its isolation in mixed mode. See Changed for the provisioning API.
  • IConnectionStringCache<TKey>.InvalidateAll(), to drop every tenant's cached connection string after rotating credentials.
  • Jobs and messages for a tenant by name, whichever tenant is current: jobs.ForTenant(tenantId).Enqueue(…) for Hangfire (any Enqueue, Schedule or ContinueJobWith), Publish(message, context => context.SetTenant(tenantId)) or Send(…) for MassTransit, and bus.Send(message, new Dictionary<string, string>().WithTenant(tenantId)) (or Publish, Defer) for Rebus. A tenant given this way wins over the current one: Hangfire's filter and Rebus's step add the current tenant only to a job or message that carries none, and MassTransit runs the callback after its filters.
  • Recurring work for each tenant: recurringJobs.AddOrUpdateForEachTenant<T>(id, job => …, cron) for Hangfire, whose runs enqueue the job once for each tenant in the store, and new JobDataMap().ForEachTenant() for a Quartz.NET job, which then, whenever it fires without a tenant, schedules a run for each tenant with the firing trigger's data and priority. Each tenant's job runs, and fails, on its own; Hangfire's recurring jobs otherwise run without a tenant.
  • MassTransit routing slips (Courier): cfg.UseTenantry(context) adds Tenantry's execute and compensate activity filters, so each activity executes and compensates as the routing slip's tenant, which it passes on. They ran without it.
  • Jobs and messages name their tenant in logs and traces: while a Hangfire job, Quartz.NET job, MassTransit consumer or activity, or Rebus handler runs as its tenant, a log scope with TenantId is open, and its trace span is tagged tenant.id (for a MassTransit consumer, the message's receive span, of which the consumer's is a child).
  • CI builds every csharp block in the README and docs against the packed packages (`scripts/check-doc-snippets.sh`), so a guide can no longer show code that does not compile; a block that is not meant to compile is marked csharp no-compile.
  • An .editorconfig shared with Tenantry core, checked in CI with dotnet format --verify-no-changes.
  • SchemaPerTenantOptions<TKey>.MaxCachedSchemas, how many schemas' compiled models each context type keeps (500 by default), and MaxCompiledQueriesPerSchema, room for each schema's compiled queries (100 by default), set in pro.UseSchemaPerTenant(o => …). See Fixed.
  • CI starts every sample in Development, where the host validates its registrations, against SQL Server, MySQL and PostgreSQL in containers and with a licence signed for the run, and checks each does what its README says (scripts/smoke-samples.cs). Each package has a conformance test: its features registered through UsePro, scopes and every registration validated, every Tenantry service resolved in a tenant's scope, the host started (licence check included). A weekly lane builds and tests Pro against Tenantry Core's master (scripts/build-against-local-core.sh --test).

Fixed

  • AuditEntry.PrimaryKey showed a byte array key as System.Byte[], and a date key without its fractional seconds, so entries of different rows could not be told apart, and wrote other values in the server's culture, so a decimal key's comma read as a separator. A byte array is now written in hexadecimal (0x0102), a date or time to the tick (2026-10-02T13:04:05.1230000, a DateTime without its kind, so an inserted row's key matches its later entries'), a strongly typed id (a value-converted key that cannot be formatted in the invariant culture) as the value it is stored as, and every other value in the invariant culture.

  • Audit entries left out the values of complex properties (ComplexProperty, and EF Core 10's ComplexCollection): an update of Address.City was saved and recorded with no old or new values. They are now recorded by path (Address.City), and a complex collection whole, as a list of its elements' values, when it changes. ExcludeProperty leaves out a complex property, or, named on the complex type, one of its properties (it no longer requires a class, so a struct complex type can be named).

  • Audit entries held the entity's own instances of mutable values, such as a byte array, so changing one after SaveChanges, before the transaction committed, changed the entry but not the saved row. Each value is now copied when the save starts, as EF Core copies it to detect changes, and an array is copied too.

  • The API reference repeated a type parameter's variance in its constraints (where TKey : IEquatable<in TKey>), which is not C#.

  • With a Guid or int tenant key, a Hangfire job, MassTransit or Rebus message or audit entry created without a tenant carried the key's default (00000000-…, 0) as its tenant: without a tenant, ITenantContext.CurrentTenantId is the key's default, not null. They now carry no tenant.

  • Tenantry.Pro.MassTransit failed on MassTransit 8.1 and later: AddTenantryConsumeFilter threw MissingMethodException at startup, because MassTransit 8.1 changed the endpoint-callback delegate it uses and the package was built against 8.0. It now requires MassTransit 8.1 or later 8.x. (Found by the sample smoke check; the tests ran on 8.0.)

  • Quartz jobs whose constructor takes a scoped service that reads the tenant (a DbContext choosing its connection, say) got it without a tenant: Quartz's DI job factory creates the job, and its scoped dependencies, before the tenant scope opened. The job is now created inside the scope, and returned to Quartz's factory before the scope closes; with Skip it is not created at all. A constructor that throws now fails that run of the job, which Quartz retries on the next firing, instead of putting its triggers in the Error state.

  • Rebus: a message whose tenant could not be resolved under Reject, or whose tenant lookup threw, was never retried or moved to the error queue, but redelivered for ever: the incoming step ran in front of Rebus's retry step. It now runs just before Rebus deserializes the message, and a pipeline without that step fails to start rather than skip the tenant step. Data bus hydration and decryption, which Rebus runs before deserializing, now run outside the tenant scope. The tenant lookup is cancelled when the bus stops, and log messages name the message id.

  • MassTransit: an endpoint configured with cfg.ReceiveEndpoint(…) consumed messages without their tenant, because AddTenantryConsumeFilter reached only the endpoints ConfigureEndpoints configured. cfg.UseTenantry(context) covers every endpoint, consumers, sagas and handlers alike, and the consume filter runs inside MassTransit's message retry, so a rejected message is retried like a consumer that throws.

  • MassTransit: a batch consumer (IConsumer<Batch<T>>) could consume a batch whose messages carried different tenants as one of those tenants. A batch is consumed as its messages' tenant only when they all carry it; one that mixes tenants fails, and the guide shows how to group batches by tenant.

  • A tenant id carried by a job or message was formatted and parsed with the current culture, so an int or long id could fail to round trip between cultures that write a minus sign differently. Both use the invariant culture.

  • The Hangfire guide said a job skipped by the Skip policy is marked succeeded; Hangfire deletes it ("Canceled by filter").

  • PeriodicTenantBackgroundService stopped for good (and, by default, stopped the host) when a sweep failed as a whole, for example because the tenant store could not be read. The failure is now logged, and the next sweep runs on schedule.

  • Schema per tenant: EF Core's cache holds the models of about 40 schemas (about 100 on EF Core 8), and about 500 compiled queries, which EF Core compiles again for each schema's model; so with more tenants in use it evicted and compiled them again as tenants took turns. Each context type now has its own cache with room for MaxCachedSchemas schemas and MaxCompiledQueriesPerSchema queries each, and the models are keyed on the schema name rather than the tenant id, so tenants that share a schema share a model. The documentation said EF Core's model cache never evicts.

  • Schema per tenant on a pooled context (AddDbContextPool, AddPooledDbContextFactory, Core's pooled database-per-tenant registration) queried the first tenant's schema for every tenant: a pooled context keeps the model it was first built with. Schema per tenant now refuses a pooled context: creating one throws InvalidOperationException.

  • Audit logging resolved IAuditStore once, into the singleton interceptor: a scoped store, such as one that saves through a DbContext, failed scope validation, or was kept for the application's lifetime with its context. The store is now resolved for each save from the saving context's scope, or from a scope created for the save when the context has none (pooled, from an IDbContextFactory, or created by hand), so a scoped store works everywhere and a transient one is disposed.

  • Every package's README said "Licensed under the Apache License 2.0". The packages now carry their own README for customers (eng/package-readme.md): the commercial licence, the feed, links to tenantry.dev and the support address. Tenantry.Pro.AspNetCore's description and the docs no longer claim per-request licence enforcement, which does not exist (the key is checked once, at startup). Security reports go to support@tenantry.dev rather than the private repository.

  • The getting-started guide, the README and most guides left out the using directives their code needs, and showed APIs from packages they did not name (a Hangfire memory storage, RabbitMQ).

  • The HangfireJobs sample could not start: it registered no tenant store and never resolved a tenant, so no job carried one. It now runs with in-memory tenants and job storage, and its job depends on a scoped service that reads the tenant.

  • The MassTransit guide and the Skip documentation said it acknowledges and drops a message; MassTransit moves it to the endpoint's _skipped queue, and what Skip does is up to each host. The Hangfire guide now says recurring jobs run without a tenant; see Added for one that runs for each tenant.

  • MigrationResult.AppliedMigrations lists what the run applied. It listed the migrations pending when the run started, so of two runners migrating the same tenant at once, the one that waited for EF Core's lock and applied nothing reported them too; and it was always empty when a run failed, even after committing some. It now lists the migrations whose row in the migration history this run wrote and committed (from EF Core's diagnostic events): each once when an execution strategy retries, never one another runner applied, including one it waited for, failed on or skipped on a retry, and on failure those committed before it.

  • Reading migration status (GetStatusAsync, was MigrationStatusTracker's) no longer fails for every tenant when one tenant's database cannot be read. That entry has the new Error set, and IsUpToDate is false; GetTenantStatusAsync returns such an entry instead of throwing. Cancellation still throws.

  • Provisioning the same tenant twice at once (a double submit, a redelivered message) no longer fails with "already exists" in the CreateDatabase or CreateSchema step: a run whose CREATE DATABASE fails waits for the database another run created to come online, and a run whose CREATE SCHEMA fails checks for the schema again.

  • The database health check reports the registration's failure status instead of always Unhealthy, and checks up to TenantHealthCheckOptions.MaxConcurrency databases at a time (default 8) instead of one after another, so unreachable tenants no longer each add a full timeout in turn.

  • A failed migration was logged twice, once as it failed and again in the run's summary. It is logged once, and the summary of a run with failures is a warning.

  • Creating several tenants' contexts at once in a new application, as the tenant health checks do, could fail on Oracle's MySql.EntityFrameworkCore, which builds its type mappings, shared by every context, the first time without a lock (seen in CI). Tenantry.Pro now works through tenants one at a time until one tenant's context has been created and used, then goes on several at once. Two such runs at once in one new process can still meet there.

  • AuditEntry.TenantId and the tenant health checks' data keys (tenant:{id}) format the tenant id with the invariant culture, as job and message headers and traces do. They used the machine's culture, so for a key type whose text depends on it they could differ from the id elsewhere.

  • ProvisionAsync, MigrateTenantAsync and GetTenantStatusAsync throw ArgumentException for an id Tenantry reserves for "no tenant" (the key type's default, Guid.Empty or 0, or an empty string), before anything runs, as Tenantry Core's RunInScopeAsync does. Provisioning such a tenant reported success when no steps were registered and a failed step otherwise, and the migration runner asked the store for it and threw TenantNotFoundException. Every Pro package now applies the same rules for formatting tenant ids and refusing the reserved ones.

Changed

  • Breaking: Tenantry.Pro follows Tenantry Core 0.5's API (see Core's changelog, "Upgrading from 0.4"): AddTenantry is the one entry point for every host (AddTenantryCore is gone), Core's types are in the Tenantry namespace and its registration methods need no using, ITenantContextSetter<TKey>.Use makes a tenant current (was ITenantScope.BeginScope), ITenantScopeFactory creates an ITenantScope<TKey> (was ITenantServiceScope), singletons read tenants through ITenantLookup<TKey> (was ITenantStoreAccessor), and a tenant's connection string comes from ITenantConnectionStringProvider<TKey>.Get or, for the current tenant, CurrentTenantConnectionString<TKey>.Get (was ITenantConnectionStringResolver.Resolve). Core's tenant.AddDbContextPerTenantDatabase<TContext>(…), pooled or not, after tenant.UseConnectionStrings(…), replaces Core's AddTenantDbContextPool for a database per tenant.

  • Breaking: the propagation options of the Hangfire, MassTransit, Quartz and Rebus integrations take Tenantry.Pro.TenantPropagationBehavior (Allow, Warn, Skip, Reject), in place of Core's MissingTenantBehavior, which is now EF Core's and has no Skip.

  • Breaking: UsePro passes an IProBuilder<TKey>, a public interface in Tenantry.Pro; the builder class it replaces (Tenantry.Pro.Internal.ProBuilder<TKey>) is internal and only UsePro, which registers the licence check, creates it. UseSchemaPerTenant and UseMixedMode are extension methods like the other features. A feature that takes a type parameter of its own can register through IProBuilder.Add(IProRegistration), which applies it with the builder's key type, so its callers need not repeat the key type.

  • Breaking: Tenantry.Pro's types are in the Tenantry.Pro namespace (they were in Tenantry.Pro.BackgroundServices, .Exceptions, .Licensing, .Lifecycle and .Strategies.*), and Tenantry.Pro.AspNetCore's in Tenantry.Pro.AspNetCore (were .Telemetry and .Telemetry.Extensions). Registration methods (UsePro, the builder's and AddTenantMetrics) are in Microsoft.Extensions.DependencyInjection, and UseTenantryMetrics in Microsoft.AspNetCore.Builder, so they need no using.

  • Breaking: UsePro reads the licence key from the Tenantry:License setting (the Tenantry__License environment variable) when the application starts, so configuration added after the services, such as a test host's, reaches it; pro.UseLicenseKey(key) sets it in code instead. pro.WithLicence(key) and the Tenantry:Licence setting are gone, and LicenseOptions is internal. Code identifiers use American spelling.

  • Breaking: a licence key must name the key it was signed with (kid, the key's RFC 7638 thumbprint), and carry the audience tenantry-pro (aud) and the licence format 1 (ver), as keys issued by tenantry.dev now do. A key in the earlier format names none of them and no longer validates; no customer was issued one, and support@tenantry.dev replaces any that turns up. Pro finds the public key by its kid, so a future signing key can be added without invalidating keys already issued; a key in a newer format than the package reads asks for an update.

  • Breaking: a database per tenant is Tenantry Core's: pro.UseDatabasePerTenant(…) and DatabasePerTenantOptions are gone. Set the connection strings with tenant.UseConnectionStrings(…) and register the context with tenant.AddDbContextPerTenantDatabase<TContext>(…). pro.CacheConnectionStrings(o => o.Duration = …) caches them (was CacheConnectionStrings and CacheDuration): it wraps the provider UseConnectionStrings registers, whether called before or after UsePro, or one of your own registered before UsePro. The application fails to start if there is none, or if the duration is not positive, and warns at startup if a provider registered later took the cache's place. IConnectionStringCache<TKey> is always registered and has InvalidateAll(); a connection string read while it is being invalidated is no longer cached afterwards.

  • Breaking: tenant provisioning. ITenantLifecycleManager<TKey> is ITenantProvisioner<TKey>, which UsePro always registers, as a singleton (pro.AddLifecycleManagement(…) is gone; pro.ConfigureProvisioning(o => …) sets TenantProvisioningOptions, was TenantLifecycleOptions). It runs ITenantProvisioningStep<TKey>s: Pro's own first (creating the database or schema, then migrations), then the steps and seeders added with pro.AddProvisioningStep<T>() and pro.AddSeeder<T>(), in the order they were added. Each step is resolved from a new scope for the tenant and gets the tenant, so it no longer reads it from the store again, and, in mixed mode, its isolation; its AppliesTo can skip a tenant. TenantProvisioningResult lists every step's outcome (Steps: Succeeded, Failed, Skipped or NotRun) in place of CompletedUpTo and the TenantProvisioningStep enum. ITenantSeeder<TKey>.SeedAsync(tenant, ct) no longer takes a service provider: a seeder takes the services it needs, such as the DbContext, in its constructor; and every seeder added runs (only the first registered one ran). Add seeders with pro.AddSeeder<T>(): a seeder registered only in the service collection (AddScoped<ITenantSeeder<TKey>, T>(), as 0.4 documented) is no longer run. ITenantInfrastructureProvisioner and ITenantMigrator are gone: AddDatabaseProvisioning, AddSchemaProvisioning and AddMigrations (see below) add steps. Cancelling stops provisioning before the next step, and a step that fails after the cancellation is reported as OperationCanceledException.

  • MigrateTenantAsync and GetTenantStatusAsync throw Core's TenantNotFoundException for a tenant the store does not return. It derives from InvalidOperationException, which they threw before, so existing handlers still catch it.

  • Breaking: mixed mode is honoured by provisioning. pro.UseMixedMode(o => o.GetIsolation = …) returns each tenant's TenantIsolation (Shared, Schema or Database; was GetStrategyForTenant and TenantStrategy). Creating a database applies only to Database tenants, creating a schema only to Schema tenants, and migrations, which go to each distinct database or schema, to both (see below). MixedStrategyResolver is gone: each tenant's connection string comes from the UseConnectionStrings delegate.

  • Breaking: per-tenant request metrics are ASP.NET Core's. pro.AddTenantMetrics() and app.UseTenantryMetrics() add a tenant.id tag to ASP.NET Core's http.server.request.duration (a histogram in seconds, on the Microsoft.AspNetCore.Hosting meter, already tagged with the route, method, status code and error.type) instead of recording Tenantry.Pro's own instruments; see Removed. For dashboards: tenantry.requests.count is the histogram's count, tenantry.requests.duration (milliseconds) the histogram, and tenantry.requests.errors the requests with error.type or a 5xx http.response.status_code. tenantry.requests.active has no per-tenant replacement: ASP.NET Core records http.server.active_requests before the tenant is known. A request without a tenant has no tenant.id tag (it was unknown). TenantMetricsOptions<TKey>.GetTagValue sets the tag for each tenant, or leaves it off, to bound the number of series; ExcludePaths and AdditionalTags are gone (on ASP.NET Core 9 and later, DisableHttpMetrics() leaves an endpoint out of the metric). UseTenantryMetrics without AddTenantMetrics says so.

  • Breaking: TenantBackgroundService<TKey>.ExecuteForTenantAsync takes the tenant's ITenantScope<TKey> (scope.Tenant, scope.ServiceProvider) in place of the tenant and a service provider, and the base class has a protected Logger, the logger passed to its constructor. Its constructor, and PeriodicTenantBackgroundService<TKey>'s, take an ITenantLookup<TKey> named tenantLookup (was storeAccessor).

  • Breaking: provisioning works through your context's own EF Core provider, so the provider packages are gone (see Removed). pro.AddDatabaseProvisioning<TContext>() and pro.AddSchemaProvisioning<TContext>(), in Tenantry.Pro.EfCore, replace their AddDatabaseProvisioning() and AddSchemaProvisioning(o => o.ConnectionString = …): the step resolves TContext in the tenant's scope, so it connects as the application does, and creates the database through EF Core's database creator, or the schema with EF Core's migrations SQL (SQL Server and PostgreSQL; another provider fails the step with NotSupportedException). o.CreateContext gives the step a context of its own, with credentials allowed to create databases or schemas. A run whose create fails but finds the database once it waits (up to 30 seconds) succeeds and logs the failure as a warning. DatabaseProvisioningService<TKey>, SchemaProvisioningService<TKey>, their base classes, SchemaProvisioningOptions.ConnectionString and ProvisionAsync(tenantId) are gone: provision with ITenantProvisioner<TKey>. The methods take the context type, so they return the builder without its key type: call them after the others in a chain.

  • Breaking: schema per tenant needs nothing in the context. pro.UseSchemaPerTenant(o => o.GetSchemaName = …) is in Tenantry.Pro.EfCore, and every context that uses UseTenantry() gets the current tenant's schema as its default schema, after OnModelCreating, with a compiled model per schema. pro.AddSchemaPerTenantCaching(), options.AddSchemaPerTenantCaching(sp), ISchemaNameResolver<TKey> and the public TenantModelCacheKeyFactory<TKey> are gone: replace options.AddSchemaPerTenantCaching<TKey>(sp) with options.UseTenantry() (a context without it gets no tenant schema, and every tenant would use the database's default), and remove pro.AddSchemaPerTenantCaching(), ISchemaNameResolver<TKey> and the HasDefaultSchema(…) call in OnModelCreating. SchemaPerTenantOptions<TKey> is in Tenantry.Pro.EfCore and has the cache limits (was SchemaPerTenantCachingOptions); options that are not valid, GetSchemaName missing included, stop the application from starting. In mixed mode GetSchemaName is called only for Schema tenants, whose contexts get their schema; the others keep the database's default. Without a current tenant, such as in dotnet ef, the model has no default schema, so migrations are generated without one. A schema name that is empty, has control characters, or is longer than the database allows (128 characters on SQL Server, 63 bytes on PostgreSQL, which would otherwise shorten it) fails the tenant's queries with InvalidOperationException. Each context type's model cache belongs to an EF Core internal service provider of the context type's own, which every application in a process shares, so a test suite that starts many hosts does not reach EF Core's limit of 20 internal service providers; an application with more than about 20 such context types would. UseMemoryCache, or ReplaceService of IModelCacheKeyFactory or IMemoryCache, on such a context's options throws InvalidOperationException, since either would undo the cache or let tenants share a model.

  • Breaking: Tenantry.Pro.EfCore's provisioning, schema-per-tenant, migration, health-check and audit types are in Tenantry.Pro.EfCore (they were in .Strategies.DatabasePerTenant, .Strategies.SchemaPerTenant, .Migrations, .Audit and .Extensions), its builder and health-check methods (UseSchemaPerTenant, AddDatabaseProvisioning, AddSchemaProvisioning, AddMigrations, AddAuditLogging, AddTenantDatabaseCheck, AddTenantMigrationCheck) in Microsoft.Extensions.DependencyInjection, and RunTenantMigrationsIfRequestedAsync in Microsoft.Extensions.Hosting, so they need no using.

  • Breaking: migrations. pro.AddMigrations<TContext>(o => …) replaces pro.WithMigrationOrchestration<TKey, TContext>(factory, runAtStartup, failStartupOnMigrationError), and ITenantMigrationRunner<TKey> (MigrateAllAsync, MigrateTenantAsync, GetStatusAsync, GetTenantStatusAsync) replaces MigrationOrchestratorService<TKey, TContext> and MigrationStatusTracker<TKey, TContext>. The context comes from the application's registration in each tenant's scope (its IDbContextFactory<TContext> when it cannot be created there, as with only an asynchronous connection string), so there is no factory from a connection string; o.CreateContext creates one of its own, for other credentials. One runner migrates every context added, in the order added, and tenants whose context has the same connection string, default schema and migration history table are migrated once: a result or status entry is per context and database or schema, with its TenantIds, ContextType, Database and Schema (it had one TenantId), MigrateTenantAsync returns a report, and GetTenantStatusAsync a list of entries, one per context (it returned one entry). o.OnStartup = StartupMigrations.LogFailures or FailOnError replaces runAtStartup and failStartupOnMigrationError. Every context added is a Migrations provisioning step; in mixed mode it applies to Database and Schema tenants (only Database before), and the shared database is migrated with the other tenants. UseConnectionStrings is no longer required.

  • Breaking: the health checks are in Tenantry.Pro.EfCore (see Removed). AddTenantDatabaseCheck<TContext>() and AddTenantMigrationCheck<TContext>() replace AddTenantryDatabaseCheck<TKey>(…) and AddTenantryMigrationCheck<TKey, TContext>(factory): they go through the application's context in each tenant's scope, so TenantHealthCheckOptions.ConnectionFactory is gone, and read each distinct database (or, for migrations, database and schema) once. They take the standard name, failureStatus (Degraded by default), tags, timeout (30 seconds by default) and configure arguments, which replace TenantHealthCheckOptions.FailureStatus and Tags; the default names are tenant-databases and tenant-migrations (were tenantry-databases and tenantry-migrations). DatabaseTimeout (was ConnectionTimeout) and MaxConcurrency apply to both checks, and a check reports its last result for CacheDuration (30 seconds by default), so frequent polls do not each reach every tenant database. A tenant's entry reads reachable or unreachable: … (was healthy or unhealthy: …). They need UsePro: without it they report their failure status, saying so.

  • Breaking: audit logging. pro.AddAuditLogging() audits every context that uses UseTenantry(), which adds the audit interceptor after Tenantry's own, so options.UseAuditLogging(sp) and options.UseAuditLogging<TKey>(sp) are gone, and a new tenant-owned entity is always recorded with its TenantId. A context that does not use UseTenantry() is not audited, nor are the saves the store makes. Entries reach the store once their changes are committed (AuditOptions.Timing, AuditTiming.AfterCommit by default): those of changes saved in a transaction (Database.BeginTransaction, shared with other contexts through UseTransaction or not, a TransactionScope, or an enlisted transaction) when it commits, through whichever context, and none if it rolls back, or for changes rolled back to a savepoint. They were passed at the end of each SaveChanges, so a transaction rolled back later still had its entries. A store failure is logged, as before, and the store gets a token that is never cancelled, so a save cancelled once its changes are committed keeps their entries (cancellation was caught and logged as a store failure). With AuditTiming.InTransaction the store is called at the end of each SaveChanges, inside its transaction, to write through the same connection and transaction, and its failure or cancellation is thrown from SaveChanges. IAuditStore.SaveAsync takes the context that saved the changes first; the store comes from a scope of its own when the saving context's scope is gone by the time the transaction commits. AuditOptions.ExcludeTypes is replaced by Exclude<T>(), which also leaves out the types derived from T (it matched the mapped type exactly) and the entities an excluded type owns, ExcludeProperty<T>(x => x.Property) and a ShouldAudit predicate, each applied before an entity's values are read (they were copied first). AuditEntry has a required EntityType, so code that creates entries (a store's tests, say) sets it, and the default store's log message names the actor. See Added for who made the change.

  • Breaking: ILicenseGuard is internal: the licensed operations check the licence themselves.

  • Breaking: the Hangfire, MassTransit, Quartz.NET and Rebus integrations are each added with one builder method and wired with one call on the host library's configuration: pro.AddHangfirePropagation() and config.UseTenantry(sp) in AddHangfire((sp, config) => …) (were AddHangfireTenantFilter() and app.UseTenantryHangfire()); pro.AddMassTransitPropagation() and cfg.UseTenantry(context) (were AddMassTransitTenantFilters(), x.AddTenantryConsumeFilter() and cfg.UseTenantryPro(ctx)); pro.AddQuartzPropagation() and q.UseTenantry() in AddQuartz (was AddQuartzTenantScope(), which had to come after AddQuartz; the two now come in either order); pro.AddRebusPropagation() and o.UseTenantry(sp) (were AddRebusTenantSteps() and o.UseTenantryPro(sp)). The application fails to start if an integration was added but its host side never ran, which left its jobs or messages without their tenant, or, with several MassTransit buses (MultiBus) or Rebus buses (Rebus.ServiceProvider's AddRebus(…, key: …)), if it did not run for each. The filters, steps and job factory are internal and need no key type, so the generic overloads are gone, and so are TenantContextPropagation, TenantPropagationOutcome and TenantPropagationDecision. The job parameter, header and job data key is TenantPropagation.HeaderName, with the same value, tenantry-tenant-id (TenantPublishFilter<TKey>.HeaderKey, TenantOutgoingStep<TKey>.HeaderKey and TenantJobData.TenantIdKey are gone). The host-side methods are in the host libraries' namespaces (Hangfire, MassTransit, Quartz, Rebus.Config, with WithTenant in Quartz) and the builder methods in Microsoft.Extensions.DependencyInjection, so they need no using. Tenantry.Pro.Hangfire no longer needs the ASP.NET Core shared framework, so a worker service can use it. Quartz.NET's WithTenant refuses the key type's default value (Guid.Empty, 0) and an empty string, which Tenantry reserves for no tenant.

  • Breaking: a job or message that carries a tenant the store does not have, or an id that is not a valid tenant id, follows a setting of its own, TenantPropagationOptions.OnUnresolvedTenant, which is Reject by default: it fails, so the host's retry and error handling take over (with TenantNotFoundException for a tenant the store does not have). It ran without a tenant, with a warning, as a job or message that carries no tenant still does (OnMissingTenant, Warn by default).

  • Every log message has an event id that does not change between versions, listed in docs/telemetry.md: 30xx the licence, 31xx provisioning, 32xx background services, 33xx connection strings, 34xx jobs and messages (the integrations' messages, which also name the job or message), and Tenantry.Pro.EfCore's 4xxx (provisioning databases and schemas, migrations, health checks, audit). Alert on the licence errors, on 4103 and 4113 (migration runs with failures) and on 4302 (lost audit entries).

  • The provisioning steps that create databases and schemas use the application's IDbContextFactory<TContext> when the context cannot be created in the tenant's scope, as migrations do, so Tenantry core's asynchronous connection strings work there too.

  • The licence check is one internal service, which the startup check and the guarded operations share. A key that is not valid is logged once, with the reason.

  • Tenantry.Pro depends on Microsoft.Extensions.DependencyInjection.Abstractions, not the DI container.

  • Only the packages that use Tenantry.Pro's internals (Tenantry.Pro.EfCore and the Hangfire, MassTransit, Quartz.NET and Rebus packages) depend on exactly their own release of it; Tenantry.Pro.AspNetCore takes it up to the next minor. Microsoft.Extensions.Diagnostics.HealthChecks takes 10.0.0 or later on .NET 10, as the other Microsoft.Extensions packages do.

  • Breaking: the Hangfire and Rebus packages no longer depend on Newtonsoft.Json, which they referenced only to raise a vulnerable version Hangfire and Rebus allow. Tenantry.Pro.Rebus needs Rebus 8.4 or later, whose .NET build requires a fixed version itself (earlier 8.x releases allow a vulnerable Newtonsoft.Json or System.Text.Json). Hangfire allows the vulnerable 11.0.1: an application using Hangfire references Newtonsoft.Json 13.0.1 or later itself (the Hangfire guide says so).

  • The database health check reports Degraded by default when a tenant database is unreachable (the failureStatus argument; it was Unhealthy): it is a monitoring check, and one tenant's outage is not the application's. ASP.NET Core answers Degraded with 200, so a monitor that reads only the status code should map it to 503 on the monitoring endpoint, as the guide shows.

  • The health checks are for monitoring, not liveness or readiness probes: one unreachable tenant database fails a probe on every replica at once. The guide recommended the tenantry-tagged checks for readiness; it now shows separate liveness, readiness and monitoring endpoints, and how to protect the monitoring one (the per-tenant data lists tenant ids and provider error messages). The TenantHealthChecks sample maps /health/live and /health/tenants.

  • The docs say the tenant store must list every tenant, suspended ones included: MigrateTenantAsync looks tenants up there, and migration runs, migration status and the health checks cover only the tenants it lists. Refuse suspended tenants with an access validator, and check the status in background jobs, which Tenantry does not do. Keep suspended tenants' databases online.

  • The docs now cover: EF Core 9 applying all pending migrations in one transaction; concurrent runners on PostgreSQL with EF Core 10+, where EF Core releases its lock between migrations, so two runners can each apply some and one fails; the permission to create databases that provisioning needs, and provisioning from a separate process (which must grant the application access); EF Core's Migrate creating a missing database; and reading the logs or the migration status after a cancelled run.

  • The samples are named Tenantry.Pro.Samples.<Name>, for their folder, project and namespace, as Tenantry Core's are Tenantry.Samples.<Name>. The Hangfire, MassTransit, Quartz.NET and Rebus samples declared their types in those libraries' own namespaces: Hangfire.BackgroundJobs is now HangfireJobs, MassTransit.Messaging MassTransitMessaging, Quartz.Scheduling QuartzScheduling and Rebus.Messaging RebusMessaging. HealthChecks, the root namespace of the AspNetCore.HealthChecks packages, is TenantHealthChecks, and SchemaPerTenant.Npgsql is SchemaPerTenantPostgreSql; the others drop the dot (DatabasePerTenant.MySql is DatabasePerTenantMySql).

  • The assemblies carry their PDBs, so stack traces from Tenantry.Pro code show file names and line numbers. There are no symbol packages any more.

  • The packages have an icon, the project URL (tenantry.dev) and a copyright notice, and their descriptions match what each package holds. Packing checks that each target framework's assembly keeps the API of the lower ones (package validation).

  • A GitHub release's notes are its section of this changelog, and the release publishes this changelog with its docs, for the Changelog page of the site's docs.

Removed

  • Breaking: Tenantry.Pro.HealthChecks: its checks are in Tenantry.Pro.EfCore (see Changed). Reference Tenantry.Pro.EfCore instead.
  • Breaking: WithMigrationOrchestration, MigrationOrchestratorService<TKey, TContext>, MigrationStatusTracker<TKey, TContext> and the Func<string, TContext> they registered; see Changed.
  • Breaking: Tenantry.Pro.EfCore.SqlServer, Tenantry.Pro.EfCore.Npgsql and Tenantry.Pro.EfCore.MySql: Tenantry.Pro.EfCore provisions with whichever EF Core provider your context uses (see Changed). Reference Tenantry.Pro.EfCore instead. Tenantry.Pro no longer depends on Microsoft.Data.SqlClient, Npgsql, MySqlConnector or the Azure.Identity and Microsoft.Identity.Client versions it pinned for SqlClient.
  • Breaking: the Tenantry.Pro meter (TenantryMeter) and its tenantry.requests.* instruments, which repeated ASP.NET Core's request metrics under other names and units; see Changed for the tenant.id tag that replaces them. Instruments of your own belong on a meter of your own.
  • Breaking: connection-string encryption: ConnectionStringEncryptionOptions, ConnectionStringEncryptionMode, IConnectionStringProtector, the AES protector and Tenantry.Pro.AspNetCore's UseDataProtectionEncryption(). It encrypted only the in-process cache, with a key held by the same process, and every read handed the plain connection string to EF Core and the ADO.NET pool, so it protected nothing.

Earlier releases are in the full changelog.

On this page