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, metadata composition, and documentation 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[base-template build Prepare-ExtendedTemplatePack.ps1]
    Pack[nuget pack BasePath]
    Compose[base-template build Invoke-TemplateCompose.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
Template documentation policy Layered documentation ownership, manifests, generated solution docs template-documentation-policy.md
Documentation source of truth Canonical coverage, ownership, validation, governance ../internal/documentation-source-of-truth.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.

Documentation alignment

Layer 3 repositories must align documentation with the same boundaries as code. Shared concepts live in ConnectSoft.Documentation. BaseTemplate keeps Layer 2 implementation references. A Layer 3 repo documents only overlay-specific behavior and the generated-solution runbook.

  1. Add or update docs-manifest.yml in the Layer 3 repo.
  2. Declare owner, scope, inherited BaseTemplate topic ids, referenced canonical doc ids, enabled features, and generated docs behavior.
  3. Keep local docs focused on product deltas: domain projects, configuration changes, enabled/disabled features, runbook, tests, and submodule update workflow.
  4. Link to canonical pages for cross-cutting concerns such as service discovery, resiliency, observability, localization, testing, security, messaging, persistence, AI/MCP, and DevOps.
  5. Link to BaseTemplate Layer 2 implementation docs when generated solutions need exact registration, MSBuild, options, package, or troubleshooting details.
  6. Backfill missing or partial canonical topics in ConnectSoft.Documentation before shrinking local template docs into overlay pages.

Recommended checks from ConnectSoft.Documentation:

powershell -ExecutionPolicy Bypass -File .\scripts\connectsoft-docs.ps1 validate -Repos C:\Git\ConnectSoft\ConnectSoft.Documentation,C:\Git\ConnectSoft\ConnectSoft.BaseTemplate,C:\Git\ConnectSoft\ConnectSoft.IdentityTemplate
powershell -ExecutionPolicy Bypass -File .\scripts\connectsoft-docs.ps1 check-canonical-coverage -Repos C:\Git\ConnectSoft\ConnectSoft.BaseTemplate,C:\Git\ConnectSoft\ConnectSoft.IdentityTemplate

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) 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.

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:

  1. Prefer MSBuild-first when an optional stack maps to entire files (e.g. *Extensions.cs per bus/actor/MCP) or entire projects (Flow models, AIModel folders, Hangfire/Mongo persistence satellites).
  2. Keep #if for config, generated template conditionals, and cross-cutting blocks where splitting would hurt readability.
  3. Do not let $(Use*) / $(Serilog) and #if disagree: after host props run, ConnectSoft.BaseTemplate may strip matching tokens from DefineConstants when a feature is off so C# #if stays aligned with package graphs (see tail of base-template/Directory.Build.props and Directory.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.cs files (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 both OpenTelemetry and UseOtelCollector are 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) — strips DefineConstants tokens 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 not true.

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:

  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 — First verify whether an unconditional PackageReference in ConnectSoft.BaseTemplate targets a package whose PackageVersion is only declared under a Condition that is false for your host (e.g. minimal gateway with UseNServiceBus=false). Fix in BaseTemplate by conditioning the reference (Option A). ExtendedHost fixes the different case: satellite projects under base-template/ not seeing Use* flags true when they should (no matching PackageVersion because flags were off during evaluation).
  • 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: pass NUGET_AUTH_TOKEN ($(System.AccessToken)) via Docker --build-arg; install Microsoft.Artifacts.CredentialProvider.NuGet.Tool in the SDK stage and set ARTIFACTS_CREDENTIALPROVIDER_ACCESSTOKEN for dotnet restore / publish (see ProductsCatalog / Identity Application Dockerfiles). Enable Allow scripts to access the OAuth token on the pipeline.

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).

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]*), and ExcludeByAttribute for generated/obsolete code as in the reference. Templates that already omit InfrastructureModel from Microsoft ModulePaths should add the matching [ConnectSoft.<ProductPrefix>.InfrastructureModel]* entry to Coverlet Exclude.

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.

