Regira-Packages

Regira Entities — Project Setup

AI Agent Rule: Follow this guide to scaffold a new Regira Entities API project from scratch. Start from the BasicApi template in the shared project setup guide — get_package(id: "Regira.Setup", section: "project.setup") — and apply the Entities-specific additions below. In consumer repositories, prefer extracted .regira/instructions/project.setup.md when it exists locally. If it is not available yet, use the fallback baseline in this guide and keep the API surface aligned with app.MapOpenApi() plus app.MapScalarApiReference(). When available, combine with entities.namespaces.md for exact using directives and entities.examples.md for complete working code.

The numbered P-steps below (P1–P4) are the one-time project bootstrap — distinct from the per-entity Steps 1–14 in entities.instructions.md.


Defaults

Defaults (unless instructed otherwise):


Project Structure

Two layouts, one file set. An entity is always the same files under the same names — model, DTOs, search object, SortBy/Includes enums, query builder, normalizer, manager/processor, {Entity}ServiceConfiguration.cs — and the two layouts differ only in which project and folder each file lives in. Start with the single project; move to the layered solution when one of its triggers applies.

Single project (default)

The recommended per-entity folder structure — one folder per entity under Entities/, keeping its model, DTOs, search object, processor/manager, and service configuration together. Controllers live under Controllers/ (one per entity), the DbContext under Data/, and DI wiring under Extensions/:

Webshop.API/
├── Controllers/
│   ├── CategoryController.cs
│   ├── CustomerController.cs
│   ├── OrderController.cs
│   └── ProductController.cs
├── Data/
│   └── WebshopDbContext.cs
├── Entities/
│   ├── Categories/
│   │   ├── Category.cs
│   │   ├── CategoryDto.cs
│   │   ├── CategoryInputDto.cs
│   │   ├── CategoryProcessor.cs
│   │   ├── CategorySearchObject.cs
│   │   ├── CategoryServiceConfiguration.cs
│   │   ├── RelatedCategory.cs
│   │   ├── RelatedCategoryDto.cs
│   │   └── RelatedCategoryInputDto.cs
│   ├── Customers/
│   │   ├── Customer.cs
│   │   ├── CustomerDto.cs
│   │   ├── CustomerInputDto.cs
│   │   └── CustomerServiceConfiguration.cs
│   ├── Orders/
│   │   ├── Order.cs
│   │   ├── OrderDto.cs
│   │   ├── OrderIncludes.cs
│   │   ├── OrderInputDto.cs
│   │   ├── OrderLine.cs
│   │   ├── OrderLineDto.cs
│   │   ├── OrderLineInputDto.cs
│   │   ├── OrderManager.cs
│   │   ├── OrderNormalizer.cs
│   │   ├── OrderQueryBuilder.cs
│   │   ├── OrderSearchObject.cs
│   │   ├── OrderServiceConfiguration.cs
│   │   └── OrderStatus.cs
│   └── Products/
│       ├── Product.cs
│       ├── ProductCategory.cs
│       ├── ProductCategoryDto.cs
│       ├── ProductCategoryInputDto.cs
│       ├── ProductDto.cs
│       ├── ProductInputDto.cs
│       ├── ProductQueryBuilder.cs
│       ├── ProductSearchObject.cs
│       ├── ProductServiceConfiguration.cs
│       └── ProductSortBy.cs
├── Extensions/
│   └── ServiceCollectionExtensions.cs
└── Program.cs

Beyond entities (larger apps)

A small app needs nothing more. Once an app carries code that belongs to no single entity — workflow services, security filters, background jobs, seeders, helpers — give each kind its own folder beside Entities/, so an entity folder keeps only its entity’s files (the set above, plus that entity’s own prepper, processor and query builder):

Webshop.API/
├── Data/
│   └── Seeding/        # seeders
├── Infrastructure/     # host and cross-cutting concerns, e.g. HostingExtensions.cs
│   ├── Security/       # role constants, row-scope and write filters, claims factory
│   └── Jobs/           # background services
├── Services/           # domain services spanning entities: workflows, notifications, code generators, a scoped WorkflowContext
└── Utilities/          # helpers that reference no app type: formatting, date and string math

Utilities/ holds no app type — a helper that needs an entity or the DbContext is a service. Don’t reuse Regira’s own *Utility names there (DateTimeUtility): with both namespaces imported the simple name is ambiguous (CS0104).

Layered solution

