Backend integration
This page shows how to add the engine to your own ASP.NET Core host. For a complete, runnable
example see the Inventory sample; for installing the packages from
GitHub Packages (auth, nuget.config) see Consuming the packages.
1. Reference the engine and wire the host
Section titled “1. Reference the engine and wire the host”Reference Genie.Engine, register it through the fluent builder, and add the exception handler and
hubs to the pipeline:
using Genie.Engine;
builder.Services.AddGenie<YourContext>(genie => genie .LoadFromConfiguration(builder.Configuration)); // config-first: datasource + connection strings + optionsbuilder.Services.AddGenieAuth(builder.Configuration); // minimal auth; features are opt-in
var app = builder.Build();app.UseGenieExceptionHandler(); // first in the pipeline — consistent { success, error, traceId } JSON// ... your middleware ...app.UseAuthentication();app.UseAuthorization();app.MapControllers();app.MapGenieHubs(); // SignalR notification hub at /hubs/genie-hubapp.Run();AddGenie(genie => …) registers the entire engine: view resolution, the unified Object services,
identity/RBAC, workflow, wizard, sequences, notifications, and the model-migration hosted
service. The <YourContext> type parameter registers the engine against a host-derived context so
its EF migrations live in the host assembly (see step 2);
the non-generic AddGenie(genie => …) overload stays on the base GenieContext.
The fluent GenieBuilder
Section titled “The fluent GenieBuilder”All engine configuration flows through the fluent GenieBuilder (the same config-first +
in-code-override pattern as GenieAuthBuilder). LoadFromConfiguration(configuration) binds the
datasource (AppSettings:Datasource), the connection strings, and every options section from
appsettings as the base, then any Use* / Configure* call overrides it in code (later
wins):
builder.Services.AddGenie<YourContext>(genie => genie .LoadFromConfiguration(builder.Configuration) .UseDatasource(Datasource.PostgreSql) // or set AppSettings:Datasource in config .UseConnectionString("PostgreSql", secretFromVault) .ConfigureErrors(e => e.ExposeErrorDetails = false) .ConfigureStorage(s => { s.Type = StorageType.Local; s.Path = "App_Data/storage"; }) .ConfigureEmail(m => { m.Enabled = true; m.SmtpHost = "smtp.example.com"; }));The builder also registers IAppIdentity (the datasource) and the IGenieConnectionStrings seam,
so the host no longer wires those by hand — the data path resolves connection strings through the
seam, not IConfiguration. Available fluent calls include UseDatasource, UseConnectionString,
UseAppIdentityType, AddWorkers, and ConfigureErrors / ConfigureStorage / ConfigureEmail /
ConfigureSms / ConfigureMeilisearch / ConfigureMigrations / ConfigureSchemaCache /
ConfigureUploads (plus ConfigureAuth / ConfigureHangfire, honoured by the umbrella
AddGenieApp).
The umbrella AddGenieApp / MapGenieApp
Section titled “The umbrella AddGenieApp / MapGenieApp”AddGenieApp(genie => …) composes the whole platform from the same builder — the engine +
Identity (ConfigureAuth) + Hangfire (ConfigureHangfire) + Assistant + OpenAPI. Because those
modules read IConfiguration directly, you must call LoadFromConfiguration(configuration)
inside the callback or AddGenieApp throws:
builder.Services.AddGenieApp(genie => genie .LoadFromConfiguration(builder.Configuration) .ConfigureAuth(auth => auth.AddImpersonation()) .ConfigureHangfire(h => h.RedisPrefix = "myapp-jobs"));
var app = builder.Build();app.UseGenieExceptionHandler();app.UseRouting();app.UseAuthentication();app.UseAuthorization();app.MapGenieApp(builder.Configuration); // Razor Pages + both hubs + Hangfire dashboard + OpenAPIapp.Run();MapGenieApp maps the Genie notification hub, the Assistant chat hub, the IP-gated Hangfire
dashboard, and OpenAPI/Swagger. If you compose the engine manually instead (as the Inventory sample
does), map the pieces yourself: MapGenieHubs() for the notification hub, and MapAssistantHubs()
if you enable the Assistant.
Background workers
Section titled “Background workers”The engine’s MonitoredBackgroundService workers (app-notification delivery, email/SMS drain loops,
Meilisearch sync) are config-driven by default — app-notifications always run, the email/SMS
workers run when their channel is Enabled + valid, and the Meilisearch sync worker runs when
Meilisearch is valid. Call AddWorkers(...) to switch to explicit opt-in — only the workers
you list run, so an API host can skip the drain/sync loops while a dedicated Worker host runs them
(the Meilisearch read provider stays active either way; only the sync worker is gated):
builder.Services.AddGenie<YourContext>(genie => genie .LoadFromConfiguration(builder.Configuration) .AddWorkers(w => w .AddAppNotificationWorker() .AddEmailWorker() // still needs a valid email channel .AddSmsWorker() // still needs a valid SMS channel .AddMeilisearchSyncWorker()));See Notifications for the delivery channels and Global search for the Meilisearch provider.
2. Derive your DbContext from GenieContext
Section titled “2. Derive your DbContext from GenieContext”The host owns the EF context so its migrations live in the host assembly:
namespace YourApp.Persistence;
public partial class YourContext(IConfiguration configuration, IAppIdentity appIdentity) : GenieContext(configuration, appIdentity){ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // full Genie schema + seeds modelBuilder.ApplyConfigurationsFromAssembly(typeof(YourContext).Assembly); // your configs + generated ones }}Mark it partial so the source generator can merge the low-code entities’ DbSets into it (see
step 3). ApplyConfigurationsFromAssembly is
what registers the generated IEntityTypeConfiguration<T> classes — no per-entity wiring needed.
Because you passed <YourContext> to AddGenie, every engine service that depends on GenieContext
resolves your derived context.
3. Wire the source generator (entity.xml → EF tables)
Section titled “3. Wire the source generator (entity.xml → EF tables)”Genie.Source is a Roslyn generator. Reference it as an analyzer and feed it your entity files:
<!-- Reference the generator as an analyzer (not a compile reference). --><ProjectReference Include="..\..\..\src\api\Genie.Source\Genie.Source.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<!-- Tell the generator which class to extend with the generated DbSets (your GenieContext-derived context). Defaults to "ZedContext" if unset. --><PropertyGroup> <GenieDbContextName>YourContext</GenieDbContextName></PropertyGroup><ItemGroup> <CompilerVisibleProperty Include="GenieDbContextName" /></ItemGroup>
<!-- Feed the entity models to the generator (it filters to *.entity.xml). --><ItemGroup> <AdditionalFiles Include="..\models\entities\*.entity.xml" /></ItemGroup>For each *.entity.xml the generator emits, into your project’s namespace:
- a
partial class {EntityName} : EntityTraits<long>(audit/tenant/soft-delete +Idcome from the base); - an enum per
Selectfield and an FK navigation perLookupfield; - an
{EntityName}Configurations : EntityBaseConfiguration<{EntityName}, long>(table, schema, indexes, relationships); - a
partial class {GenieDbContextName}with aDbSet<>per entity (merges into your context).
Then create the physical tables with a normal migration:
dotnet ef migrations add <Name> --project YourHost --startup-project YourHost \ --context YourContext --output-dir Persistence/Migrations4. Engine SQL objects (deployed at startup)
Section titled “4. Engine SQL objects (deployed at startup)”The engine’s own database objects — the password-encryption key/procs (sp_encrypt_password /
sp_decrypt_password, or the PostgreSQL pgcrypto functions), the sequence-number functions
([Genie].[GetNextSequenceNumber] / [Genie].[GetSequenceNumberPreview]), and the company-scope
helpers — are deployed at startup by EngineSqlBootstrapper (an IHostedService registered by
AddGenie, ahead of the model-migration service). There is nothing to add to a migration — it’s
automatic. Each unit of engine SQL is owned by a per-feature IEngineSqlContributor
(PasswordEncryptionSqlContributor, SequenceSqlContributor, CompanyScopeSqlContributor); the
bootstrapper collects them, each picks the dialect from the active datasource, substitutes the
configured secrets in memory, executes via raw ADO.NET, and records each unit in the
Genie.ExecutedScripts table keyed by a content hash:
- On first boot, each unit runs and is recorded.
- On later boots, an unchanged unit (same hash) is skipped — the heavy DDL runs only when the SQL
(or the unit’s own
Version) actually changes. This is the same hash-tracking the model migration uses for*.sql/*.entity.xmlfiles.
Secrets are read from the host’s IConfiguration (Security:MasterKeyPassword,
Security:PasswordEncryptionKey, Security:SeedAdminPassword) and are never persisted — the
stored ExecutedScripts.Script and its hash use the tokenised template; only the executed batches
carry the substituted values.
5. Background jobs (Hangfire)
Section titled “5. Background jobs (Hangfire)”AddGenieHangfire(configuration) wires Hangfire on a Redis storage
backend plus an embedded processing server, and registers the Jobs dashboard service that powers
GET/POST /api/v1/hangfire/* (consumed by the React Jobs dashboard). It is included
automatically by AddGenieApp; a host that composes the engine manually calls it itself:
services.AddGenieHangfire(configuration); // after AddGenie(...)The Redis connection is read from ConnectionStrings:Redis (the same instance backing the cache).
If no Redis connection is configured, only the dashboard service registers and the Jobs API returns
503 — the rest of the engine still boots. Settings bind from the Genie:Hangfire section (code
overrides via ConfigureHangfire win on top):
"Genie": { "Hangfire": { "RedisPrefix": "hangfire", "KnownQueues": [ "default", "pipelines", "transfers", "notifications", "logs" ], "DefaultPageSize": 25, "MaxJobsPerStateQuery": 1000 }}All /api/v1/hangfire/* endpoints require the System role. The legacy built-in Hangfire
dashboard (MapGenieHangfireDashboard → /hangfire) remains available behind the IP-allowlist for
hosts that want it.
6. Data Protection (key ring)
Section titled “6. Data Protection (key ring)”AddGenieAuth(configuration) also calls AddGenieDataProtection(configuration), which persists the
ASP.NET Core Data-Protection key ring per the DataProtection config section. The key ring
protects auth cookies, antiforgery tokens, and — critically — the encrypted JWT signing key (see
Identity → signing keys).
It is a no-op when Enabled is false (the default), in which case the framework default
applies: a per-user local key ring, or an ephemeral one under service identities without a
writable profile — keys that don’t survive a restart.
"DataProtection": { "Enabled": true, "Provider": "Redis", // "Redis" or "FileSystem" (default) "ApplicationName": "YourApp", // pins the DP discriminator — default "Zed" "RedisKey": "DataProtection:Keys", // Redis provider: the ring's key (default shown) "KeyPath": "C:\\ProgramData\\YourApp\\dp-keys", // FileSystem provider (default C:\ProgramData\Zed\dp-keys) "UseDpapi": true // FileSystem on Windows: DPAPI-NG at-rest encryption (default true)}Provider: "Redis"stores the ring in Redis (connection fromConnectionStrings:Redis) — one ring shared by every node, surviving restarts and redeploys. Required posture for multi-node hosts, and for any host usingJwtKeyStorage.CacheStore.Provider: "FileSystem"persists toKeyPath(the app-pool identity needs read/write; the ring is DPAPI-NG-encrypted at rest on Windows unlessUseDpapiisfalse). Per-node — fine for single-node hosts withFileSystemJWT keys.ApplicationNamesets the DP application discriminator. Without it the framework default is the content-root path, so moving the deployment folder silently makes old ciphertext undecryptable. Always set it.
What next
Section titled “What next”- Frontend configuration — wire the React app with
createGenieApp. - Model migration — how the engine applies your model files at startup.
- Errors & exception handling — the JSON error envelope and
Genie:Errors. - Object endpoints — the JSON API surface the UI consumes.