Csag.Blueprint.Infrastructure 0.7.0

dotnet add package Csag.Blueprint.Infrastructure --version 0.7.0
                    
NuGet\Install-Package Csag.Blueprint.Infrastructure -Version 0.7.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Csag.Blueprint.Infrastructure" Version="0.7.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Csag.Blueprint.Infrastructure" Version="0.7.0" />
                    
Directory.Packages.props
<PackageReference Include="Csag.Blueprint.Infrastructure" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Csag.Blueprint.Infrastructure --version 0.7.0
                    
#r "nuget: Csag.Blueprint.Infrastructure, 0.7.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Csag.Blueprint.Infrastructure@0.7.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Csag.Blueprint.Infrastructure&version=0.7.0
                    
Install as a Cake Addin
#tool nuget:?package=Csag.Blueprint.Infrastructure&version=0.7.0
                    
Install as a Cake Tool

Csag.Blueprint.Infrastructure

Overview

This package provides the shared infrastructure implementation layer for CSAG Blueprint-based applications.

It contains the reusable EF Core persistence backbone, session/auth infrastructure, localization infrastructure, tenancy helpers, authorization transformation, and table-view execution components that consuming applications compose into their own host.

What this package owns

Persistence backbone

Component Purpose
BlueprintDbContext<TAppTenant, TAppUser, TAppRole> Shared EF Core base context that owns the blueprint persistence model.
Blueprint*Configuration classes EF Core mappings for blueprint-owned entities and inheritance roots.
MultiTenancyModelBuilderExtensions Applies tenant filters/indexing/model conventions for tenant-scoped entities. See Data isolation topology for the database-per-tenant option.
EntityFilteringModelBuilderExtensions Registers the named soft-delete query filter and creates the supporting IX_{table}_DeletedAt and composite IX_{table}_ActiveRange indexes for ISoftDeletable / IHasActiveRange.
LocalizationModelBuilderExtensions Wires IHasLocalizedTexts entities to their ILocalizedText side, with the per-language unique constraint and indexes.
ContractModelBuilderExtensions Applies the model conventions the domain contracts imply (IHasInternalName constraints, localized text constraints).
KeyConventionModelBuilderExtensions On SQL Server, gives every single-column Guid primary key a NEWSEQUENTIALID() column default. This applies to all entities, not only contract adopters. EF Core still assigns the key client-side on Add; the default serves inserts that bypass EF Core.

Data isolation topology

ConfigureBlueprintMultiTenancy implements the pooled topology: every entity implementing IMustHaveTenant gets a global query filter on TenantId, an index on that column, and a foreign key to the tenant table.

modelBuilder.ConfigureBlueprintMultiTenancy<ApplicationTenant, ApplicationDbContext>(this);

Because isolation is enforced by the filter and not the database, any table that cannot carry a tenant discriminator — the user table, since identity is shared — is a permanent sharp edge that must be scoped by membership on every query.

Moving to database-per-tenant. Pass addTenantForeignKey: false when tenant-owned data lives in a different database from the tenant table; a foreign key cannot cross databases, so emitting one there produces an invalid model. The filter and index still apply — only referential integrity moves from the database to the application.

modelBuilder.ConfigureBlueprintMultiTenancy<ApplicationTenant, BusinessDbContext>(
    this, addTenantForeignKey: false);

Note that IMustHaveTenant is not a synonym for "shardable". Some tenant-owned entities are identity concerns — a service account is tenant-scoped but authentication needs it, so it must stay in the central database. Classify each entity by plane before splitting anything.

Named global query filters

Both blueprint-owned global filters are registered under a name (BlueprintQueryFilters.Tenant and BlueprintQueryFilters.SoftDelete) so a query can drop one without dropping the other. Parameterless IgnoreQueryFilters() disables every filter on the entity, which would silently remove tenant isolation — never use it to reach soft-deleted rows.

// Reads deleted rows, still scoped to the current tenant.
var pedalo = await context.Pedalos
    .IgnoreSoftDeleteFilter()
    .FirstOrDefaultAsync(p => p.PedaloId == id, ct);

// Same, but restricted to the deleted rows only.
var deleted = await context.Pedalos.OnlySoftDeleted().ToListAsync(ct);

There is deliberately no convenience extension for opting out of the tenant filter: crossing tenants must stay an explicit, reviewed IgnoreQueryFilters([BlueprintQueryFilters.Tenant]) at the call site.

EF Core does not allow an unnamed and a named query filter on the same entity. If your application adds its own filter to a tenant-owned or soft-deletable entity, register it under a name of its own (HasQueryFilter("Archived", e => !e.Archived)); an unnamed one makes the model fail to build. The Tenant and SoftDelete keys are reserved: a filter of yours registered under one of them would replace the blueprint's filter or be replaced by it, so that fails the model build too.

