Obliga · technical summary
A single-tenant .NET 10 / Blazor Server application, deployed entirely inside the customer's own Azure subscription. The engineering bias throughout: boring, explicit code; configuration over hardcoding; and a small set of architecture rules enforced structurally rather than by convention alone.
Stack
| App | .NET 10 (LTS), Blazor Server, MudBlazor component library |
|---|---|
| Data | EF Core → Azure SQL, always via IDbContextFactory — never an
injected DbContext, because Blazor Server circuits outlive a normal request scope |
| Identity | Entra ID OIDC for sign-in; Microsoft Graph for directory sync and mail send |
| Auth model | Managed identity wherever Azure supports it — SQL and Graph auth
both fall back from a client secret to DefaultAzureCredential(), so identical
code runs against a developer's local credential and the deployed App Service's managed
identity |
| Infra | Bicep, deployed via az CLI into the customer's subscription |
Solution layout
Domain references nothing. It's pure C# — the notice-scheduling engine (business
days, notice-point calculation, interim/terminal decision semantics, the failsafe window) lives
here and is exhaustively unit-tested with zero database or network dependency.
Ground rules
User.IsInRole calls
anywhere in the codebase.180
or "Director" in logic is a defect.Kind is Terminal or Interim; the engine
branches on Kind only. Renaming an option can never change behavior.SaveChanges call as the change itself. No
update or delete path exists for audit tables.DateOnly; "days before" math runs in the tenant's configured
timezone, isolated in one Domain service and unit-tested hard.obliga, PascalCase.Consistent
naming and key strategy across every table, no exceptions.Patterns worth knowing
One HasQueryFilter on Contract, referencing per-DbContext-instance
fields (not static state) — the documented EF Core multi-tenancy pattern. Applied uniformly
to the grid, dashboard, detail page, console, and exports through one shared
ContractVisibilityScope.ApplyAsync call, so scoping can't drift between surfaces.
One IMailDispatcher. Notices, weekly digests, and admin job-failure alerts are
three kinds of thing it logs — not three separate send paths — so rule #6 holds by
construction, not by remembering to check it everywhere.
Department mappings and rule-derived role grants carry a provenance flag
(FromDirectorySync / FromRuleId), so a scheduled sync can safely
retract what it granted without ever touching something an admin set by hand.
Service-layer tests run against EF Core's InMemory provider; every task additionally verifies against a real SQL Server (Podman locally, Azure SQL for deployment) — catches provider-specific behavior InMemory silently papers over.
// The recurring shape: client-secret when configured, managed identity otherwise — // identical code path locally and in Azure. if (string.IsNullOrWhiteSpace(options.ClientSecret)) return new DefaultAzureCredential(); return new ClientSecretCredential(tenantId, clientId, options.ClientSecret);
Deployment
App Service (Linux, .NET 10) with a system-assigned managed identity is the one credential
everything else trusts: Storage Blob Data Contributor on the attachment storage account, Key
Vault Secrets User for the sign-in secret, and — the more consequential choice — Microsoft Graph
application permissions (User.Read.All, Group.Read.All,
Mail.Send) granted directly to that same identity rather than to a separate service
principal. Directory sync and mail send need zero stored secrets as a result. The interactive
sign-in app registration is the one exception that genuinely needs a client secret — issued in a
second deployment phase (once the App Service's hostname exists for its redirect URI) and held
in Key Vault, never in application configuration.
270+ automated tests across the Domain and Infrastructure projects — xUnit, exhaustive coverage of the scheduling engine's business-day and cadence math in particular, since it's the one component where a subtle bug would silently cost a customer a missed renewal.