Skip to content

Latest commit

 

History

History
319 lines (259 loc) · 21.9 KB

upgrading.md

File metadata and controls

319 lines (259 loc) · 21.9 KB

Versions of the template

The template is versioned with git tags, by semantic versioning.

Tag What it is
v1.0.0 The template before the breaking changes of 2026.
v2.0.0-preview.1 The breaking changes in progress.
v2.0.0 The breaking changes completed and checked.

From 2.9 dotskit upgrade brings a solution of a 2.x release to the current one, and git keeps the team's changes where they merge with the template's; the rest are conflicts to resolve. First dotskit init --template-version with the release the solution was made by (dotskit init alone names the closest one), then dotskit upgrade, a part a run. Inside one major version that goes from the first release of the major to any later one; --to stops at a release between, for a team that wants to build and test the solution there first (above). A solution of v1 is brought to v2.0.0 by hand first, below. The sections below are the same changes by hand.

What each version brings, from v2.1.0 on, is in version.json and on the releases page; the table lists only the versions those do not explain.

From v2.0.0 on, a change keeps generated solutions working, or comes with a short way to update them. A change that cannot do either is a new major version.

The course of an upgrade

dotnet tool update -g SawKing.DotsKit.Tool
dotskit upgrade

What an upgrade does, from the plain to what a team would otherwise do by hand:

What an upgrade does
One command dotskit upgrade, from where the solution stands to the tool's version of the template
A part a run the shared files, then each project of Common, then a service at a time: the conflicts of one layer at a time, not of the whole distance
Any distance inside a major from the first release of the major to any later one; --to 2.x.y stops at a release between, for a team that wants to build and test there first
An ordinary git merge the template's output is a branch, dotskit/template, and the merge is git merge: the IDE, git mergetool and git merge --abort work as always, and the merge is the team's to commit
Undone by git a run that fails is taken back by git reset --hard to the commit it started from, and the branch goes back where it was
The usings and the namespaces of moved types a type the template moved to another project or namespace is followed into the team's own code: its using is replaced, its full name gets the new namespace, and the project gets the reference - 419 such edits on one real solution, none of them by hand
The references and the versions the references between the template's projects, the package versions of central package management and the projects of All.sln are brought to what the new version has

What is checked. A solution nobody has changed - the template's own output - goes from any release of a major version to any later one with no conflicts and no errors: on a clean solution of 2.0 taken to the current version, straight in 12 runs and with a stop at 2.7.0 in 23, every part merged clean, the solution builds and its tests pass. On a solution a team has been changing for months the template's side merges the same way, and what the team changed in the same lines is a conflict for the team - the tool never guesses there.

The sections below say how each of these works, what it does not do, and what a solution may still have to change by hand.

dotskit upgrade brings a solution to the tool's version of the template, keeping what the team changed. It is the merge of dotskit new: the base is the solution's manifest generated by the solution's version, the target the same manifest generated by the tool's. It needs a manifest - a solution made without dotskit gets one from dotskit init - and a clean working tree. Its first lines name the minor versions it crosses and link upgrading.md on GitHub, of master for a release and of dev for a pre-release: its sections for these versions say what a solution may have to change by hand.

What the merge adds for an upgrade:

  • a file the template moved (git diff -M between the two outputs) takes the team's changes to its new path, and the old path is deleted;
  • a file the template removed is deleted when the team did not change it, and kept and reported when it did;
  • the package versions follow the manifest's packages, below;
  • after writing, the solution is built; the references and using lines of the types the template moved are repaired, below, and the errors left are listed by project.

A type the template moves to another project or namespace, as 3.0 moves the types of Common, breaks the team's code that uses it. The tool reads where each public type lives - project and namespace - from both outputs of the template it generated for the merge, so the step works for any version and needs no list kept by hand. A type is followed inside its own area: a service's copy of a type leads to the same service, a type of the shared code to the shared code, so a service never gets a using or a reference of another service.

