Skip to content

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 1ConnectSoft.Extensions.* (and related) NuGet packages. Layer 2ConnectSoft.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
Hold "Alt" / "Option" to enable pan & zoom

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
Hold "Alt" / "Option" to enable pan & zoom

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

  1. Submodule path: base-template/ → remote ConnectSoft.BaseTemplate (often ../ConnectSoft.BaseTemplate in .gitmodules for local clones).
  2. After clone: git submodule update --init --recursive
  3. Pinned commit: the parent records a specific BaseTemplate commit; detached HEAD in base-template/ is normal.
  4. 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) contains base-template (convention: repo-root base-template/src/...). It sets the usual Use* flags to true so 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.props or *Minimal.props).

Dual conditional Import (entry file, e.g. build/DisableMicrosoftExtensionsStackForMinimalHost.props):

  • Do not wrap <Import> in <Choose> / <When> — MSBuild reports MSB4067 (Choose cannot contain Import).
  • Use two sibling top-level imports with mutually exclusive conditions, for example (see Identity’s props file for the exact Condition expressions using MSBuildProjectDirectory and Contains('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:

  1. ConnectSoft.TemplateRepositoryDirectory.Build.props — sets ConnectSoftBaseTemplateDirectoryPostImport to the entry slim-host file (the one with the dual Import), e.g. build/DisableMicrosoftExtensionsStackForMinimalHost.props.
  2. Root Directory.Build.props — imports the file above, then $(MSBuildThisFileDirectory)base-template/Directory.Build.props.
  3. Root Directory.Packages.props — imports base-template/Directory.Packages.props; add only Layer 3 PackageVersion rows (and conditional duplicates such as SerilogAnalyzer when the Layer 3 Directory.Build.props adds the analyzer but BaseTemplate versions it only under Serilog).

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 and ConnectSoftBaseTemplateDirectoryPostImport as Identity (reconcile samples and CPM first).

Troubleshooting:

  • NU1010 — a PackageReference is active but the matching PackageVersion is behind a condition that is false for satellite projects: ensure ExtendedHost is imported on the base-template path.
  • MSB4067 — move Import elements out of Choose/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, root Directory.*.props, ConnectSoft.TemplateRepositoryDirectory.Build.props, slim build/*.props including the repo-specific minimal fragment and base-template/build/ExtendedHost.BaseTemplateSatelliteDefaults.props, nuget.config, project graph, then base-template/src/ before dotnet restore when ApplicationModel references base projects.
  • Avoid copying root Directory.Build.props into the Application project directory; that breaks MSBuildThisFileDirectory imports and CPM (NU1010).
  • Private feeds: optional NUGET_AUTH_TOKEN + VSS_NUGET_EXTERNAL_FEED_ENDPOINTS in Linux builds; endpoint URLs must match nuget.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 dockerRegistryServiceConnection to the same service connection id as BaseTemplate’s azure-pipelines.yml (in the reference repos this is the shared connection used for connectsofttestregistry.azurecr.io).
  • Keep imageRepository / containerRegistry consistent with org conventions so produced images land next to BaseTemplate and other Layer 3 services.
  • If docker push fails 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: self with submodules: recursive on jobs that restore/build or Docker-build.
  • Lint/build templates: often isRemoveDockerComposeEnabled: true and strip .dcproj that break Linux restore (match Identity/BaseTemplate).
  • Docker publish job: build-and-push-microservice-docker-steps.yaml; dockerFile path from repo root; pass dockerRegistryServiceConnection aligned with BaseTemplate/Identity (see Azure Container Registry and dockerRegistryServiceConnection above).
  • Solution variable: point at the Layer 3 .slnx only (avoid **/*.slnx picking up base-template).

CI/CD — template installer (azure-pipelines-template.yml)

Used to publish the template NuGet (installer), not the running microservice image.

  1. checkout: self with submodules: recursive.
  2. build/prepare-<extender>-template-pack.ps1 — copies a staging tree, strips AuthoringMode-only blocks from staged base-template/Directory.Build.props, injects the consumer fragment via base-template/build/New-DirectoryBuildConsumerFragment.ps1 (script lives in the submodule; the Layer 3 repo calls it, does not edit submodule sources).
  3. nuget pack ... -BasePath <staging> — pack from the staging directory.
  4. Extract .nupkg → run build/template-compose-<extender>.ps1: start from base-template/.template.config/template.json, apply extend file overrides (identity, groupIdentity, name, shortName, description, tags, classifications, and when needed sourceName, defaultName, preferNameDirectory, guids, primaryOutputs), inject "source": "base-template/" on the first sources entry, append a second sources entry with "source": "./" and Layer 3–specific modifiers (and packaging excludes so base-template/ is not copied twice). Merge ide.host.json / dotnetcli.host.json name/description blocks from the Layer 3 .template.config.
  5. Repack the composed package; push the composed .nupkg (not the raw pre-compose package).
  6. CI gate: dotnet new install <composed.nupkg>, dotnet new <shortName> with a fixed flag set aligned to slim host / product defaults, then dotnet build the 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

  • .gitmodulesbase-template/ → ConnectSoft.BaseTemplate
  • Clean clone builds after git submodule update --init --recursive

MSBuild / CPM

  • Root Directory.Build.props / Directory.Packages.props import chain matches Identity (or documented Worker/ApiGateway variant)
  • ConnectSoft.TemplateRepositoryDirectory.Build.props points at the entry build/DisableMicrosoftExtensionsStack*.props (dual conditional Import, not Choose/When)
  • Repo-specific minimal fragment (*Host.props / *Minimal.props) + base-template/build/ExtendedHost.BaseTemplateSatelliteDefaults.props on satellite paths
  • Layer 3–only package versions documented (e.g. Roslyn, SerilogAnalyzer when applicable)

DI / projects

Docker

  • docker build -f .../Dockerfile . from repo root succeeds; base-template/src and slim props present in Dockerfile

Application CI

  • Recursive submodules on build/test/Docker jobs; solution path excludes nested base .slnx
  • dockerRegistryServiceConnection (and related imageRepository / containerRegistry variables) 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 .nupkg is what you push
  • dotnet new <shortName> + dotnet build gate in azure-pipelines-template.yml

Docs / onboarding

  • Repo docs state submodule init, Docker context, and pointer to this playbook

See also