Extended templates: full multi-layer extensibility alignment¶
This document is the primary operational entry point for aligning a Layer 3 repository with ConnectSoft.BaseTemplate (base-template/ git submodule) and the surrounding template system. It ties together build-time (clone, MSBuild, Docker, tests), template installer pipelines (NuGet pack, composed template.json, dotnet new CI gate), and where to read generation-time overlays and metadata—without duplicating the long-form specifications.
Canonical reference pair
Treat ConnectSoft.BaseTemplate (Layer 2) + ConnectSoft.IdentityTemplate (Layer 3) as the baseline. Walk Identity first for submodule layout, slim host props, Docker, application CI, and template pack/composition. ConnectSoft.WorkerTemplate (worker host, HangFire) and ConnectSoft.ApiGatewayTemplate (API gateway / YARP-style stack) are additional examples with documented deltas.
Audience and scope¶
| In scope | Out of scope |
|---|---|
| Layer 1–3 roles: Extensions NuGet (L1), BaseTemplate submodule (L2), specialized repo (L3) | Editing files inside base-template/ from a Layer 3 repo (work in BaseTemplate, then bump the submodule pointer) |
| MSBuild/CPM import chain, slim-host / post-import props, solution shape | Full Software Factory recipe design (see overlay specs) |
Docker from repo root; application azure-pipelines.yml (lint/build/test/Docker) |
Rewriting BaseTemplate product code |
Template installer azure-pipelines-template.yml: staging pack, compose, dotnet new gate |
Consuming only NuGet with no submodule when the repo is an extended template |
| Pointers to generation-time overlays and metadata composition |
Deep theory and formal rules: Template layering and reuse, Template architecture specification.
Concept map — layers¶
Layer 1 — ConnectSoft.Extensions.* (and related) NuGet packages. Layer 2 — ConnectSoft.BaseTemplate (shared kernel, optional base-template/ submodule in L3). Layer 3 — specialized template repos (Identity, Worker, ApiGateway, …) that add domain/host projects and usually replace the default BaseTemplate Application in their solution.
flowchart LR
subgraph layer1 [Layer1]
Ext[ConnectSoft.Extensions NuGet]
end
subgraph layer2 [Layer2]
Base[ConnectSoft.BaseTemplate]
end
subgraph layer3 [Layer3]
Id[IdentityTemplate primary]
Other[Worker ApiGateway etc]
end
Ext -->|CPM| Base
Ext -->|CPM| Id
Base -->|submodule base-template| Id
Base -->|submodule base-template| Other
Build-time repo vs shipped installer vs generation-time¶
| Phase | What it is | Typical artifacts |
|---|---|---|
| Build-time | Developers clone the Layer 3 repo; base-template/ is a submodule |
Root Directory.*.props, solution .slnx, build/*.props, Dockerfile, azure-pipelines.yml |
| Shipped .nupkg | Template installer package produced by azure-pipelines-template.yml |
Staged tree, consumer Directory.Build.props under base-template/, composed .template.config/template.json (base + extend + second sources entry for Layer 3 files) |
| Generation-time | Software Factory / dotnet new recipes, overlays, metadata merge |
Template overlays specification, Template metadata composition |
flowchart LR
subgraph buildTime [BuildTimeRepo]
Sub[base-template submodule]
MSBuild[Directory Build props]
App[Layer3 projects]
end
subgraph installer [TemplateInstaller]
Stage[prepare-template-pack.ps1]
Pack[nuget pack BasePath]
Compose[template-compose.ps1]
Gate[dotnet new plus build]
end
subgraph gen [GenerationTime]
Over[Overlays and recipes]
end
buildTime --> installer
installer --> Gate
gen -.->|specs| installer
Which document should I read?¶
| Document | Purpose | Link |
|---|---|---|
| Template layering and reuse | Three layers, build vs generation, MSBuild notes | template-layering-and-reuse.md |
| Template architecture specification | Formal template system architecture | template-architecture-specification.md |
| Template overlays specification | Generation-time overlays, stacking | template-overlays-specification.md |
| Template metadata composition | template.json composition, extend files, anti-patterns |
template-metadata-composition.md |
| BaseTemplate DI extensibility | Hook-based DI extension | base-template-di-extensibility.md |
| Templates dependencies | Catalog and relationships between templates | templates-dependencies.md |
| Template architecture overview (Company) | Business-oriented overview | Template architecture overview (Company) |
| Template layering guide (Company) | Technical layering, submodule narrative | Template layering guide (Company) |
| Extensibility guide (Company) | Extension patterns | Extensibility guide (Company) |
| Template overlays (Company) | Overlays narrative | Template overlays (Company) |
| ADR-0002 (Company) | Why submodules and three layers | ADR-0002 (Company) |
Related: ConnectSoft.Extensions catalog, Templates overview.
Submodule workflow¶
- Submodule path:
base-template/→ remote ConnectSoft.BaseTemplate (often../ConnectSoft.BaseTemplatein.gitmodulesfor local clones). - After clone:
git submodule update --init --recursive - Pinned commit: the parent records a specific BaseTemplate commit; detached HEAD in
base-template/is normal. - Bump: change BaseTemplate on
master(or your branch), then in each Layer 3 repo update the submodule pointer and commit.
Never commit product fixes only inside base-template/ from a Layer 3 repo—change ConnectSoft.BaseTemplate and move the pointer.
MSBuild and Central Package Management¶
Goals: keep Central Package Management (PackageVersion definitions) in ConnectSoft.BaseTemplate with conditional item groups (UseOrleans, UseNServiceBus, UseMassTransit, Migrations / UseNHibernate, UseMCP, MessagingModelTypeNone, and other feature flags). Layer 3 “minimal” hosts turn off stacks they do not ship so MSBuild does not require those outputs.
Invariant (Option A — CPM alignment): In ConnectSoft.BaseTemplate, every active PackageReference in a satellite *.csproj must use Condition (or live in a conditioned ItemGroup) that matches the PackageVersion conditions in Directory.Packages.props. If a package’s PackageVersion is omitted when a feature flag is off, the PackageReference must also be omitted for that graph. Fixing NU1010 belongs in BaseTemplate (conditional references), not in a Layer 3 PackageVersion shim file.
Legacy / escape hatch: Some repos imported build/CentralPackageVersions.MinimalHost.props via ConnectSoftCentralPackageVersionOverrides or the Directory.Packages.props fallback next to the submodule. That pattern is not the standard Layer 3 approach once BaseTemplate exposes Option A satellite conditions; prefer dropping those files after the base-template/ submodule includes the fix. Directory.Packages.props in ConnectSoft.BaseTemplate may still define optional ConnectSoftCentralPackageVersionOverrides import targets for third-party hosts that cannot bump the submodule promptly.
Projects under the base-template/ submodule path must still see ExtendedHost (below) so satellite evaluations get Use* flags true where full stack package lines apply; the host path uses the strict minimal fragment instead.
Extended host satellite defaults (ExtendedHost.BaseTemplateSatelliteDefaults.props):
- Lives in ConnectSoft.BaseTemplate at
build/ExtendedHost.BaseTemplateSatelliteDefaults.props. - Import it only for evaluations where
$(MSBuildProjectDirectory)containsbase-template(convention: repo-rootbase-template/src/...). It sets the usualUse*flags totrueso CPM lines match BaseTemplate project references. - Layer 3 application projects do not import this file on the host path; they use a strict minimal fragment instead (repo-specific
*Host.propsor*Minimal.props).
Dual conditional Import (entry file, e.g. build/DisableMicrosoftExtensionsStackForMinimalHost.props):
- Do not wrap
<Import>in<Choose>/<When>— MSBuild reports MSB4067 (Choosecannot containImport). - Use two sibling top-level imports with mutually exclusive conditions, for example (see Identity’s props file for the exact
Conditionexpressions usingMSBuildProjectDirectoryandContains('base-template')):- Satellite path → import
../base-template/build/ExtendedHost.BaseTemplateSatelliteDefaults.props. - Host path → import the repo’s minimal fragment (e.g.
DisableMicrosoftExtensionsStackForMinimalHost.IdentityHost.props).
- Satellite path → import
- Reference: ConnectSoft.IdentityTemplate
build/DisableMicrosoftExtensionsStackForMinimalHost.props; same mechanical pattern in ApiGateway, Worker, Authorization Server, Health Checks Aggregator, etc.
Optional stacks: MSBuild-first and #if (intentional mix)¶
Layer 2 and Layer 3 use both mechanisms on purpose:
| Mechanism | Best for | Why |
|---|---|---|
MSBuild-first — conditional PackageReference / ProjectReference, and Compile Remove in ConnectSoft.BaseTemplate/Directory.Build.targets for optional projects or whole .cs files |
Extended templates (minimal hosts, satellites still in the solution graph, CPM) | Turning a stack off is a property (UseMassTransit, UseMCP, Serilog, …). No forked source in Layer 3; optional assemblies stay listed in BaseTemplate.slnx but compile as stubs or drop file-level entry points. This is the main lever for Layer 3 extensibility. |
C# #if + DefineConstants |
Base template as shipped (dotnet new, AuthoringMode), JSON/config fragments, and large registration types (HealthChecksExtensions, MicroserviceRegistrationBase, options binding) |
Template generation and consumer choice still rely on symbols in Directory.Build.props and overlays. Inside one file, many orthogonal toggles are clearer with #if than with dozens of one-line partials. |
Rules of thumb:
- Prefer MSBuild-first when an optional stack maps to entire files (e.g.
*Extensions.csper bus/actor/MCP) or entire projects (Flow models, AIModel folders, Hangfire/Mongo persistence satellites). - Keep
#iffor config, generated template conditionals, and cross-cutting blocks where splitting would hurt readability. - Do not let
$(Use*)/$(Serilog)and#ifdisagree: after host props run, ConnectSoft.BaseTemplate may strip matching tokens fromDefineConstantswhen a feature is off so C##ifstays aligned with package graphs (see tail ofbase-template/Directory.Build.propsandDirectory.Build.targets).
Representative MSBuild-first coverage in BaseTemplate (see base-template/Directory.Build.targets and conditional .csproj refs for the authoritative list):
- ApplicationModel — whole optional
*Extensions.csfiles (messaging, actors, Azure Functions, SignalR, CoreWCF, MCP, Microsoft.Extensions.AI / embeddings / ingestion, Mongo, HangFire, Log4Net, Serilog, Redis/in-memory cache, gRPC hosting, NHibernate + FluentMigrator, Audit.Net, OpenTelemetry SDK wiring when bothOpenTelemetryandUseOtelCollectorare off). - Satellites — Flow (MassTransit / NServiceBus sagas), ActorModel.Orleans, AgentFramework samples, ModelContextProtocol, AIModel (embeddings / ingestion / tools), SchedulerModel.Hangfire, PersistenceModel.MongoDb, PersistenceModel.NHibernate, DatabaseModel.Migrations (NHibernate SQL migrations), DatabaseModel.MongoDb.Migrations (single skeleton file).
Directory.Build.props(after imports) — stripsDefineConstantstokens to match$(Use*)/$(Serilog)/$(Migrations)/ cache / gRPC hosting, including NHibernate-related symbols (dialects, second-level cache, MassTransit NHibernate persistence),UseAuditNet,OpenTelemetry,UseOtelCollector, when the corresponding MSBuild properties are nottrue.
Layer 3 authors: extend minimal hosts by setting MSBuild properties and (when needed) adjusting DefineConstants in the same props files—do not patch base-template/src for optional-stack wiring; fix ConnectSoft.BaseTemplate and bump the submodule.
Wiring:
ConnectSoft.TemplateRepositoryDirectory.Build.props— setsConnectSoftBaseTemplateDirectoryPostImportto the entry slim-host file (the one with the dualImport), e.g.build/DisableMicrosoftExtensionsStackForMinimalHost.props.- Root
Directory.Build.props— imports the file above, then$(MSBuildThisFileDirectory)base-template/Directory.Build.props. - Root
Directory.Packages.props— importsbase-template/Directory.Packages.props; add only Layer 3PackageVersionrows (and conditional duplicates such as SerilogAnalyzer when the Layer 3Directory.Build.propsadds the analyzer but BaseTemplate versions it only underSerilog).
Variants:
- Worker: worker-specific entry + minimal fragment (HangFire and other product flags stay on where needed)—see Template layering and reuse and ConnectSoft.WorkerTemplate
build/. - Health Checks Aggregator: minimal host turns off Orleans, NHibernate/Migrations, and HangFire where required for that product; satellite paths still use ExtendedHost for CPM.
- MicroserviceTemplate / Microsoft Bot Framework template: default remains full BaseTemplate stack; opt-in by adopting the same
build/layout andConnectSoftBaseTemplateDirectoryPostImportas Identity (reconcile samples and CPM first).
Troubleshooting:
- NU1010 — First verify whether an unconditional
PackageReferencein ConnectSoft.BaseTemplate targets a package whosePackageVersionis only declared under aConditionthat is false for your host (e.g. minimal gateway withUseNServiceBus=false). Fix in BaseTemplate by conditioning the reference (Option A). ExtendedHost fixes the different case: satellite projects underbase-template/not seeingUse*flagstruewhen they should (no matchingPackageVersionbecause flags were off during evaluation). - MSB4067 — move
Importelements out ofChoose/When; use two conditional imports at the root of the props file.
Solution: include the subset of base-template/src/ConnectSoft.BaseTemplate.* projects you compile, plus all Layer 3 projects; scope the root .slnx variable in CI so you do not accidentally build base-template’s own solution.
DI and host extension¶
Layer 3 ApplicationModel typically project-references base-template/src/ConnectSoft.BaseTemplate.ApplicationModel/... and overrides registration hooks. Do not fork BaseTemplate pipelines wholesale—use hooks: BaseTemplate DI extensibility.
Docker¶
- Context: repository root (not only
*Application/). - COPY order (baseline):
base-template/Directory.*.props, rootDirectory.*.props,ConnectSoft.TemplateRepositoryDirectory.Build.props, slimbuild/*.propsincluding the repo-specific minimal fragment andbase-template/build/ExtendedHost.BaseTemplateSatelliteDefaults.props,nuget.config, project graph, thenbase-template/src/beforedotnet restorewhen ApplicationModel references base projects. - Avoid copying root
Directory.Build.propsinto the Application project directory; that breaksMSBuildThisFileDirectoryimports and CPM (NU1010). - Private feeds: optional
NUGET_AUTH_TOKEN+VSS_NUGET_EXTERNAL_FEED_ENDPOINTSin Linux builds; endpoint URLs must matchnuget.config.
Azure Container Registry and dockerRegistryServiceConnection¶
Layer 3 application pipelines that push images (via build/build-and-push-microservice-docker-steps.yaml or equivalent) should use the same Azure DevOps Docker Registry service connection as ConnectSoft.BaseTemplate and ConnectSoft.IdentityTemplate, unless you intentionally publish to another registry.
- Set pipeline variable
dockerRegistryServiceConnectionto the same service connection id as BaseTemplate’sazure-pipelines.yml(in the reference repos this is the shared connection used forconnectsofttestregistry.azurecr.io). - Keep
imageRepository/containerRegistryconsistent with org conventions so produced images land next to BaseTemplate and other Layer 3 services. - If
docker pushfails with invalid client/secret, fix the service connection in Azure DevOps (Project Settings → Service connections), not the submodule; only change the guid in YAML if you replace the connection with a new one.
CI/CD — application repository (azure-pipelines.yml)¶
checkout: selfwithsubmodules: recursiveon jobs that restore/build or Docker-build.- Lint/build templates: often
isRemoveDockerComposeEnabled: trueand strip.dcprojthat break Linux restore (match Identity/BaseTemplate). - Docker publish job:
build-and-push-microservice-docker-steps.yaml;dockerFilepath from repo root; passdockerRegistryServiceConnectionaligned with BaseTemplate/Identity (see Azure Container Registry and dockerRegistryServiceConnection above). - Solution variable: point at the Layer 3
.slnxonly (avoid**/*.slnxpicking upbase-template).
Code coverage (Coverlet) and excluding base-template assemblies¶
Application pipelines run dotnet test with --collect:"XPlat Code Coverage" (Coverlet) and --settings pointing at the repo-root product runsettings file (see each repo’s azure-pipelines.yml — commonly ConnectSoft.<Product>.Docker.runsettings on CI, alongside non-Docker ConnectSoft.<Product>.runsettings for local runs).
Extended templates load base-template/ as a submodule; ConnectSoft.BaseTemplate* assemblies must not contribute to Layer 3 coverage thresholds. In the DataCollector friendlyName="XPlat Code Coverage" block (placed before the legacy Microsoft Code Coverage collector in the same file):
Include—[ConnectSoft.<ProductPrefix>*]*so only Layer 3 product assemblies are measured (use your assembly name root, e.g.IdentityTemplate,Saas.BillingTemplate).Exclude—[ConnectSoft.BaseTemplate*]*, test assemblies ([*.UnitTests]*,[*.ArchitectureTests]*,[*.AcceptanceTests]*), andExcludeByAttributefor generated/obsolete code as in the reference. Templates that already omitInfrastructureModelfrom MicrosoftModulePathsshould add the matching[ConnectSoft.<ProductPrefix>.InfrastructureModel]*entry to CoverletExclude.
Reference: ConnectSoft.WorkerTemplate — root ConnectSoft.WorkerTemplate.Docker.runsettings / ConnectSoft.WorkerTemplate.runsettings (canonical Include / Exclude snippet).
CI/CD — template installer (azure-pipelines-template.yml)¶
Used to publish the template NuGet (installer), not the running microservice image.
checkout: selfwithsubmodules: recursive.build/prepare-<extender>-template-pack.ps1— copies a staging tree, strips AuthoringMode-only blocks from stagedbase-template/Directory.Build.props, injects the consumer fragment viabase-template/build/New-DirectoryBuildConsumerFragment.ps1(script lives in the submodule; the Layer 3 repo calls it, does not edit submodule sources).nuget pack ... -BasePath <staging>— pack from the staging directory.- Extract .nupkg → run
build/template-compose-<extender>.ps1: start frombase-template/.template.config/template.json, apply extend file overrides (identity,groupIdentity,name,shortName,description,tags,classifications, and when neededsourceName,defaultName,preferNameDirectory,guids,primaryOutputs), inject"source": "base-template/"on the firstsourcesentry, append a secondsourcesentry with"source": "./"and Layer 3–specific modifiers (and packaging excludes sobase-template/is not copied twice). Merge ide.host.json / dotnetcli.host.json name/description blocks from the Layer 3.template.config. - Repack the composed package; push the composed
.nupkg(not the raw pre-compose package). - CI gate:
dotnet new install <composed.nupkg>,dotnet new <shortName>with a fixed flag set aligned to slim host / product defaults, thendotnet buildthe generated solution.
Reference implementations: ConnectSoft.IdentityTemplate (template-compose.ps1, prepare-identity-template-pack.ps1); ConnectSoft.ApiGatewayTemplate (template-compose-apigateway.ps1, prepare-apigateway-template-pack.ps1, template/apigateway.template.extend.json).
Details: Template metadata composition.
Extended installer: one registerable template per .nupkg¶
Composed Layer 3 installers (Identity, Worker, ApiGateway, Authorization Server, Microsoft Bot Framework, …) merge base template.json / host files with the extend file, then repack a single consumer-facing package. To avoid registering two templates from one package (the base short name connectsoft-base plus the extended short name), the pipeline removes base-template/.template.config from the staged tree after composition and before the final archive. The ConnectSoft.BaseTemplate.Installer package remains the supported way to install and scaffold connectsoft-base alone.
dotnet new CLI caveat: HealthCheckPublisher and allowMultipleValues¶
The base template defines HealthCheckPublisher as a multi-value choice. When invoking dotnet new with --healthcheck-publisher, pass it before --no-update-check and after other template options so the CLI does not treat the next flag (for example --authentication) as another publisher value. See base template.json and extended template CI gates for the canonical argument order.
Template short names (connectsoft-*)¶
Use the connectsoft-* short names consistently: connectsoft-base (BaseTemplate), connectsoft-microservice (MicroserviceTemplate), and connectsoft-<product> for each extended template (for example connectsoft-apigateway, connectsoft-identity). Reinstall template packages and update scripts after renames; older cs-* names are breaking and removed.
Generation-time: overlays and metadata (where to read)¶
| Need | Read first |
|---|---|
| Overlay operations and stacking | Template overlays specification |
Composing template.json / extend files |
Template metadata composition |
| Company narrative on overlays | Template overlays (Company) |
This playbook focuses on repo + installer alignment; generation-time recipes build on those specs.
Reference implementations¶
| Repository | Role |
|---|---|
| ConnectSoft.BaseTemplate | Layer 2 kernel; submodule source of truth |
| ConnectSoft.IdentityTemplate | Primary Layer 3 example: submodule, MSBuild, slim host, Docker, application CI, template staging + compose + dotnet new gate |
| ConnectSoft.WorkerTemplate | Worker host; HangFire-friendly slim props; same operational pattern with domain deltas |
| ConnectSoft.ApiGatewayTemplate | API gateway stack; template compose with second sources entry and apigateway.template.extend.json |
| ConnectSoft.AuthorizationServerTemplate | OAuth/authorization host; same MSBuild/Docker pattern as Identity (dual Import, ExtendedHost, Dockerfile root context) |
| ConnectSoft.HealthChecksAggregatorTemplate | Health aggregation host; minimal host with Orleans/NHibernate off; same satellite ExtendedHost pattern |
| ConnectSoft.Saas.TenantsTemplate | SaaS reference (Tenant aggregate): MassTransit outbox, Orleans on, REST + gRPC, NHibernate multi-dialect, full observability stack. See SaaS Template Baseline Checklist. |
| ConnectSoft.Saas.ProductsCatalogTemplate | SaaS Product aggregate (Edition as in-aggregate entity — ADR 0002) |
| ConnectSoft.Saas.EntitlementsTemplate | SaaS Entitlement aggregate (effective entitlements per tenant) |
| ConnectSoft.Saas.BillingTemplate | SaaS Subscription aggregate (invoices as read-model projection this wave) |
| ConnectSoft.Saas.MeteringTemplate | SaaS UsageMeter aggregate (Orleans grain key {tenantId, dimension}) |
Master checklist (new or aligned Layer 3 repo)¶
Submodule and repo
-
.gitmodules→base-template/→ ConnectSoft.BaseTemplate - Clean clone builds after
git submodule update --init --recursive
MSBuild / CPM
- Root
Directory.Build.props/Directory.Packages.propsimport chain matches Identity (or documented Worker/ApiGateway variant) -
ConnectSoft.TemplateRepositoryDirectory.Build.propspoints at the entrybuild/DisableMicrosoftExtensionsStack*.props(dual conditionalImport, notChoose/When) - Repo-specific minimal fragment (
*Host.props/*Minimal.props) +base-template/build/ExtendedHost.BaseTemplateSatelliteDefaults.propson satellite paths - Layer 3–only package versions documented (e.g. Roslyn, SerilogAnalyzer when applicable)
DI / projects
- ApplicationModel references
base-template/.../ConnectSoft.BaseTemplate.ApplicationModelwhere required; hooks used per base-template-di-extensibility.md
Docker
-
docker build -f .../Dockerfile .from repo root succeeds;base-template/srcand slim props present in Dockerfile
Application CI
- Recursive submodules on build/test/Docker jobs; solution path excludes nested base
.slnx -
dockerRegistryServiceConnection(and relatedimageRepository/containerRegistryvariables) aligned with ConnectSoft.BaseTemplate / ConnectSoft.IdentityTemplate for the same ACR/org policy
Template installer
-
prepare-*-template-pack.ps1+nuget pack -BasePath -
template/*.template.extend.json+template-compose-*.ps1; composed.nupkgis what you push -
dotnet new <shortName>+dotnet buildgate inazure-pipelines-template.yml
Docs / onboarding
- Repo docs state submodule init, Docker context, and pointer to this playbook
See also¶
- Template naming guide —
connectsoft-*short names, identities, migration from legacy names - Template layering and reuse
- Template architecture specification
- Templates overview