What it is
The Application Template is a working project skeleton plus a pattern library. It covers the solution structure we use for internal web applications, the authentication and authorization approach, database access conventions, deployment steps, and a growing set of documented pitfalls. A new internal application starts from this rather than from an empty folder.
Why it mattered
By the time three internal applications had shipped, the same problems were being solved for the third time. Each had independently worked out how to derive its base path when hosted under a virtual directory, how to configure the web server for a long-running workload, how to structure role-based authorization.
Worse than the duplicated effort was the duplicated failure. The same mistakes were being made a second time in a new codebase, because the lesson from the first occurrence lived in one project’s history where the next project would never look.
What separates this from a generic starter
Almost nothing in this template is theoretical. Every pattern and every documented pitfall traces to a specific incident in a named project, with the symptom recorded alongside the fix. That property is what makes it worth maintaining, and it is also what makes the pitfalls table the most valuable part of it.
The structure
Solution
├── BlazorApp/ Web layer
│ ├── Features/ Vertical slices: page, code-behind, API, client, DTOs
│ ├── Shared/ Layout, nav, authorization wrapper, filter bar
│ └── Program.cs Dependency registration, middleware
├── ServerLibrary/ Business logic, user security service
├── DALLibrary/ Data access implementations, EF context
├── SharedLibrary/ Models, DTOs, interfaces. No external packages.
└── UnitTests/
Dependencies run one direction only, web layer to server library to data access to shared library, with the web layer also referencing shared directly for types. No circular references anywhere.
The organizing principle is vertical slice: a feature’s page, its code-behind, its business logic entry point, its data access client, and its data transfer objects live together, rather than being distributed across folders organized by technical layer. Finding everything related to one feature means opening one folder.
What is actually in it
The template is seven reference documents plus a working skeleton. The documents are the substance, and they are split by concern rather than combined, so a specific question has one place to be answered:
| Document | Covers |
|---|---|
| Interface patterns | Component library setup, the page and code-behind pattern with strict member ordering, shared components, inline edit and confirmation patterns, build quality standards, and a production-learned gotchas section |
| Data patterns | Dependency registration, entity and DTO patterns, the interface pair for business logic and data access, context configuration, anti-patterns, and vertical slice architecture in full |
| Authentication patterns | Package requirements, the user allowlist table, the layout-level auth gate, directory lookup for admin screens, impersonation, and virtual directory hosting |
| Database patterns | Query conventions, bounded retry backoff, connection string rules, and the scheduling-metadata permission recipe |
| Data warehouse patterns | Medallion layering, transformation tooling, scheduling agent usage, and target-mode strategy |
| Integrations | Third-party service patterns, the largest of the documents |
| Setup and deploy | Environment configuration, publish sequence, and the pitfalls table |
The gotchas section, which is the part I would keep if I could keep only one
Every entry below is a production incident that cost real debugging time, and every one shares a property: the code looks correct and the failure is silent.
Script tags in component markup never execute. When the framework updates part of a page over its persistent connection, it diffs the DOM. Script tags introduced by that diff are ignored by the browser entirely; only scripts present in the initial HTML response ever run. Anything that must run on page load has to be registered in the application root, after the framework’s own scripts. There is no error. The script simply does not exist as far as the browser is concerned.
A table’s sort resets on every render if its data source is a computed property. Binding a table component to a property that returns a freshly computed sequence gives the component a new collection reference on each render, so it discards its internal sort state. Clicking a sort header appears to do nothing at all. The fix is to materialize the filtered results into a plain field, updated only when the source data or the filter actually changes, and bind to that field.
A loading spinner cannot appear before the operation it describes. Setting a loading flag does not update the interface. The framework defers re-rendering until the first await point, so if the first await is the slow database call itself, the spinner never renders before the wait begins. The fix needs two steps rather than one: force a render, then yield so the browser actually paints, and only then start the blocking call.
An interpolated string inside a quoted attribute breaks the parser. The inner quote terminates the attribute. This produces a compile error rather than a silent failure, which makes it the friendliest entry here, and the fix is to extract the logic into a named method rather than trying to escape your way out.
A background service registered only as a hosted service cannot be injected. The standard registration creates an internal instance that dependency injection cannot resolve by its concrete type. A component that injects it does not error; it hangs, showing an infinite spinner, because the circuit never resolves the dependency. The fix registers the same instance twice, once as a singleton and once as a hosted service resolving to it.
A page with a route parameter goes stale when navigated to from itself. The framework reuses the component instance, and the initialization method fires only on first load, so changing the parameter leaves the previous data on screen. The documented fix tracks both an initialized flag and a copy of the last parameter value, detects the change in the parameter-set lifecycle method, and reloads, with care taken not to double-load on the first render.
Interactivity is configured once, globally, never per page. A page missing its interactivity directive renders as static output, meaning every button on it silently does nothing. The first fix attempted was per-page, which is the wrong fix; the correct one sets it on the root component so new pages inherit working controls automatically. Adding it per page is redundant and a nested boundary can throw at runtime.
Non-serializable services do not cross an interactivity boundary. Passing a service down the component tree works in the older hosting model and returns null in the newer one, because the value cannot cross into an interactive island. The parent layout keeps working, which is what makes this confusing to diagnose. Direct injection is the documented rule.
Anti-patterns, stated as prohibitions
The template also records things not to do, which is a category most starters omit and which prevents a specific class of quiet bug.
No default row limits on data access methods. A fetch method with a
limit parameter defaulting to five silently returns five rows to every caller
that forgets to override it. This was not hypothetical: a certifications tab
in one application was hiding every record past the fifth most recent, and
nobody noticed because five records looked like a plausible amount of data. If
a caller needs a limit, it can apply one; a hidden default is a page that lies.
No mixed concerns in a partial class split. Import statements do not transfer between partial files. Each file needs its own imports for every type it references, including project namespaces and any file-scoped type aliases, which have to be repeated rather than inherited.
Zero build warnings, as a policy rather than an aspiration. The document covers how to legitimately suppress the two categories of noise that otherwise make the policy impossible: platform warnings on deliberately Windows-only services, and nullable warnings generated by the templating engine rather than by hand-written code. Warnings you have decided to tolerate are warnings you stop reading, and then a real one hides among them.
Patterns earned from specific incidents
Bounded backoff on per-item retries. One project’s background service retried correctly and had no memory of prior failures, so a permanently broken item was retried every few seconds and logged at error level every time. It ran more than five weeks, producing roughly 1.3 million log rows and about forty-one gigabytes of log data. Nothing crashed; the existing guidance already prevented a bad tick from taking down the host and did nothing about this. The pattern that went into the template tracks consecutive failures per item and steps the interval down to a low but deliberately non-zero rate, low enough to stop flooding logs, non-zero so the problem stays visible instead of being silently suppressed.
Explicit certificate trust on every connection string, documented directly alongside the backoff pattern because an unset value here can fail silently forever if the connection sits inside a retry loop, which is precisely the combination that produced the incident above.
The scheduling-metadata permission recipe. Building a job monitoring page surfaced a two-part gap. The built-in roles grant access to a job view and execute rights on the helper procedures, but not direct select rights on the history, activity, or session tables. Separately, granting permissions to the documented login was insufficient, because the connection used integrated security and the real connecting principal was therefore a machine account rather than the login anyone would have expected. Both halves are documented together, including the step of confirming which principal is actually connecting before granting anything, and the rule to query the job view rather than the underlying table.
Warm-up after every recycle, not only first start. The setting that warms an application on startup applies only to the pool’s initial start. A recycle from a health check, configuration change, or scheduled restart leaves the new worker cold, and an application with no regular interactive traffic has nothing arriving to warm it. Named as a symptom-and-fix entry after a production incident.
Never verify a string inside a compiled binary with pattern matching. Reading a binary and searching for a known string is a valid way to confirm a deployment. Doing that search with a regular expression is not, because binary encoding produces false negatives even when the string is present. Three successful deployments were each reported as failed for this reason, costing a full debugging round on a problem that did not exist.
Stage an offline marker before publishing. The web server holds the application binary open while running, so publishing over a live application fails after repeated retries, and can fail in a way that leaves the previous version on disk while appearing to succeed.
Two structures, not one
A late addition worth noting, because it corrected an over-generalization in the template itself. The five-project structure is right for applications with real domain logic, and it is unnecessary ceremony for a job runner or a read-only portal that has no domain of its own.
Rather than forcing every project into the same shape, the template documents a single-project variant for those cases explicitly. A template that only offers one answer gets used where it does not fit, and the resulting project is worse than one built without a template at all, because the mismatch is now load-bearing.
Reusable patterns that came from one project and generalized
Two entries began as project-specific work and were rewritten as general patterns, which is the transition worth watching for:
The dry-run pattern for scheduled jobs. Originally built for one audit job, the document now states it as a general rule with the full wiring on both the job side and the interface side, including the preview email structure and an explicit note on why it is a general pattern rather than a special case for the job that prompted it.
Directory typeahead for email recipient selection. A free-text recipient field was replaced with a lookup against the corporate directory, and the pattern is documented with the fields, methods, and markup needed to reproduce it rather than as a description of what one application does.
A documentation convention that is not fussiness
One convention covers padding markdown table cells to their column’s maximum width. This looks like pure pedantry and is not.
These documents are read in two very different contexts: rendered through a viewer, where alignment is irrelevant, and as raw text in a diff or a plain editor, which is how they are read during review and how an AI assistant loads them as context. Unpadded tables are close to unreadable in the second case, and the second case is the one these files exist for.
Timeline and effort
Work on the template spans roughly 8 weeks, with 9 distinct working sessions recorded. It is not a project with a completion date; entries accumulate whenever another project surfaces something worth carrying forward.
Technical reference
Documented pattern areas. User interface patterns, database patterns, authentication, data access, data warehouse conventions, third-party integrations, and setup and deployment, each maintained as its own reference document rather than one combined file, so each can be referenced independently.
Data warehouse conventions were added as a separate document rather than folded into the general database patterns, since medallion layering, transformation tooling, scheduling agent usage, pointer-based pivoting, and target-mode strategy form a large enough topic to stand alone.
Parameterized route reload pattern. Any page whose route includes a parameter and can be navigated to from itself must handle the parameter changing without re-initialization, since the framework reuses the component instance. Tracking the previously loaded value and reloading on change is documented as a required pattern rather than a suggestion, because the failure mode is a stale page rather than an error.
Where it stands now
Actively maintained and used as the starting point for new internal applications. The working convention I hold to is that a lesson learned on one project is not captured until it lands here, because this is the only place a future project will actually encounter it. A lesson recorded in the project where it happened is a lesson recorded for someone who already knows it.