The shape of an application that has outgrown the free tier. The free budget (5 simple + 2 complex registrations — Checklist 0) fits a single project by construction: none of the triggers below pays for a split at that size, and dividing a free-tier app into layer projects adds .csproj files and buys nothing.

Split the single project into one project per technical layer once the domain has outgrown the free tier and one of these applies:

Two principles decide every file’s home:

  1. Projects by layer, mirroring the package stack. Each layer project references only the package its layer needs, so the project graph is the package graph of §Packages: Models → Data → Services → DependencyInjection → host. Abstractions two stacks share — and no entities — go in an optional Webshop.Core any layer may reference.
  2. Domain folders repeat across layers. Inside every layer project the folders are per domain, then per entity — a folder per entity where it has several files (Models, Services), one file per entity where it has one (DependencyInjection). The same path recurs in every layer, and namespaces follow the folders (Webshop.Models.Catalog.Products).

Layer projects live under src/, hosts and tools under app/. The Webshop example with two domains — Catalog (Products, Categories) and Sales (Customers, Orders):

src/
├── Webshop.Models/                    → Regira.Entities
│   ├── Catalog/
│   │   ├── Categories/                Category.cs · CategoryDto.cs · CategoryInputDto.cs · CategorySearchObject.cs · RelatedCategory*.cs
│   │   └── Products/                  Product.cs · ProductDto.cs · ProductInputDto.cs · ProductSearchObject.cs · ProductSortBy.cs · ProductCategory*.cs
│   └── Sales/
│       ├── Customers/                 Customer.cs · CustomerDto.cs · CustomerInputDto.cs
│       └── Orders/                    Order.cs · OrderDto.cs · OrderInputDto.cs · OrderSearchObject.cs · OrderIncludes.cs · OrderStatus.cs · OrderLine*.cs
├── Webshop.Data/                      → Regira.Entities.EFcore + the EF Core provider; references Models
│   └── WebshopDbContext.cs
├── Webshop.Services/                  references Data + Models (Regira.Entities.EFcore arrives through Data)
│   ├── Catalog/
│   │   ├── Categories/                CategoryProcessor.cs
│   │   └── Products/                  ProductQueryBuilder.cs
│   └── Sales/
│       └── Orders/                    OrderQueryBuilder.cs · OrderNormalizer.cs · OrderManager.cs
└── Webshop.DependencyInjection/       → Regira.Entities.DependencyInjection + Regira.Entities.Mapping.Mapster; references Services
    ├── Catalog/                       CatalogServiceConfiguration.cs · CategoryServiceConfiguration.cs · ProductServiceConfiguration.cs
    ├── Sales/                         SalesServiceConfiguration.cs · CustomerServiceConfiguration.cs · OrderServiceConfiguration.cs
    └── Extensions/
        └── ServiceCollectionExtensions.cs
app/
├── Webshop.API/                       → Regira.Entities.Web; references DependencyInjection
│   ├── Controllers/                   CategoryController.cs · CustomerController.cs · OrderController.cs · ProductController.cs
│   ├── Infrastructure/
│   │   └── HostingExtensions.cs
│   ├── Program.cs
│   └── appsettings.json
└── Webshop.Seeder/                    console (`ConsoleWithLogging`); references DependencyInjection only — no Regira.Entities.Web
    └── Program.cs

DI has three levels, each a file in Webshop.DependencyInjection. The root also owns the license registration and AddDbContext — in the single project P3 and P4 put both in Program.cs — so every host, API or console, gets the same license, context and provider from one call:

// Extensions/ServiceCollectionExtensions.cs — root: the one call every host makes
public static IServiceCollection AddEntityServices(this IServiceCollection services, IConfiguration configuration)
    => services
        .UseRegira(configuration)   // Regira.Licensing.DependencyInjection — paid tier: this app is past the free budget by definition
        .AddDbContext<WebshopDbContext>(o => o.UseSqlite(configuration.GetConnectionString("Default")))
        .UseEntities<WebshopDbContext>(options =>
        {
            options.UseDefaults();
            options.UseMapsterMapping();
        })
        .AddCatalog()
        .AddSales();

// Catalog/CatalogServiceConfiguration.cs — domain aggregator: one chain per domain
public static EntityServiceCollection<WebshopDbContext> AddCatalog(this IEntityServiceCollection<WebshopDbContext> services)
    => services
        .AddProducts()
        .AddCategories();

