Bsft.Data
2.2.0
dotnet add package Bsft.Data --version 2.2.0
NuGet\Install-Package Bsft.Data -Version 2.2.0
<PackageReference Include="Bsft.Data" Version="2.2.0" />
<PackageVersion Include="Bsft.Data" Version="2.2.0" />
<PackageReference Include="Bsft.Data" />
paket add Bsft.Data --version 2.2.0
#r "nuget: Bsft.Data, 2.2.0"
#:package Bsft.Data@2.2.0
#addin nuget:?package=Bsft.Data&version=2.2.0
#tool nuget:?package=Bsft.Data&version=2.2.0
Bsft.Data
The query layer of a Bsft application: a whitelisted filter, sort and pagination surface for list endpoints, index-backed full-text search declared beside it, and multi-tenancy enforced in the database rather than remembered at every call site.
Install
dotnet add package Bsft.Data
Requires Bsft.Data.Contracts (pulled transitively; it carries the wire types a client shares),
Bsft.Core, Bsft.Instrumentation, Microsoft.EntityFrameworkCore with its relational
package, and Microsoft.AspNetCore.Http.Abstractions. Full-text search additionally needs one of the
provider packages — Bsft.Data.Postgres, Bsft.Data.SqlServer or Bsft.Data.Sqlite — which supply
the ISearchProvider this package validates and calls.
Register
services.AddBsftDataQueryFiltering(cfg => cfg.FromAssembly(typeof(ProductQueryConfiguration).Assembly));
services.AddBsftDataMultiTenancy(cfg => cfg.ResolveFromHeader());
// or with the header name and claim type bound from configuration, section "Bsft:Data"
services.AddBsftDataMultiTenancy(configuration, cfg => cfg.ResolveFromHeader());
// the framework's unit of work over your own context: IUnitOfWork + ITransactionalUnitOfWork
services.AddBsftDataUnitOfWork<AppDbContext>();
// hand out a tenant identifier nothing else uses, and take it back again
services.AddBsftSandboxTenants();
services.AddSingleton<ISandboxTenantRegistry, MyRegistry>(); // required, and yours
Each of the three registers the relational-model warm-up as well; a host that uses EF Core without
any of them adds it on its own with services.AddBsftDataModelWarmup(). It builds the
relational model of each relational context type as the first instance of that type initializes,
one type at a time, because EF Core's own lazy build can fail when two contexts of a new model run
their first command at the same moment ("The model must be finalized and its runtime dependencies
must be initialized before 'GetRelationalModel' can be used") — which is what the first requests after
a host starts do. It listens from before the first hosted service starts, covers every context however
it is registered, leaves non-relational providers alone and costs one annotation lookup per context
once a model is built. BsftDataOptions.ShouldWarmRelationalModels = false turns it off.
The unit of work is what the packages that stage rather than write commit through — the API key
expiry reminders and the notification digest sweep among them. A commit is the context's
SaveChangesAsync; a unit is one transaction on its connection, run through the provider's execution
strategy and joining a transaction the host already opened rather than nesting a second one. On a
non-relational provider the work simply runs. Registered with TryAdd, so a host with a unit of work
of its own keeps it.
The first two are independent: a single-tenant application still filters and searches, and a multi-tenant
one that exposes no list endpoint still has to scope its writes. Multi-tenancy needs three more things
from the application: TenantMiddleware in the pipeline, so the tenant is resolved before a request
touches the database, modelBuilder.UseTenantFilter(this) in the context's OnModelCreating,
which is what makes the isolation a query filter rather than a convention, and the save guard on the
context's options, which is the write half of the same isolation:
services.AddDbContext<AppDbContext>((sp, options) => options
.UseSqlServer(connectionString)
.AddInterceptors(sp.GetRequiredService<TenantSaveChangesInterceptor>()));
Leaving that call out is the one omission nothing at runtime reports: reads stay filtered while inserts
are written with no tenant and an update moving a row to another tenant is not refused. A startup
validator therefore fails the host when a context implementing ITenantScopedDbContext carries no such
interceptor. Adding it in the context's own OnConfiguring satisfies the check just as well.
Registering it in the container as an IInterceptor or ISaveChangesInterceptor does not, and is not
a way to wire it at all: EF Core does not resolve an interceptor from the application's container, so
one registered only there never runs. A context the check cannot read at startup, because constructing
it needs something the host does not have that early, is reported as a warning and passed rather than
failed, and one reached only through AddDbContextFactory may go unexamined.
The context implements
ITenantScopedDbContext for the filter, so the filter reads the tenant off the context executing the
query instead of capturing one scope's ITenantContext — EF Core caches the model for the life of the
process, and a captured tenant would still be the first request's tenant on every later one. Register
such a context with AddDbContext, never AddDbContextPool: a pooled instance keeps the
ITenantContext it was constructed with and freezes on the first tenant that rented it.
AddBsftDataMultiTenancy registers a startup validator that fails the host on that combination.
AddBsftSandboxTenants registers ISandboxTenantProvisioner and nothing to remember what it handed
out: a startup validator fails the host until an ISandboxTenantRegistry is registered, because an
in-memory default would look like the seam working and would hand out a colliding identifier the first
time the process recycled. A sandbox tenant is entered with the same EnterTenant as any other, and
teardown removes the registry entry plus whatever each registered ISandboxTenantCleanup speaks for.
Configure
BsftDataOptions.SectionName is "Bsft:Data". BsftSandboxTenantOptions.SectionName is
"Bsft:SandboxTenants" and carries IdPrefix (sandbox), DefaultLifetime (7 days) and
MaxProvisionAttempts (5).
| Option | Default | Effect |
|---|---|---|
BsftDataOptions.MultiTenancy.HeaderName |
"X-Tenant-Id" |
The request header HeaderTenantResolver reads the tenant from. |
BsftDataOptions.MultiTenancy.ClaimType |
"tenant_id" |
The claim type ClaimTenantResolver reads the tenant from. |
BsftDataOptions.ShouldWarmRelationalModels |
true |
Builds each relational context type's relational model as its first instance initializes, instead of lazily by its first command. false keeps EF Core's lazy build. |
Both defaults are DataDefaults.TenantHeaderName and DataDefaults.TenantClaimType, so the
authentication stack and the resolver name the same claim without repeating the literal.
Validation: the header name must not be blank and must be a valid HTTP field name — a name a request
cannot carry would leave the tenant silently unresolved — and the claim type must not be blank. A
failure names the member and the offending value, and it is thrown from AddBsftDataMultiTenancy as
well as on start.
A name stated in code (ResolveFromHeader("X-Account")) is applied after the section is bound and
therefore wins over it. Passing no argument leaves whatever the section says.
The other two configuration surfaces are deliberately not options types and have no section:
QueryFilteringConfiguration carries assemblies to scan and MultiTenancyConfiguration selects a
resolver type, and neither an assembly nor a type is a value a configuration provider can supply.
Extend
ITenantResolver decides where the current tenant comes from — ResolveFromHeader,
ResolveFromClaim and ResolveFromSubdomain select the three shipped ones, ResolveUsing<T>()
registers an application's own. ISearchProvider translates a search into the database's own
full-text operator; the three provider packages implement it and a host may add another.
ISandboxTenantRegistry remembers the sandbox tenant identifiers AddBsftSandboxTenants handed out
and has no default — startup fails without one, because an identifier is only isolated if something
durable remembers it was given away — and each ISandboxTenantCleanup removes one package's rows
when a sandbox is taken away. ISearchScopeProvider supplies the tenant and org-unit scope a search
is narrowed to and has no package default: this package does not reference Bsft.Organization, so a
host declaring a scoped search group registers one, and startup says so when none is there.
IDataTelemetry receives the search metrics and is registered with TryAdd, so a host's own
registration wins. QueryConfigurationBase<TEntity, TDto> is the extension point an application
writes against: one subclass per resource, discovered by the assembly scan.
Documentation
Topic page: docs/data.md. Search: docs/search.md. Conventions: docs/package-conventions.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Bsft.Core (>= 2.2.0)
- Bsft.Data.Contracts (>= 2.2.0)
- Bsft.Instrumentation (>= 2.2.0)
- Microsoft.AspNetCore.Http.Abstractions (>= 2.3.0)
- Microsoft.EntityFrameworkCore (>= 10.0.12)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
-
net8.0
- Bsft.Core (>= 2.2.0)
- Bsft.Data.Contracts (>= 2.2.0)
- Bsft.Instrumentation (>= 2.2.0)
- Microsoft.AspNetCore.Http.Abstractions (>= 2.3.0)
- Microsoft.EntityFrameworkCore (>= 9.0.19)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.19)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.19)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.19)
- Microsoft.Extensions.Logging (>= 9.0.19)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.19)
- Microsoft.Extensions.Options (>= 9.0.19)
NuGet packages (7)
Showing the top 5 NuGet packages that depend on Bsft.Data:
| Package | Downloads |
|---|---|
|
Bsft.Idempotency
BSFT Framework idempotent unsafe endpoints: an Idempotency-Key header, a request fingerprint, replay of the first response and a pluggable record store |
|
|
Bsft.Localization
BSFT Framework DB-backed localization with tenant support |
|
|
Bsft.Testing
BSFT Framework test harnesses and test doubles for integration, mediator, and saga tests |
|
|
Bsft.Caching.Data
BSFT Framework bridge that folds the current tenant into every Bsft.Caching key, so a cacheable query answered for one tenant is never served to another. |
|
|
Bsft.Data.Sqlite
BSFT Framework SQLite FTS5 full-text search provider for Bsft.Data |
GitHub repositories
This package is not used by any popular GitHub repositories.