Centralized scripts (no Layer 3 shims). The staging and composition logic now lives in the base-template submodule under base-template/build/. Layer 3 repos call these scripts directly from azure-pipelines-template.yml; they no longer keep their own build/prepare-*-template-pack.ps1 or build/template-compose*.ps1. Per-repo differences are expressed as parameters (-PackProfile, -ComposeProfile) plus the template/*.template.extend.json data file. Run git submodule update --init --recursive so the scripts are present locally.

  1. checkout: self with submodules: recursive.
  2. base-template/build/Prepare-ExtendedTemplatePack.ps1 -RepoRoot <repo> -StagingDir <staging> -PackProfile <Simple|Layer3Full> — 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. Layer3Full (Microservice/Agent) additionally writes ConnectSoft.TemplateConsumerSymbols.props, patches base-template/Directory.Packages.props, and applies persistence-none patches.
  3. nuget pack ... -BasePath <staging> — pack from the staging directory.
  4. Extract .nupkg → run base-template/build/Invoke-TemplateCompose.ps1 -BaseTemplateRoot <extracted>/base-template -ExtenderTemplateRoot <extracted> -OutputTemplateConfigDir <composed>/.template.config -ComposeProfile <Simple|Identity|Microservice|ApiGateway>: 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 (both artifact kinds, as of 2026-07-17): dotnet new install <composed.nupkg> once, then two generate-and-build steps sharing that install: dotnet new <shortName> --artifact-kind solution with the repo's fixed flag set, build, validate; and dotnet new <shortName> --artifact-kind template with the same flags, build, validate. Each step runs base-template/build/Validate-TemplateArtifactModel.ps1 -Mode GeneratedSolution|GeneratedTemplate against the actual resolved solution root — not a guessed $genOut\$instanceName\ path. preferNameDirectory/sourceName substitution can make generated content land flat at the output root rather than nested (confirmed for Identity and HealthChecksAggregator); validating the wrong directory silently "passes" without checking real content. Reuse each repo's own $buildTarget resolution fallback chain (nested → flat → recursive search) and validate Split-Path -Parent $buildTarget, never a guessed root. Two concrete pitfalls worth knowing before writing a new CI gate: each Azure Pipelines pwsh step is a separate process, so $sourcesDir (or any variable from an earlier step) must be re-declared at the top of every step that needs it; and a validator call against the wrong directory returns a false "OK" (the wrong, near-empty folder simply has none of the forbidden paths either) rather than an error, so always verify by inspecting the generated directory directly at least once when wiring a new gate.

Compose profiles (-ComposeProfile)

Profile When Behavior
Simple / Identity SaaS family, Identity, Worker, Authorization, Microsoft Bot, ApiGateway non-gateway repos Apply top-level identityOverrides; rewrite first sources entry to base-template/; disable Dapper symbols. Simple and Identity share one implementation.
Microservice Microservice, Agent Simple plus merge of the Layer-3 partial .template.config/template.json (sources, symbols, postActions) and symbolPatches from the extend file.
ApiGateway ApiGateway apigatewayOverrides, guids/primaryOutputs/preferNameDirectory, gateway-only-symbols.json, and layer3PackagingRootExclude/layer3SourceModifiers.

Reference implementations: ConnectSoft.IdentityTemplate (-ComposeProfile Identity, template/identity.template.extend.json); ConnectSoft.ApiGatewayTemplate (-ComposeProfile ApiGateway, template/apigateway.template.extend.json); ConnectSoft.MicroserviceTemplate (-PackProfile Layer3Full -ComposeProfile Microservice).

Anti-pattern: Do not reintroduce repo-root build/prepare-*-template-pack.ps1 or build/template-compose*.ps1 scripts. Extend behavior through template/*.template.extend.json and the shared compose profiles. New profiles belong in base-template/build/Invoke-TemplateCompose.ps1, not per-repo forks. (ConnectSoft.AI.SoftwareFactory.AgentTemplate keeps a domain-specific prepare-agent-template-pack.ps1 that performs Agent prefix renaming, but it delegates composition to the submodule's Invoke-TemplateCompose.ps1.) ConnectSoft/tools/Validate-Layer3Alignment.ps1 enforces this layout.

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.

Authoring-ready template-kind output: the _template.config mechanism

dotnet new never copies a template's own .template.config/ into generated output — that folder name is reserved by the templating engine. So --artifact-kind template output (meant to be re-installable and further extensible) needs an explicit workaround to actually ship .template.config/. Invoke-TemplateCompose.ps1 stages a sibling copy of the composed .template.config/ named _template.config/ (an unreserved name); the composed sources[] then copyOnlys and renames _template.config → .template.config for IsTemplateArtifact output, while excluding _template.config/** entirely for IsMaterializedSolution (so it never leaks into a clean solution). CI pipelines that recompose inside an already-extracted .nupkg must copy this _template.config sibling into the extraction root themselves, alongside the existing template.json/ide.host.json/dotnetcli.host.json copies — the compose script writes it next to wherever -OutputTemplateConfigDir points, which for that pattern is a temp working directory, not the extraction root. Full field-level detail: Template metadata composition.

Regenerating materialization snapshots on a submodule bump

Every solution-supporting Layer 3 repo carries two committed snapshot files that let it build without the base-template/ submodule present (a materialized solution): ConnectSoft.TemplateConsumerSymbols.props (repo root) and build/MaterializedDirectory.Packages.props. Both must be regenerated whenever the base-template submodule pointer bumps, using base-template/build/New-DirectoryBuildConsumerFragment.ps1 against the newly-pinned submodule content.

  1. Bump the submodule pointer in the Layer 3 repo as usual.
  2. Regenerate both snapshots from the new submodule content.
  3. Before committing, check whether the repo has an intentional, hand-applied override in either snapshot file. ConnectSoft.AuthorizationServerTemplate's ConnectSoft.TemplateConsumerSymbols.props forces UseRestApi on regardless of the composed symbol default, because its OAuth2/OIDC endpoints are MVC controllers and a Grpc-only host would leave AddControllers/ApiExplorer unregistered (a SwaggerGenerator DI failure). A blind regeneration silently reverts that override. Search the snapshot file for an inline comment before overwriting it, and re-apply any override the comment describes.
  4. Verify the repo still builds both artifact kinds (--artifact-kind solution with base-template/ deleted, and --artifact-kind template with it present) after regenerating.

Declaring an artifact kind as deferred

If a Layer 3 repo genuinely cannot support one of the two artifact kinds yet (for example, a repo whose generated output still compiles against the base-template submodule through unresolved project references, so a clean solution-kind output isn't achievable without further work), declare that honestly rather than leaving the unsupported kind silently broken or half-working:

  • In template/connectsoft.template.json, list only the kinds the repo genuinely supports in supportsArtifactKinds, and add a deferredArtifactKinds array naming the rest with a short reason.
  • Do not wire a CI gate for a deferred kind — a gate that's expected to fail (or worse, one that's skipped silently) is worse than no gate; it either blocks unrelated changes or erodes trust in the CI signal.
  • Validate-TemplateArtifactModel.ps1 -Mode SourceTemplateRepo cross-checks supportsArtifactKinds against the composed ArtifactKind symbol's default and choices — keep the manifest and the template.json symbol in agreement.
  • ConnectSoft.AI.SoftwareFactory.AgentTemplate documents this exact situation in the Template Artifact Model "In Scope" section — use it as the reference example when a repo needs to defer a kind.

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
Documentation composition and ownership Template documentation policy
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

  • .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 for every artifact kind the repo's manifest declares in supportsArtifactKinds (not just one)
  • Each gate step calls Validate-TemplateArtifactModel.ps1 -Mode GeneratedSolution|GeneratedTemplate against the actual resolved solution root (verify by inspecting the generated directory once, not just trusting a green exit code)
  • ConnectSoft.TemplateConsumerSymbols.props and build/MaterializedDirectory.Packages.props regenerated against the current submodule pointer, with any repo-specific override re-applied (check for an inline comment before overwriting)
  • Any kind the repo cannot yet support is declared in deferredArtifactKinds, not silently half-wired

Docs / onboarding

  • Repo docs state submodule init, Docker context, and pointer to this playbook
  • docs-manifest.yml exists and declares owner, scope, inherited BaseTemplate topics, canonical docs, enabled features, and generated docs behavior
  • Repo docs describe only Layer 3 deltas and link to central canonical docs for shared concerns
  • Generated solution docs include a curated runbook, feature-docs matrix, selected inherited implementation references, and canonical links

See also