// Catalog/ProductServiceConfiguration.cs — per entity: identical to the single-project file
public static EntityServiceCollection<WebshopDbContext> AddProducts(this IEntityServiceCollection<WebshopDbContext> services)
    => services.For<Product, ProductSearchObject, ProductSortBy, EntityIncludes>(e =>
    {
        e.AddFilter<ProductQueryBuilder>();
        // …
    });

What does not change

Controllers stay in the host; when a second host must serve the same endpoints, move Controllers/ into a Webshop.Web class library (references Regira.Entities.Web + DependencyInjection) that both hosts reference.

The folders of Beyond entities map onto the layers: cross-entity domain services into Webshop.Services (under their domain folder), helpers into a Utilities/ folder of Webshop.Core, seeders beside the host that runs them, and Infrastructure/ stays in each host — except role constants, which move to Webshop.Core once a layer below the host needs them.


Checklist

  1. Plan the simple/complex split first. Mark every entity simple or complex before scaffolding — it sets each entity’s endpoints and controller generics, and the free-tier budget. Definitions, the decision table, and a worked budget example: entities.instructions §Step 0. 0.5. Pin your EF Core provider to your target framework. Regira’s EF Core packages multi-target, so the provider you add (Microsoft.EntityFrameworkCore.Sqlite/.SqlServer/Npgsql.EntityFrameworkCore.PostgreSQL/…) must match the EF Core major for your TFM: net8.0/net9.0 → 9.x, net10.0 → 10.x. A mismatch builds cleanly but crashes on the first query — see Troubleshooting.
  2. Create an ASP.NET Core Web API project — use the BasicApi template in the shared project setup guide (get_package(id: "Regira.Setup", section: "project.setup")) as the starting point.
  3. Add required packages to .csproj.
  4. Create YourDbContext deriving from DbContext.
  5. Configure Program.cs.
  6. Create the DI extension method (AddEntityServices).
  7. Add entities — see Entity Implementation Workflow.

Packages

Each package references the one below it, so installing the top of the stack pulls the rest in transitively. A Web API needs only two references: Regira.Entities.Web + a mapper.

Regira.Entities.Web                       ← Web API entry point — EntityControllerBase + HTTP endpoints
└─ Regira.Entities.DependencyInjection    ← non-web entry point — UseEntities() / .For<>()
   └─ Regira.Entities.EFcore              EntityRepository (EF Core)
      └─ Regira.Entities                  abstractions / interfaces

Regira.Entities.Mapping.Mapster           ← add separately — DTO mapping (NOT pulled transitively)
Regira.Entities.Validation.FluentValidation ← optional — AbstractValidator rules as entity validators
Host Install
Web API (default) Regira.Entities.Web + Regira.Entities.Mapping.Mapster
Console / worker (no HTTP) Regira.Entities.DependencyInjection + Regira.Entities.Mapping.Mapster

Regira.Entities.Mapping.AutoMapper is an alternative to Mapster (deprecated). Its AutoMapper floor is pinned to 14.0.0 — the last MIT-licensed release — which restores with a NU1903 high-severity advisory; prefer Mapster, or accept the advisory consciously.

EF Core provider (e.g. Microsoft.EntityFrameworkCore.Sqlite): pin its major to your TFM — see Checklist 0.5.

Your TFM EF Core provider major
net8.0 / net9.0 9.x
net10.0 10.x

A mismatch (e.g. an EF Core 10 provider on net8.0) restores and builds cleanly, then throws on the first query at runtime — the hardest variant to diagnose. Pin the provider major explicitly.

Optional — entity attachments: Regira.IO.Storage (local file system / SFTP) or Regira.IO.Storage.Azure (Azure Blob Storage).

NuGet advisory with no patched version (NU1903 / NU1902). A transitive dependency can raise a security advisory for which no fixed version exists yet (commonly seen via SQLitePCLRaw.bundle_e_sqlite3 pulled in by the SQLite provider). Options:

Known-good package versions. What these guides assume, so nothing resolves from *. Two kinds of row: an exact pin you copy verbatim, and a major constraint where the patch is whatever dotnet add package resolves today — never invent a patch number for those.

