AI Agent Rule: Follow this guide to scaffold a new Regira Entities API project from scratch. Start from the
BasicApitemplate 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.mdwhen it exists locally. If it is not available yet, use the fallback baseline in this guide and keep the API surface aligned withapp.MapOpenApi()plusapp.MapScalarApiReference(). When available, combine withentities.namespaces.mdfor exactusingdirectives andentities.examples.mdfor 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 (unless instructed otherwise):
Microsoft.EntityFrameworkCore.Sqlite)Database.EnsureCreated() for the default SQLite starter/test setup; keep the local database disposable and do not scaffold an initial migration unless the user explicitly asks for migrations or chooses a more mature databaseRegira.Entities.Mapping.Mapster)EntityRepository (unless complex logic requires wrapping)EntityControllerBaseTwo 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.
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
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).
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:
Build() and Run(), and a console can reference the API project directly);DbContext (accounts/identity beside the domain);Two principles decide every file’s home:
Models → Data → Services → DependencyInjection → host. Abstractions two stacks share — and no entities — go in an optional Webshop.Core any layer may reference.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
.For<>() registration. Single project → layered is a move by suffix, not a rewrite: the entity and its plain domain enums (OrderStatus.cs), *Dto.cs, *SearchObject.cs, *SortBy.cs, *Includes.cs to Models; *QueryBuilder.cs, *Normalizer.cs, *Manager.cs, *Processor.cs to Services; *ServiceConfiguration.cs to DependencyInjection; *Controller.cs stays in the host.this IEntityServiceCollection<TContext> in, EntityServiceCollection<TContext> out) holds at all three levels, and the simple/complex budget (Checklist 0) counts the same entities.ConfigureDefaultJsonOptions() and keeps its controllers; a console host references only DependencyInjection (§Packages, console row) and seeds per entities.instructions.md — §Seeding via IEntityService.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.
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.BasicApi template in the shared project setup guide (get_package(id: "Regira.Setup", section: "project.setup")) as the starting point..csproj.YourDbContext deriving from DbContext.Program.cs.AddEntityServices).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.AutoMapperis 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.09.x net10.010.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_sqlite3pulled in by the SQLite provider). Options:
- Float the transitive up by adding an explicit higher version as a direct reference, lifting the resolved version above the advisory. The advisory affects the whole
2.1.xline — only the3.xline clears it, so float to the latest3.x:<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.3" />Then verify the advisory is gone:
dotnet list package --vulnerable --include-transitive.- Or accept/suppress it when no fixed version is available and the advisory does not affect your usage:
<PropertyGroup> <NoWarn>$(NoWarn);NU1903</NoWarn> </PropertyGroup>Re-check on each restore and drop the suppression once a patched version ships.
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.Webis the onlyRegira.*package that floorsMicrosoft.AspNetCore.OpenApi, so referencing it whiledotnet 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.OpenApiclears 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.
Start from the
BasicApitemplate in the shared project setup guide and apply the Entities-specific additions below. Needs sign-in? Stay onBasicApiand layerSelfHostingApiWithAuth’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.
- Via MCP: call
get_package(id: "Regira.Setup", section: "project.setup")to read the template.- Via local extraction: read
.regira/instructions/project.setup.mdif it exists.- Fallback (if neither is available): ASP.NET Core Web API project, thin
Program.cs, DI via extension methods,app.MapOpenApi()(requiresMicrosoft.AspNetCore.OpenApi),app.MapScalarApiReference()(requiresScalar.AspNetCore+using Scalar.AspNetCore;), and noUseSwaggerUI().
⚠️ Source encoding — keep
.csfiles ASCII-only. A.csfile 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
\uXXXXescapes for any non-ASCII literal (\UXXXXXXXXabove 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.editorconfigconfigures 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.csIf 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
.editorconfigat the repo root:root = true [*.cs] charset = utf-8-bom indent_style = space indent_size = 4
→ See:
entities.examples.md— DbContext
modelBuilder.SetDecimalPrecisionConvention() in OnModelCreating for global decimal precision (default (18, 4))UseEntities(e => e.UseDefaults()) (see P3), so all DateTime values round-trip as UTC and JSON gets the ISO 8601 Z suffix. (Standalone EF without the entities stack: configurationBuilder.SetUtcDateTimeConvention() in ConfigureConventions, or .AddUtcDateTimeConvention() in AddDbContext.)e => !e.IsArchived on every IArchivable entity) is auto-wired by UseEntities<TContext>(e => e.UseDefaults()) (see P3) and applied after everything OnModelCreating configured. ⚠️ A DbContext you construct yourself — new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()…Options) in tests, a design-time factory, a seeding tool — bypasses that wiring: add .AddArchivedQueryFilter() to those options, or call modelBuilder.SetArchivedQueryFilter() at the end of OnModelCreating (after your own HasQueryFilter(...) calls, exactly once). Startup validation errors out on a model that ends up without it. Round-trip: entities.patterns.md → Soft Delete.net10.0 only, where a real query filter is installed): EF then logs one PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning per relationship whose required principal is IArchivable. ⚠️ Check what the principal is before suppressing it. For an aggregate parent (Order → its lines) it is the intent, and the golden path’s startup-warning checkpoint expects it. For reference data — a category/status/type that separately-registered entities point at through a required FK — it is a silent data bug: the filter propagates into Include(...) as an inner join, so those rows vanish from items while /search still counts them. Such an entity should not be IArchivable at all (real DELETE + OnDelete(Restrict) → 409), or its FK should be optional; startup validation warns on the shape. See entities.patterns.md → Soft Delete. Once confirmed benign, ignore it on the AddDbContext options builder (P3 below, not here in OnModelCreating) with using Microsoft.EntityFrameworkCore.Diagnostics; + .ConfigureWarnings(w => w.Ignore(CoreEventId.PossibleIncorrectRequiredNavigationWithQueryFilterInteractionWarning)), and add a matching HasQueryFilter(x => !x.Parent!.IsArchived) for any dependent you query directly rather than through its parent.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
}
}
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.SqliteleavesPRAGMA foreign_keysoff, soDeleteBehavior.Restrictnever 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: withoutReferenceHandler.IgnoreCyclesa cyclic graph (Order → Customer → Orders) throws aJsonExceptionat request time; withoutJsonStringEnumConverterenums serialize as0/1/2instead of names. Configuring onlyAddControllers().AddJsonOptions(...)fixes the controller wire format but notHttp.Json.JsonOptions— the setAddOpenApi()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-bodyDateTimeproperties (a local offset converted, an offset-less value taken as UTC) that leaves the schema and the wire format alone; itsconfigure/configureHttpcallbacks 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 mapsEntityInputException→ 400 (itsErrorsas aValidationProblemDetails) andEntityConstraintException→ 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}/approveand 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’sIDbContextOptionsConfiguration<TContext>), regardless ofAddDbContext↔UseEntities()call order —AddDbContext<AppDbContext>(options => …)only needs the provider. The match is by assignability, soUseEntities<AppContextBase>()(abstract base) also wires derived provider-specific contexts (AddDbContext<SqlServerAppContext>()). Control the wiring withe.WireDbContext(...)(DbContextWiringflags):DbContextWiring.Noneopts out entirely; withoutUseDefaults()pick pieces à la carte, e.g.e.WireDbContext(DbContextWiring.PrimerInterceptors | DbContextWiring.UtcDateTimeConvention). ⚠️ It reaches contexts EF builds from the service collection. ADbContextyou 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 itsIEntityService<>fromHttpContext.RequestServices, so a controller ↔.For<>()arity mismatch is invisible to it — that one is caught by Regira’s own startup validation (§Startup validation inentities.instructions.md) and by the error message inControllerExtensions.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()plusapp.MapScalarApiReference()and do not addSwashbuckle.AspNetCoreorUseSwaggerUI().
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 callDatabase.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()andapp.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 buildfails withMSB3027/MSB3021— not anerror 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
.dbfile size — query through the app. SQLite’s default is rollback journaling, so committed rows land directly in the.db(only a transient<db>.db-journalappears mid-transaction). Under WAL (<db>.db-walpresent — 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.dbcan look empty (a few KB) while the data is really there.
Quiet SQL logging: EF Core logs every command at
Information. SetLogging:LogLevel:Microsoft.EntityFrameworkCore.Database.CommandtoWarninginappsettings.json. Code-configured Serilog ignores that section unless you callReadFrom.Configuration(...); otherwise add.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning).
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.
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:
vite.config.ts:
server: { proxy: { '/api': { target: 'https://localhost:7xxx', changeOrigin: true, secure: false, xfwd: true } } }
Absolute URLs the API builds (an attachment’s uri) take the scheme and host it received — the target’s,
behind a proxy — so downloads leave the SPA’s origin. xfwd: true forwards the SPA’s; honour it in
Development, after app.UseHttpsRedirection() (placed first, the forwarded http scheme triggers the
redirect):
using Microsoft.AspNetCore.HttpOverrides;
if (app.Environment.IsDevelopment())
app.UseForwardedHeaders(new() { ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto });
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
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().
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).
services.UseRegira(configuration) before UseEntities() to apply paid license keys — omit for the free tier. See §License requirement.options.UseDefaults() to register primers, global query filters, and normalizer servicesoptions.UseMapsterMapping() (default) or options.UseAutoMapper() for DTO mappingoptions.UseAttachmentUris() (namespace Regira.Entities.Web.Attachments.DependencyInjection, package Regira.Entities.Web) and register AddHttpContextAccessor() so attachment DTOs get a resolved Uri. Regira.Entities.DependencyInjection does not reference ASP.NET Core, so this resolution is opt-in; without it the Uri is null (not an error). The Uri links to the GetFile action on the attachment entity’s controller, so keep the generated EntityAttachmentControllerBase<…> endpoints mapped — a custom/replacement download route is not auto-discovered (the Uri stays null; use the download endpoint directly).options.DefaultPageSize / options.MaxPageSize to cap what List/Search controller endpoints return by default — see Paging defaultsAdd{EntityNameInPlural}() extension method per entity — take this IEntityServiceCollection<TContext> as the parameter and return EntityServiceCollection<TContext> (the concrete type every For<>() returns). This keeps chains composable and assignable to IServiceCollection without extra unwrapping.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:
usingEnables Regira.Licensing.DependencyInjectionservices.UseRegira(configuration)inProgram.cs(comes transitively viaRegira.Entities.DependencyInjection)Regira.Entities.Mapping.Mapsteroptions.UseMapsterMapping()Regira.Entities.DependencyInjection.ServiceCollections.ModelsDbContextWiringflags fore.WireDbContext(...)(à-la-carte wiring withoutUseDefaults())Regira.DAL.EFcore.Servicesoptions.AddAutoTruncateInterceptors()inAddDbContext(standalone EF — auto-wired byUseDefaults())Microsoft.EntityFrameworkCore.Include(...)/.ThenInclude(...)inside ane.Includes((q, _) => ...)registration — plain EF Core, needed in the service-config fileThese are all extension methods — the compiler error “does not contain a definition for X” means the
usingis missing, not that the method doesn’t exist.