Skip to content

Upgrade order and promotion levels

This page explains how ConnectSoft moves a whole estate to a new framework or package band, for example the .NET 11 wave. It is written for engineers and release owners who run or review an upgrade. The decisions behind the .NET 11 wave are recorded in ADR-0102: Framework support policy for the .NET 11 wave.

Why the order is forced

Promoted libraries declare their dependencies on the packages they were built against. Once a library that requires Microsoft.Extensions.* >= 11.0.0 is promoted to the feed, a consumer that still pins the 10.0.x runtime family fails restore with NU1605 (package downgrade). This holds even when the consumer itself targets net10.0.

The consequence is a strict order:

  • A repository can only be moved after every repository it depends on has been promoted, not just edited or committed.
  • Inside one dependency level the repositories do not depend on each other, so they can be edited, verified and promoted in any order or in batches.
  • Skipping ahead produces restore failures that look like package problems but are really ordering problems.

Dependency levels

flowchart TD
    L0["Extensions level 0 (27 repos)"] --> L1["Extensions level 1 (36 repos)"]
    L1 --> L2["Extensions level 2 (18 repos)"]
    L2 --> L3["Extensions level 3 (7 repos)"]
    L3 --> L4["Extensions level 4 (2 repos)"]
    L4 --> L5["Extensions level 5 (1 repo)"]
    L5 --> OL["Other libraries"]
    OL --> LT["Library templates"]
    LT --> BT["BaseTemplate"]
    BT --> DT["Derived templates (13)"]
    DT --> OT["Other templates"]
    OT --> ES["DerechHaLev estate"]
Hold "Alt" / "Option" to enable pan & zoom
Stage What it contains Why it sits there
Extensions levels 0 to 5 The ConnectSoft.Extensions.* libraries, grouped by their dependency depth (27, 36, 18, 7, 2 and 1 repositories) Level N depends only on levels below N
Other libraries Libraries outside the Extensions family (API clients, shell and UIKit libraries and similar) They consume the Extensions packages
Library templates LibraryTemplate, ApiLibraryTemplate, AISkillsLibraryTemplate They generate libraries that reference promoted packages
BaseTemplate ConnectSoft.BaseTemplate It composes the promoted libraries into the base service
Derived templates The 13 templates that are built on base-template They copy or extend the BaseTemplate output
Other templates Templates that do not derive from BaseTemplate (Blazor, Marketing, MAUI and similar) They depend on promoted libraries and the shared pipeline templates
DerechHaLev estate Nine services, the Blazor shell and the E2E solution They consume everything above

The dependency levels of the Extensions family are computed from the repositories' package references, so recompute them for the next wave instead of reusing the counts above.

The per-level gate

Every level, and every stage after the Extensions levels, passes through the same gate. The gate always ends with a human.

  1. Edit. Apply the framework and package changes for the repositories of the level. Do not touch repositories of other levels.
  2. Verify. Restore, build and run the tests of every repository of the level. Check that no repository of the level still has a restore warning that points to an ordering problem.
  3. Owner commits. The release owner reviews the diffs and commits the repositories. The automation never commits, adds or pushes.
  4. Owner promotes. The release owner promotes the commit so that the packages reach the feed and the next level can restore against them.
  5. Next level. Only after promotion does the next level start. A level that has been edited but not promoted blocks the levels above it.

This is intentionally slow at the boundaries and fast inside a level. The gate is also the point where a package hold or an exception (see the ADR) is decided, because it is cheaper to decide it once at the level where the package first appears.

Estate promotion order

The DerechHaLev internal packages are pinned by exact version. A producer's move to net11.0 therefore does not break a consumer until that consumer bumps the package. After a producer has been promoted, the consumers are bumped to the head of the Release view of the feed.

The estate order is:

  1. HrLite and Employers
  2. Identity
  3. AuthServer
  4. CaseManagement and Shell
  5. Tasks
  6. Compliance and MinistryReporting
  7. Gateway
  8. E2E

Services in the same step are bumped together. Each later step consumes packages produced by the earlier steps, and the E2E solution comes last because it exercises the whole estate.

Lasting traps

The traps below were found during the .NET 11 wave and apply to the next wave as well.

Template engine traps

  • dotnet new removes MSBuild lines whose Condition is a literal 'a' == 'b' comparison. It also removes lines that mix a template symbol with $(TargetFramework). A condition that is only a comparison of two literals is evaluated by the template engine at generation time, not by MSBuild.
  • The fallback case of a switch symbol must be a real condition. A fallback written as "true" is not treated as a condition and the case is dropped.
  • Never put a literal net11.0 in a condition in a template where Framework declares replaces: net11.0. The engine replaces the literal and the condition changes meaning. Use the literal net10.0 in per-TFM conditions (== 'net10.0' and != 'net10.0') so the replacement does not touch it.
  • The composed .template.config/template.json is regenerated by base-template/build/Invoke-TemplateCompose.ps1. Check that the compose step is idempotent before trusting a diff: each run appends another base-template/** exclude pair, so keep the committed diff to the intended framework delta.
  • Templates that copy base-template/** unchanged cannot offer a net10.0 choice next to a net11.0 copy, because mixed target frameworks fail restore (NU1201). Remove the unusable choice at pack time.

Cleanup tools can break template repositories

Tools that remove unused using directives are unsafe in template repositories. The code that needs the namespaces sits inside #if branches that are compiled out of the repository layout, so the tool sees the directive as unused. This broke the ApiGateway template CI after AddMicroserviceObjectMapping lost the using that its generated branch needs. Run cleanup tools only on generated output, or exclude template source.

Parallel target-framework test hosts

Multi-TFM test projects run the net10.0 and net11.0 hosts in parallel (the default since SDK 9). Shared state then collides:

  • Serialize tests that pack or build into shared folders, for example with a named Mutex.
  • Serialize tests that share one database, with <TestTfmsInParallel>false</TestTfmsInParallel> in the test project.
  • Tests that spawn processes need MSBUILDDISABLENODEREUSE=1, DOTNET_CLI_USE_MSBUILD_SERVER=0 and UseSharedCompilation=false, asynchronous output reads and bounded waits.

UseDotNet@2 and SDK availability

UseDotNet@2 leaves only the SDKs that it installed on the agent. A pipeline that installs only 11.x loses the 10.x SDK, which breaks a net10.0 generation or test host.

  • Install 11.x and keep 10.x wherever a net10.0 generation or test host is needed.
  • In the shared ConnectSoft.AzurePipelines template-ci/v1 templates the extra SDK is passed through AdditionalDotNetSdks: ['11.0.x'].
  • For IIS deployment, check the installed ASP.NET Core runtime through requiredAspNetCoreRuntime (for example Microsoft.AspNetCore.App 11.). The matching Hosting Bundle must exist on the server before the first deployment.

Generated documentation snapshots

Template parameter pages under starters/ are snapshots of .template.config/template.json. Regenerate them with tools/template-docs/Update-TemplateDocumentation.ps1 when a symbol, default or choice changes, instead of editing the generated table by hand.