Package Version
Regira.Entities.* major 6; resolve the patch at add time. Keep the Regira.* packages you reference directly on the same version, or restore reports NU1605
Microsoft.OpenApi no direct reference — it arrives through Microsoft.AspNetCore.OpenApi, whose range is the compatible one by definition — from 10.0.11 on it is past the 2.x advisory (10.0.12 → [2.12.0, 3.0.0)); 10.0.10 and earlier pull the vulnerable 2.0.0, one more reason that package is added first (below). It must stay on 2.x: 3.x breaks the .NET 10 OpenAPI source generator, and dotnet add package Microsoft.OpenApi resolves 3.x with at most an NU1608 warning. Overriding it anyway? Pin by hand the version restore already resolves (dotnet list package --include-transitive) — anything lower fails restore with NU1605
SQLitePCLRaw.bundle_e_sqlite3 pin 3.0.3
Microsoft.EntityFrameworkCore.* (+ provider) major must equal the TFM’s EF Core major (net10.0 → 10.x, see Checklist 0.5); resolve the patch at add time
Microsoft.AspNetCore.OpenApi major must equal your TFM (net10.0 → 10.x); resolve the patch at add time. ⚠️ dotnet new webapi pins the patch its SDK shipped with, which can sit below the floor Regira.Security.Authentication.Web sets (SDK 10.0.400 writes 10.0.11; the floor is 10.0.12) — adding sign-in then trips NU1605 until you raise the pin
Scalar.AspNetCore major 2; resolve the patch at add time
Serilog.AspNetCore, Serilog.Settings.Configuration, Serilog.Sinks.Console latest stable major — never a preview; resolve the patch at add time. The console/file sinks arrive transitively with Serilog.AspNetCore, so add them explicitly only if you pin them

Add them as commands, not as hand-written XML. Which rows you may type by hand is then structural rather than a rule to remember: only the exact pin below is XML, and every major-constraint row resolves its own patch.

dotnet add package Microsoft.AspNetCore.OpenApi   # first — lifts the template's pin, so adding auth later can't downgrade (see below)
dotnet add package Regira.Entities.Web            # resolves the newest stable as a concrete pin; check it's a 6.x
dotnet add package Regira.Entities.Mapping.Mapster    # same version as the row above
dotnet add package Microsoft.EntityFrameworkCore.Sqlite     # patch resolved at add time; check the major against your TFM
dotnet add package Scalar.AspNetCore
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Settings.Configuration

⚠️ Adding sign-in later? Regira.Security.Authentication.Web is the only Regira.* package that floors Microsoft.AspNetCore.OpenApi, so referencing it while dotnet new webapi’s stale pin is still in place fails restore with NU1605. Raising the pin first (above) avoids it. If you hit it anyway, dotnet add package Microsoft.AspNetCore.OpenApi clears it — that command still lands while the downgrade is live, and no hand-editing is needed.

<!-- the one row that is pinned rather than resolved -->
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.3" />

⚠️ A patch number you invent restores with NU1603 (“x.y.z was not found; x.y.z+n was resolved instead”) — or NU1605 when a sibling package pulls a higher version of the same dependency. NU1603 is a warning, so a hand-authored .csproj can carry a version that does not exist and still build; NU1605 is an error by default and stops the restore dead.


P1: Project Files

Start from the BasicApi template in the shared project setup guide and apply the Entities-specific additions below. Needs sign-in? Stay on BasicApi and layer SelfHostingApiWithAuth’s auth registrations onto it — the selection guide’s Standard hosted API with auth row spells out which parts to take. Hosting and auth are separate choices; this combination is ordinary, not a deviation.

⚠️ Source encoding — keep .cs files ASCII-only. A .cs file saved as UTF-8 without a BOM that contains non-ASCII characters (→, ×, é, an em dash, smart quotes — in comments, seed data or string literals) is misread by the C# compiler/tooling on Windows as Windows-1252, producing mojibake (Lille Studio — Meeting rooms) that survives into the database and out through the API.

Write plain ASCII and use \uXXXX escapes for any non-ASCII literal (\UXXXXXXXX above U+FFFF — every emoji; a @"..." or raw string literal processes no escapes, so keep such text in a regular string). That is the only remedy that holds when files are written by a tool: an .editorconfig configures editors, so a process writing bytes directly ignores it and every file it creates is BOM-less regardless. Non-ASCII prose in comments is the norm, so this bites early and the fix is a full reseed.

Audit and escape an existing file without improvising a script — this reads first and writes once, so a failure mid-way cannot truncate the source; it drops a BOM, and it holds no backslash for a shell to mangle (chr(92)), so it runs unchanged from bash and PowerShell:

# report every non-ASCII character, with its line number
grep -nP "[^\x00-\x7F]" Data/SeedCatalog.cs
# rewrite the file with \uXXXX / \UXXXXXXXX escapes in place of them
python -c "import sys,io; p=sys.argv[1]; s=io.open(p,encoding='utf-8-sig').read(); e=lambda c: chr(92)+('u%04x'%ord(c) if ord(c)<65536 else 'U%08x'%ord(c)); io.open(p,'w',encoding='ascii').write(''.join(c if ord(c)<128 else e(c) for c in s))" Data/SeedCatalog.cs

If you do write non-ASCII, the file must carry a UTF-8 BOM — verify it after writing, per file, and pin the editor default with an .editorconfig at the repo root:

root = true

[*.cs]
charset = utf-8-bom
indent_style = space
indent_size = 4

P2: Create DbContext

→ See: entities.examples.md — DbContext

using Regira.DAL.EFcore.Extensions;      // SetDecimalPrecisionConvention

public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
    // public DbSet<Product> Products => Set<Product>();   // add per entity

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.SetDecimalPrecisionConvention(); // global decimal precision, default (18, 4)
        // configure relationships per entity here
    }
}

P3: Program.cs

Changes to BasicApi

// ... usings from BasicApi
using Microsoft.EntityFrameworkCore;

// HTTP contract: ignore reference cycles + nulls, serialize enums as names, read DateTimes as UTC — applied to BOTH the MVC
// options and Http.Json.JsonOptions, the set AddOpenApi() and minimal-API results read, so the generated
// schema matches the wire format — plus the entity-exception filter (400 / 409) for every action.
builder.Services.AddControllers();
builder.Services.ConfigureDefaultJsonOptions();   // Regira.Entities.Web.DependencyInjection

// add DbContext — only the provider is needed here: UseEntities(e => e.UseDefaults()) (inside
// AddEntityServices) auto-wires the primer/normalizer/auto-truncate interceptors and the UTC date
// convention (DateTime values round-trip as UTC → Z suffix in JSON) into these options
builder.Services.AddDbContext<AppDbContext>(options =>
    // use DB provider at wish
    options.UseSqlite(builder.Configuration.GetConnectionString("Default")));

// add entity services (repositories) and configurations
builder.Services.AddEntityServices(builder.Configuration);

// ...
// build app and configure as in BasicApi

⚠️ SQLite ignores foreign keys unless the connection string enables them. Microsoft.Data.Sqlite leaves PRAGMA foreign_keys off, so DeleteBehavior.Restrict never fires, cascades don’t run, and orphan rows are accepted — no error, no 409. Enable it per connection:

"ConnectionStrings": { "Default": "Data Source=app.db;Foreign Keys=True" }

⚠️ Why the one-liner, not AddJsonOptions. All three settings are required once entities have related entities or enums: without ReferenceHandler.IgnoreCycles a cyclic graph (Order → Customer → Orders) throws a JsonException at request time; without JsonStringEnumConverter enums serialize as 0/1/2 instead of names. Configuring only AddControllers().AddJsonOptions(...) fixes the controller wire format but not Http.Json.JsonOptions — the set AddOpenApi() and minimal-API results (Results.Ok(...)) read — so the generated schema types enums as integers while the API sends names, a mismatch nothing reports that reaches the SPA as wrong types. ConfigureDefaultJsonOptions() applies all three to both — plus a UTC read of request-body DateTime properties (a local offset converted, an offset-less value taken as UTC) that leaves the schema and the wire format alone; its configure / configureHttp callbacks target the two sets separately (a converter added to only one re-creates the mismatch).

The second reason to call it: the entity exceptions. It also registers EntityExceptionFilter, which maps EntityInputException → 400 (its Errors as a ValidationProblemDetails) and EntityConstraintException → 409 for every MVC action. Without it that mapping reaches only the generated write actions, so a hand-written domain action on an entity controller — POST {id}/approve and its kind — answers a rule breach with a 500 and a stack trace. MapEntityExceptions() registers the filter on its own for a host that configures its JSON itself.