When the conventions are applied

BlueprintDbContext does not apply the contract-driven conventions inside OnModelCreating. It registers BlueprintModelFinalizingConvention in ConfigureConventions, which applies them once EF Core has finished building the model.

That covers every entity type however it entered the model: a DbSet, a configuration applied after your base.OnModelCreating(builder) call (the usual shape of ApplyConfigurationsFromAssembly), or an owned type. Applied inline instead, the conventions would only see the entity types discovered up to that point, and a type registered later would silently get none of them — for IMustHaveTenant no tenant filter, for ISoftDeletable no soft-delete filter.

Your own configuration wins. Every setting is applied at EF Core's convention level, so anything you configure explicitly — fluent API or data annotations — takes precedence, even though the blueprint conventions run last. Map the tenant relationship yourself (with a Tenant navigation or a different delete behavior), give InternalName another length or an index another name, and your mapping is kept; the conventions only fill in what you left unconfigured.

Owned types carry no query filters of their own (EF Core does not allow it); their rows are only reachable through the owner, whose filters apply. A primary key that is also a foreign key, such as an owned type's, gets no NEWSEQUENTIALID() default.

A model convention is used rather than a replaced IModelCustomizer because registering that service means modifying DbContextOptions from OnConfiguring, which EF Core forbids once DbContext pooling is enabled:

'OnConfiguring' cannot be used to modify DbContextOptions when DbContext pooling is enabled.

Since AddPooledDbContextFactory is a normal way to register a context, that approach is not available to a library. A convention touches neither options nor the service provider, so it works under pooling.

If you override ConfigureConventions, call base.ConfigureConventions — that is where the convention is registered, and without it none of the blueprint conventions, tenant isolation and soft-delete filtering included, are applied.

Applications that do not derive from BlueprintDbContext can call the model-wide Configure* extension methods (ConfigureBlueprintMultiTenancy, ConfigureEntityFiltering, ConfigureContractConstraints, ConfigureLocalizedTextConventions, ConfigureGuidPrimaryKeyDefaults and their parts) directly from their own OnModelCreating. They configure at convention level too, so explicit configuration wins whether it comes before or after the call, but they only see the entity types registered so far — call them last. The per-entity helpers you call with type arguments (ConfigureLocalizedTextRelationship, ConfigureLocalizedTextUniqueConstraint, ConfigureLocalization) are explicit configuration of those types, like any fluent call: the last configuration of an aspect wins.

Query extensions for the domain contracts

EntityFilteringExtensions and LocalizationExtensions provide the query-side counterparts to the Csag.Blueprint.Domain contracts:

Contract Extensions
ISoftDeletable WhereNotDeleted(), WhereDeleted(), WhereDeletedBefore()
IHasActiveRange WhereActiveNow(), WhereActiveAt(), WhereActiveToday(), WhereActiveInRange(), WhereInactiveNow(), WhereActiveAndNotDeleted()
IHasLocalizedTexts / ILocalizedText SelectWithCurrentLanguageText(), CurrentLanguageTextExpression(), IncludeCurrentLanguageText(), WhereHasCurrentLanguageText(), GetCurrentLanguageText(), GetCurrentLanguageTextValue()

WhereActiveNow(), WhereActiveAt(), WhereActiveInRange() and WhereInactiveNow() compare against an instant. WhereActiveToday() deliberately compares against midnight of the current UTC day, so an entity whose range starts later today does not yet count as active — use WhereActiveNow() when you need instant precision.

IHasActiveRange is deliberately not a global query filter. An active range is evaluated against a point in time chosen by the caller, and "now" is only one of them — availability searches look at a future window, and administrators legitimately need to see entities that are not active yet or no longer active. Pick the point in time explicitly with the WhereActive* extensions.

Language resolution goes through ICurrentLanguageProvider; DefaultLanguageProvider reads CultureInfo.CurrentUICulture (set per request by the ASP.NET Core request localization middleware) and falls back to a configured language code when the culture is invariant. Use ExplicitLanguageProvider where the language comes from the call itself (an endpoint parameter, a user profile, a tenant setting), or register your own implementation in DI.

The fallback ranking is exact current language → same language in another region (including the bare language code, so de matches a current language of de-CH) → exact fallback → same language as the fallback, compared case-insensitively. A language that matches none of these four tiers is never returned; the projection yields null instead of an arbitrary translation.

SelectWithCurrentLanguageText() applies that ranking inside a single EF Core query, so the translation is resolved by the database rather than in memory:

var cards = await context.Pedalos
    .WhereActiveNow()
    .SelectWithCurrentLanguageText<Pedalo, PedaloText, PedaloCard>(
        languageProvider,
        (pedalo, name) => new PedaloCard(pedalo.PedaloId, name ?? pedalo.InternalName))
    .ToListAsync(ct);

Interceptors

Interceptor Purpose
AuditableTimestampInterceptor Sets CreatedAt/UpdatedAt automatically for IAuditable entities.
TenantSaveInterceptor Assigns and protects TenantId for IMustHaveTenant entities.

Both are registered as singletons by AddBlueprintTenancyRuntime(). Resolve them from the container when wiring the pooled DbContext factory rather than constructing them — they are singleton-safe because they read ambient AsyncLocal state and a singleton TimeProvider, never scoped services. AuditableTimestampInterceptor stamps from that TimeProvider, so a FakeTimeProvider registered before AddBlueprintTenancyRuntime() controls the timestamps it writes.

Session and authorization infrastructure

Component Purpose
DistributedCacheTicketStore ASP.NET Core ITicketStore implementation for server-side session storage. Owns session tracking: every ticket it stores or renews is mirrored into the session's tracking row.
ActiveSessionTracker<TContext> Writes the tracking rows (BlueprintActiveSessions) for the ticket store: create or re-attribute on sign-in, follow the ticket on renewal, delete on removal.
TicketCacheService Serialization/cache wrapper for authentication tickets.
PostConfigureCookieAuthenticationOptions Injects the ticket store into cookie authentication options.
ISessionLifecycle implementation The intents applications call; registered by AddBlueprintSessionInfrastructure.
ExpiredSessionCleanupService<TContext> Hosted job deleting the tracking rows of expired sessions, user and service-account sessions alike.
IServiceAccountCredentials / IServiceAccountSessionLifecycle implementations The service-account intents; registered by AddBlueprintServiceAccountAuthentication. See Service-account credentials.
PermissionClaimsTransformation Expands role claims into permission claims after authentication.
UserManagerAuthorizationExtensions Loads roles and permissions for users.

The packages own the cookie session lifecycle; the application registers it and states intents.

services.AddBlueprintSessionInfrastructure<ApplicationUser, ApplicationDbContext>(options =>
    options.ClientIpHeader = "X-Client-IP");
services.AddBlueprintCookieSessions(cookie =>
{
    cookie.SecurePolicy = securitySettings.CookieSecurePolicy;
    cookie.SameSite = SameSiteMode.Lax;
});

