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.
dotnet tool update -g SawKing.DotsKit.Tool
dotskit upgradeWhat 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 -Mbetween 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
usinglines 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
usingof a namespace the template no longer has (CS0234) becomes theusingof 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 itsusingand 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.
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 eachAn 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.
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 mergePackage 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.
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.
dotskit upgradeEvery 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.
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.
Build and projects:
- Package versions are declared once, in
src/Directory.Packages.props; a.csprojreferences 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.Contractsdepends onCommon, not the other way round; the user and execution context moved intoCommon(Domain/Context).- The version is set by hand in
version.jsoninstead of Nerdbank.GitVersioning; see ADR-004. - One
Dockerfileat the solution root builds every service.
Service host:
- The web layer comes from
Common.Web:AddPlatformLogging,AddPlatformWebApiandUsePlatformPipelinereplace the service's ownSetupfiles for logging, JSON, CORS, Swagger, validation and the pipeline. See web layer. - The application is built in
SchemaHost.Build, whichProgramcalls. - The DI container is validated at startup in every environment.
- The service's
DbContextis registered withAddDbContext, not from a pool.
API contract:
- Errors are RFC 9457 problems; the
ErrorResponseenvelope is gone. See errors. - Enums are written as names instead of numbers, and
DateTimeOffsetvalues in UTC withZinstead of the server's offset. See JSON. - A route without an
api/v{n}segment is listed only in theallSwagger document; before, it was counted asv1. See Swagger documents. - Failed validation answers 422, not 400.
- Health endpoints are
/healthand/readyinstead of/healthzand/readyz.
Code:
TenantIdreplacesPartnerIdin 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
IPermissionServicemoved toCommon; permissions come from the token's claims by default. See authentication and permissions.