Right after the merge is written, a project of the template references the projects of the template its new version references, no more and no fewer: a reference its earlier version got wrong goes, one it adds comes, and a reference to a project of the team's stays; on central package management, a version a PackageReference still names goes to src/Directory.Packages.props and leaves the project. A version declared already, there or in a flag's file of src/package-versions, is not declared again; the project's own version, when it differs, stays as its VersionOverride, so no package changes its version. A folder whose Directory.Build.props turns central management off, a project left in conflict and a project of a part not merged yet are left as they are. A package the template drops keeps its version while a project of the team's still references it. Then, before any build and whether it has conflicts or not, every using of the solution's code that names a namespace the template no longer has is replaced by where the types the file names went, and the project gets the reference to their projects alone - the domain does not reference the infrastructure for a namespace whose types went both ways. A file that names no moved type, as one that only calls extension methods, gets every destination. A type the template moved to another namespace that the code names by its full name - as a type, in new, typeof, a generic argument or before a static member, with global:: or without, in an alias or a static using - gets its new namespace in place, read from the C# syntax, and the project the reference; a cref of an XML comment and code under an inactive #if are left to the build. A file left in conflict, a file of a project left in conflict and a namespace the solution's own code still declares are left for the build after the conflicts are resolved. In the merge by git the changed files join the merge. Then the build has less to find, often nothing. After a build that fails:

  • a using of a namespace the template no longer has (CS0234) becomes the using of the namespaces its types went to, and the project gets a reference to the projects they are in;
  • a type not found (CS0246) that the template moved out of a namespace the file uses gets its using and the reference to its project; a type of the team's with a template's name, a type the template did not move, or moved to two homes, is left as it is;
  • the solution is built again, while each round finds something new to repair: once a project builds, the projects that reference it show their own errors.

The report names, by project, the using lines and full names it changed and the references it added; git diff shows them. The repair runs after the merge is written. The one before the build is a step of the write: in the merge by git a failure or Ctrl+C there takes back the run; in the merge by files the files stay written, and the error says so. A write of the repair after a build that fails stops the repair, and the report says why. With --no-build there is no build and no repair.

The versions an upgrade covers

Supported upgrades:

From To Supported
any release of a major any later release of that major yes, in one dotskit upgrade
the last release of a major the first release of the next major yes: 2.9 to 3.0, 3.x to 4.0
a release in the middle of a major the next major through the last release of its own major first: 2.3 to 2.9, then to 3.0
a major the one after the next no: 2.9 reaches 3.x, not 4.x - go major by major

dotskit X.y upgrades a solution of every minor of X and of the last minor of X-1. From an older major the tool names the steps: the tool of the solution's own major, dotskit upgrade, then the next major, and so on. A solution newer than the tool is refused with the command that updates the tool. Where a version comes from does not change any of this: the tool takes it from the package on nuget.org, which carries the template from 2.6.1, and anything earlier from the archive of its tag on GitHub, so a solution of any 2.x release is upgraded the same way. The tools of 2.x also take 1.0.0, the only release of 1.x, which differs from 2.0.0 in most files (a solution of v1).

Inside one major version the template takes a solution of its first release to any later one, so the usual way is one dotskit upgrade from where the solution stands. --to 2.x.y stops at a release between instead, a release newer than the solution and not newer than the tool: the more conservative path, for a team that wants to build the solution and run its tests on that release before going on. Either way the upgrade goes a part at a time, so a step is a part, not the whole distance.

dotskit upgrade --to 2.7.0   # a part of 2.7.0 a run, until the manifest says 2.7.0; build and test there
dotskit upgrade              # then the parts of the tool's version, a run each