AddBlueprintCookieSessions turns the Identity application cookie into the session cookie, after every other Configure (including AddIdentity's) in either registration order. The callback shapes the cookie; the package then applies what it owns:

  • the ticket store, with the session tracked server-side and the cookie carrying only its key;
  • an HTTP-only, persistent cookie whose lifetime is BlueprintSessionOptions.Lifetime — set from Blueprint:Security:SessionExpirationHours by Csag.Blueprint.Web — with sliding expiration, so sign-in code sets no expiry;
  • 401 and 403 instead of login and access-denied redirects;
  • no Identity security-stamp validation: the session principal carries no stamp, and sessions end through the lifecycle instead;
  • ISessionEligibility asked at every sign-in and sliding renewal.

Cookie event handlers the application assigns on CookieAuthenticationOptions.Events keep running, before the package's. A scheme that sets EventsType, or whose events object overrides the events the package relies on, fails when its options are built.

The tracking row records the client address with the rule the rate limiter uses: ClientIpHeader when present and parseable, otherwise the connection's remote address. A sign-in on a request that still carries a live session cookie reuses that cookie's key; the row is then attributed to the signing-in account.

Applications report what happened through ISessionLifecycle, after their database commit:

Intent Typical caller
CredentialsChangedAsync password change or reset, second factor enabled or disabled
AccessRevokedAsync (optionally per tenant) account disabled or deleted, sign-in identity changed, membership removed
ClaimsChangedAsync (optionally per tenant) profile, language, roles or permissions changed
TenantRemovedAsync tenant deleted
SwitchTenantAsync the caller switches tenant
ListMySessionsAsync / RevokeMySessionAsync the caller's own session management; RevokeMySessionAsync returns false when the caller's account has no session with that id, for the application to answer 404

Hooks the application keeps: ISessionRevocationParticipants for its own credentials, an ISessionEligibility, the cookie callback, its own cookie event handlers, and decorators around ITicketStore or IActiveSessionTracker (resolve DistributedCacheTicketStore or ActiveSessionTracker<TContext> as the inner instance). The expired-session cleanup runs hourly by default; set ExpiredSessionCleanup.Interval, or turn it off with ExpiredSessionCleanup.Enabled = false.

Service-account credentials

The packages own a service account's credentials from end to end: the secret and its hash, issuing bearer tokens, the lockout, rotation, revocation, and validating the tokens. The application keeps its endpoints (route, rate limit, OpenAPI text) and the validation of the account data it accepts, and states intents.

services.AddBlueprintServiceAccountAuthentication<ApplicationDbContext>();

The settings come from Blueprint:Security:Jwt and Blueprint:Security:ServiceAccountLockout, which Csag.Blueprint.Web applies to BlueprintServiceAccountOptions. The registration needs no signing key, so it can run unconditionally — also in a host started only to export its OpenAPI document. A configured key must have at least 32 characters besides surrounding white space, checked at startup; a missing key fails the first issuance or validation.

Intent Effect
IssueTokenAsync(clientId, clientSecret) Returns a token (AccessToken, TokenType = Bearer, ExpiresIn in seconds) or a refusal: InvalidCredentials (401) for an unknown client, a wrong secret or a locked account, AccountInactive (403) only for the correct secret of a deactivated account.
CreateCredentialsAsync(serviceAccount) Generates the client id and secret, stores the account with the secret's hash, returns the secret once.
RotateSecretAsync(serviceAccountId) Generates a new secret; the previous secret and its tokens stop working at once. Looks the account up within the ambient tenant; null when there is none.
AccountDeactivatedAsync(serviceAccountId) / AccountDeletedAsync(serviceAccountId) Ends the account's sessions, after the application committed the change.

Issuance verifies every secret with the same key derivation, unknown clients included. A wrong secret counts towards the lockout in one statement, so concurrent attempts all count; a locked account is refused whatever secret it is given. Every time — issuance, expiry, lockout — comes from the injected TimeProvider. Rotation, deactivation and deletion tell the ISessionRevocationParticipants, with a service-account SessionSubject.

Each token refers to a server-side session and carries only ServiceAccountClaimTypes claims (sub, name, type, sid). The service-account scheme pins HS256, validates issuer, audience and lifetime with no clock skew, signs and validates over the key's UTF-8 bytes, and does not map inbound claims. Every request resolves the token's session and builds the principal from the account's current name, tenant, roles and permissions; register IServiceAccountClaimsEnrichers to add the application's own claims.

Secrets are hashed in ASP.NET Core Identity's PasswordHasher format with fixed parameters, so hashes the application wrote with a default PasswordHasher keep verifying. A data seed sets a known secret with serviceAccount.SetClientSecret(secret).

Three authentication schemes are registered. Each request is authenticated by exactly one scheme:

Scheme Role
BlueprintAuthenticationSchemes.ServiceAccountToken Validates service-account tokens. Endpoints do not list it.
BlueprintAuthenticationSchemes.Bearer ("Bearer") Forwards each request to the handler of its token kind; the service-account scheme unless an application token kind added through BlueprintBearerOptions.AddTokenKind claims the token.
BlueprintAuthenticationSchemes.SessionOrBearer Forwards a request that carries a bearer token to Bearer, every other request to the session cookie.

Expired service-account sessions are deleted by the job AddBlueprintSessionInfrastructure registers.

Tenancy and localization infrastructure

Component Purpose
TenantService / TenantManager Reusable tenant access and membership logic.
BlueprintDbStringLocalizer / BlueprintDbStringLocalizerFactory Database-backed localization infrastructure.
PassThroughStringLocalizer Generation-mode localizer that returns keys as-is.
TranslationCacheKeys Cache key helpers for localization caching.
MigrationBuilderExtensions Translation seeding helpers for migrations.

Table view infrastructure

Component Purpose
TableViewExecutor Executes filtering, sorting, counting, pagination, and projection for table-view queries.
TableViewCatalogService Discovers and filters registered table views by permission.
BlueprintTableViewPreferencesService Persists per-user table view preferences.

Ownership Boundary

This package owns reusable infrastructure, not application composition.

The consuming application still owns:

  • the concrete ApplicationDbContext
  • DI composition and host setup
  • app-specific options and policies
  • app-specific entities and migrations
Product Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Csag.Blueprint.Infrastructure:

Package Downloads
Csag.Blueprint.Web

ASP.NET Core middleware, validated options, security configuration, and FastEndpoints extensions for CSAG Blueprint applications.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.7.0 71 10/28/2026
0.6.0 155 10/23/2026
0.5.0 463 10/20/2026
0.4.0 697 10/14/2026
0.3.0 1,077 9/15/2026
0.2.0 319 9/8/2026
0.1.3 750 8/20/2026
0.1.2 190 8/19/2026
0.1.1 160 8/13/2026
0.1.0 165 7/3/2026