DbContext wiring is automatic. UseEntities<TContext>(e => e.UseDefaults()) contributes the primer/normalizer/auto-truncate interceptors, the UTC date convention, the archived query filter and the concurrency-token convention to the context’s options itself (via EF’s IDbContextOptionsConfiguration<TContext>), regardless of AddDbContext ↔ UseEntities() call order — AddDbContext<AppDbContext>(options => …) only needs the provider. The match is by assignability, so UseEntities<AppContextBase>() (abstract base) also wires derived provider-specific contexts (AddDbContext<SqlServerAppContext>()). Control the wiring with e.WireDbContext(...) (DbContextWiring flags): DbContextWiring.None opts out entirely; without UseDefaults() pick pieces à la carte, e.g. e.WireDbContext(DbContextWiring.PrimerInterceptors | DbContextWiring.UtcDateTimeConvention). ⚠️ It reaches contexts EF builds from the service collection. A DbContext you construct yourself (new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()…Options)) gets none of it — add what that context needs to its own options builder (.AddArchivedQueryFilter(), .AddConcurrencyTokenConvention(), .AddUtcDateTimeConvention(), …).

ValidateOnBuild: turn it on — a .For<>() with a wrong generic argument, or one never added at all, then throws when the host builds instead of on some later request.

builder.Host.UseDefaultServiceProvider(o =>
{
    o.ValidateOnBuild = true;
    o.ValidateScopes = true;
});

⚠️ It only sees constructor-injected dependencies. EntityControllerBase<> resolves its IEntityService<> from HttpContext.RequestServices, so a controller ↔ .For<>() arity mismatch is invisible to it — that one is caught by Regira’s own startup validation (§Startup validation in entities.instructions.md) and by the error message in ControllerExtensions.GetRequiredEntityService.

OpenAPI/UI note: If the shared project guide is not available locally yet, keep the API surface aligned with the Regira baseline here as well: use app.MapOpenApi() plus app.MapScalarApiReference() and do not add Swashbuckle.AspNetCore or UseSwaggerUI().

SQLite starter note: For the default SQLite starter/test setup, do not scaffold an initial EF migration. After app = builder.Build(), create a scope and call Database.EnsureCreated() instead:

using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    dbContext.Database.EnsureCreated();
    // seed here, in the same scope — see entities.instructions §Seeding via IEntityService
}

⚠️ This block, and any seeding, must sit between builder.Build() and app.Run(). app.Run() blocks until shutdown, so anything written after it never executes and the app simply starts up empty — no exception, no log line.

⚠️ Reset the database after any model change — EnsureCreated() does not migrate. It creates the schema only when the database is absent, so any model change after the first run stays invisible — a new table/column simply won’t exist — until the database is dropped and re-created (and re-seeded). Treat the SQLite database as disposable; adopt explicit migrations only once the schema stabilizes. A switch in the startup block beats deleting the file by hand, which a running host or an open viewer may still hold:

if (app.Environment.IsDevelopment() && app.Configuration.GetValue<bool>("ResetDatabase"))
    dbContext.Database.EnsureDeleted();          // dotnet run -- --ResetDatabase=true
dbContext.Database.EnsureCreated();

Stop the running app before rebuilding. The host holds its own DLL, so dotnet build fails with MSB3027/MSB3021 — not an error CS…, which is what a build log is usually searched for — and the next start runs the previous binary. A seeder or model fix that “has no effect” is this, until proven otherwise.

Don’t judge seeding by the .db file size — query through the app. SQLite’s default is rollback journaling, so committed rows land directly in the .db (only a transient <db>.db-journal appears mid-transaction). Under WAL (<db>.db-wal present — treat it as expected, not as something you must have configured), freshly-committed rows instead sit in the WAL file until a checkpoint, so the .db can look empty (a few KB) while the data is really there.

Quiet SQL logging: EF Core logs every command at Information. Set Logging:LogLevel:Microsoft.EntityFrameworkCore.Database.Command to Warning in appsettings.json. Code-configured Serilog ignores that section unless you call ReadFrom.Configuration(...); otherwise add .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning).

API route prefix

Keep controller routes resource-relative and spell the resource — [Route("products")] — then apply a shared api base once at the host/app level so it stays configurable. Avoid the [controller] token: it expands to the class-name stem, so a SupplierController serves /Supplier instead of the /suppliers the SPA calls. Either host under that base path (IIS virtual directory / reverse proxy, or app.UsePathBase("/api")), or register a global route-prefix convention. Regira.Entities.Web already brings Regira.Web transitively, so Regira.Web.Routing’s UseCentralRoutePrefix is available without another reference — it configures MvcOptions, so call it where those are built: builder.Services.AddControllers(o => o.UseCentralRoutePrefix(new RouteAttribute("api")));. The self-contained equivalent below is here for a host that does not reference it:

// namespace Microsoft.AspNetCore.Mvc.ApplicationModels (RouteAttribute is in Microsoft.AspNetCore.Mvc)
public sealed class RoutePrefixConvention(string prefix) : IApplicationModelConvention
{
    private readonly AttributeRouteModel _prefix = new(new RouteAttribute(prefix));
    public void Apply(ApplicationModel app)
    {
        foreach (var controller in app.Controllers)
            foreach (var selector in controller.Selectors)
                selector.AttributeRouteModel = selector.AttributeRouteModel is { } existing
                    ? AttributeRouteModel.CombineAttributeRouteModel(_prefix, existing)
                    : _prefix;
    }
}
// Program.cs — the prefix can come from configuration
builder.Services.AddControllers(o => o.Conventions.Add(new RoutePrefixConvention("api")));

Spell a multi-word resource in kebab-case plural: InterventionType → [Route("intervention-types")], FacetGroup → [Route("facet-groups")]. The SPA’s IConfig.api must match it character-for-character, so a flattened interventiontypes or a camelCase interventionTypes costs a 404 on every call to that entity.

Building the paired SPA? This prefix is one of four settings that must line up — SPA axios base, entity IConfig.api, the Vite dev proxy, and this route prefix. The front-end guide resolves them as one contract: regira_modules.vue.entities → entities.setup → The URL contract — four owners, one request.

Local development vs production (HTTPS + a dev SPA)

app.UseHttpsRedirection() 307-redirects every HTTP request to HTTPS. That is correct in production, but it breaks a browser SPA dev server (Vite, etc.) that proxies API calls over plain HTTP — the preflight/redirect chain fails and requests never reach the API. Two safe options:

CORS for a dev SPA on a different origin. When the SPA is served from its own dev origin (e.g. http://localhost:5173) rather than proxied, the browser needs CORS. This is a general web concern the consumer owns — the front-end guide wires it as part of the dev SPA setup (regira_modules.vue.entities → entities.setup). Two constraints an agent must not get wrong when writing the policy: use SetIsOriginAllowed(...) (e.g. accepting any loopback origin in dev), not AllowAnyOrigin() — the latter throws at runtime when combined with AllowCredentials(); and place app.UseCors(...) before UseAuthorization()/MapControllers().


P4: Create the DI Extension Method

Create Extensions/ServiceCollectionExtensions.cs. The complete wiring pattern is below; for a real Add{Entities}() body see entities.examples.md — Order + OrderLine entities (OrderServiceConfiguration.cs).

Complete wiring pattern — callback vs. return value:

// Program.cs — paid keys only: register the license once, before any module setup. Omit on the free tier
services.UseRegira(configuration);

// Extensions/ServiceCollectionExtensions.cs
using Regira.Entities.DependencyInjection.Extensions;
using Regira.Entities.Mapping.Mapster;

public static IServiceCollection AddEntityServices(this IServiceCollection services, IConfiguration configuration)
    => services
        .UseEntities<AppDbContext>(options =>
        {
            // ↑ options is EntityServiceCollectionOptions — global settings only
            // a paid license is resolved from the UseRegira() registration — no license key here
            options.UseDefaults();
            options.UseMapsterMapping();
        })
        // ↑ UseEntities returns EntityServiceCollection<AppDbContext>
        // Chain per-entity registrations
        .AddProducts()
        .AddCategories();

First-build usings quick-reference — the most commonly missed extension-method namespaces:

using Enables
Regira.Licensing.DependencyInjection services.UseRegira(configuration) in Program.cs (comes transitively via Regira.Entities.DependencyInjection)
Regira.Entities.Mapping.Mapster options.UseMapsterMapping()
Regira.Entities.DependencyInjection.ServiceCollections.Models DbContextWiring flags for e.WireDbContext(...) (à-la-carte wiring without UseDefaults())
Regira.DAL.EFcore.Services options.AddAutoTruncateInterceptors() in AddDbContext (standalone EF — auto-wired by UseDefaults())
Microsoft.EntityFrameworkCore .Include(...) / .ThenInclude(...) inside an e.Includes((q, _) => ...) registration — plain EF Core, needed in the service-config file

These are all extension methods — the compiler error “does not contain a definition for X” means the using is missing, not that the method doesn’t exist.