An upgrade goes one part a run, so the team resolves the conflicts of one part before the next: first the shared parts - the root with deploy and the scripts, then the files of src/common and each of its projects, a project before those that reference it, then src/capabilities - then a service folder at a time, in order. A project the template adds to src/common is a part of its own, after those it references; the first run takes the parts and the manifest keeps them. A package the template drops keeps its version while a project of the parts left, or of the team's own, references it. A folder of the team's own is a part too, which merges nothing. Each run is the same merge, of the template's new files for the parts merged so far and this one over its files for the parts before. After the merge the run repairs the usings and project references of the types the template moved between the parts merged so far, in every project of the solution, and names them by project; a type that goes to a part not yet merged waits for it, and the usings of its namespace stay. Then the solution builds, and its errors repair the team's files, leaving the template's files of a part not merged yet to that part; a service whose files of the template wait for their part may fail until it, and the build names it. The manifest keeps the release and the parts merged, and the next dotskit upgrade takes the next part; --part Orders names one, after the shared parts. A part is committed before the next; the last one moves the manifest to the release, and dotskit new waits until then. When the last part leaves conflicts, the upgrade stays open: once they are resolved and committed, dotskit upgrade changes no file of the template, repairs the usings of the whole upgrade, builds and moves the manifest. --all-at-once merges every part in one run. The template's output for a version is generated once and kept in the tool's folder under the user's local application data (dotskit/outputs) for two days, so a part after the first skips the generation.

Speed

An upgrade of a solution with seven services is dozens of merges, and each of them could have cost a full generation, a full build and a compilation of the solution's code. None of them does:

How
The template's output generated once per version and kept, keyed by the tool's build and the manifest; every later part of the upgrade copies it instead of running dotnet new again
The types the template moved read from the two generated outputs as an index of name to project and namespace, so nothing has to be compiled to know where a type went, and no list is kept by hand
The team's code read by the C# syntax (Roslyn), not by a compilation: a full name is found and rewritten where it is written, and a string or a comment that merely spells the type is left alone
The commit of the template's output written through an index of the tool's own (GIT_INDEX_FILE), so the solution's index and files are never touched by it
The paths given to git handed over in a file (--pathspec-from-file), so a solution of any size fits in one command
The build run once per part, and the repair by its errors only while a round still finds something new

On the two production solutions above a part took 30 to 37 seconds, the whole distance 20 to 23 minutes of machine time, and the generation ran twice instead of dozens of times.

dotskit upgrade --to 2.7.0               # the shared part; resolve, commit
dotskit upgrade                          # the next part; resolve, commit
dotskit upgrade --part Billing           # the service named
dotskit upgrade --to 2.7.0 --all-at-once # or every part in one merge

Package versions come with an upgrade as far as the manifest lets them:

"packages": {
  "updates": "minor",
  "pinned": { "Npgsql": "8.0.4" }
}
updates What an upgrade brings
minor (default) patches and minor versions
patch patches only
none no change of a package the solution has

A package in pinned stays on that version. A major version of a package comes only with a major version of the template. A package new to the solution comes as the template has it. Before the merge, the held versions are written into the template's side, so these lines do not conflict - a version the team raised itself does, unless it is pinned; the plan has a line for each package that changes or is held - "Npgsql: pinned on 8.0.4, the template brings 8.0.11". When the template's new code needs a newer version than a pinned one, the build fails and the report names the project.

The upgrade does the mechanical part: the text of the files, moves and removals, the package versions, the build. Whether the team's own code should change with the template's - a handler of its own beside one the template changed - is the team's to decide, and its tests show it.

The version a solution came from

Directory.Build.props at the root of a solution names the version of the template it was generated from:

<DotNetSolutionKitVersion>2.7.0</DotNetSolutionKitVersion>

The value is written when the template is packed, so a solution generated from the package on nuget.org or from a release's .nupkg names that release; one generated from the template's sources says source. The file comes with the solution (--Solution): a service added later with a newer template does not change it. A solution generated before the property was added has none; the dates on the releases page tell its version.

Each service's API project names the version it was generated from too, as DotNetSolutionKitServiceVersion. A service whose major version differs from the solution's does not build: its code expects the Common of its own major, and the error says which versions met. Update the solution first, then generate the service. A solution generated before the version was recorded counts as 2.x; source on either side is not checked.

To update, read the release notes after that version. Your src/common compared with the template's sources at the tag v<version> shows what your team changed; that tag compared with the new one shows what the template changed.

The files where the template's own mechanisms start, its README files and Directory.Build.props carry a line with the template's name, its site, the MIT license and its author. MIT asks for the copyright notice to stay in copies of the code, and this line is that notice. It does not call the file generated: Roslyn treats a file marked <auto-generated> as generated code, most analyzers skip it, and nullable warnings are off in it.

