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. Projects under the base-template/ submodule path must still see the matching flags on wherever BaseTemplate .csproj files reference packages behind those conditions—otherwise restore fails with NU1010 (missing PackageVersion for a PackageReference).
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). - Reference: ConnectSoft.IdentityTemplate
build/DisableMicrosoftExtensionsStackForMinimalHost.props; same mechanical pattern in ApiGateway, Worker, Authorization Server, Health Checks Aggregator, etc.
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 — a
PackageReferenceis active but the matchingPackageVersionis behind a condition that is false for satellite projects: ensure ExtendedHost is imported on thebase-templatepath. - 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).
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 |
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