From any release of 2.x

dotskit upgrade

Every file of the template - Common, the capabilities, the root, deploy, the scripts, the workflows, the test servers, the package versions - is merged by the tool, whatever release the solution comes from. The releases between ask nothing of a team that uses the template as it is.

What stays with the team - its own code and its own call:

Since What is the team's to do
v2.7.0 Post-commit handlers of domain events now run after a save with no transaction too. A handler of the team's that was silently not running starts running: read the post-commit handlers of each service before the upgrade.
v2.8.0 A service's tests carry the attributes of TestFramework.cs ([Test], [Integration], [RunsInParallel], [RunsAlone]). The tests of the team keep their own attributes until it replaces them; under NUnit a fixture that keeps state between its tests in fields needs that state made static or moved into each test.
v2.8.0 A service of the team's own added to deploy/compose/infrastructure.yml moves into deploy/compose/infra/, a file per part.
v2.8.0 Infisical__SnapshotPath and Vault__SnapshotPath are gone: remove the key where it is set and delete the snapshot file, which holds secrets nothing updates any more.
v2.8.0 A service of the team's that writes files in its container writes them to /tmp, or to object storage when they must outlive the container: the root filesystem is read-only.

Everything else these releases changed - where a file lives, which project references which, how the infrastructure is split, how the test servers start - the upgrade brings on its own.

A solution of v1

v1.0.0 and v2.0.0 differ in most files, and a merge of the two leaves most of them in conflict: on one real solution of v1, 96 files. So the code is brought to v2.0.0 by the list below, the solution is described as 2.0.0 with dotskit init --template-version 2.0.0, and dotskit upgrade takes it on from there. dotskit init names this way when the closest release is 1.0.0. dotnet new ... --force over a solution of v1 overwrites the files v2 changed and leaves the rest, and the result does not build.

What that path gives, measured on two SaaS telecom platforms built on this template and running in production for months, each of them generated by v1. Each was described as 1.0.0 and taken to the current version, one of them straight through 2.0.0, the other with a stop at 2.7.0; the whole distance went through, the team's code came along, and the tests of the services passed at the end - 2376 tests on one of them. The tool made 419 edits of usings, namespaces and references on the straight path that nobody had to make by hand. What it did not do: the architecture v2 changed is still the team's to change. On one solution the code of Common used Common.Contracts, which v2 reversed, and 42 compiler errors were left for a person - a reference back would be a cycle, so the tool refuses to add it and says so.

The changes of v2

Build and projects:

  • Package versions are declared once, in src/Directory.Packages.props; a .csproj references a package by name only.
  • A project sees only the projects it references itself: transitive project references are off. The API project reaches the domain through the application project.
  • Common.Contracts depends on Common, not the other way round; the user and execution context moved into Common (Domain/Context).
  • The version is set by hand in version.json instead of Nerdbank.GitVersioning; see ADR-004.
  • One Dockerfile at the solution root builds every service.

Service host:

  • The web layer comes from Common.Web: AddPlatformLogging, AddPlatformWebApi and UsePlatformPipeline replace the service's own Setup files for logging, JSON, CORS, Swagger, validation and the pipeline. See web layer.
  • The application is built in SchemaHost.Build, which Program calls.
  • The DI container is validated at startup in every environment.
  • The service's DbContext is registered with AddDbContext, not from a pool.

API contract:

  • Errors are RFC 9457 problems; the ErrorResponse envelope is gone. See errors.
  • Enums are written as names instead of numbers, and DateTimeOffset values in UTC with Z instead of the server's offset. See JSON.
  • A route without an api/v{n} segment is listed only in the all Swagger document; before, it was counted as v1. See Swagger documents.
  • Failed validation answers 422, not 400.
  • Health endpoints are /health and /ready instead of /healthz and /readyz.

Code:

  • TenantId replaces PartnerId in the user context.
  • Domain events dispatch the cascade raised by pre-save handlers, run post-commit and rollback handlers in a fresh scope, and reach Hangfire jobs. See domain events.
  • The permission check and IPermissionService moved to Common; permissions come from the token's claims by default. See authentication and permissions.