Server build during generation
Unless the --nobuild option is used, neos generate builds the .NET projects of the cluster after generating their code. This article explains which projects are built, when their NuGet packages are restored, and how to diagnose a build or restore that does not behave as expected.
Overview
flowchart TD
A[Server code generation] --> B[CSharpAbstractions project<br/>restore if needed, then build]
B --> C[Client code generation]
B --> D[Server host projects<br/>restore if needed, then build]
C --> E[Generation completed]
D --> E
The CSharpAbstractions project is built first: the client code generation references its compiled assembly to analyze the C# code of the UI views and UI components it transpiles to TypeScript. The server host projects are then built while the client code is generated.
Every build runs with --no-restore: the NuGet packages are restored beforehand by a separate dotnet restore, at most once per build target and per generation, and only when a restore is needed. An implicit restore at the start of each build would evaluate every referenced project a second time, which costs much more than a separate restore.
What is built
| Build target | Built when |
|---|---|
<RootNamespace>.CSharpAbstractions |
It is modified, the hand-written code it references changed, or its assembly is missing |
<RootNamespace>.AspNetCore |
It is modified, the hand-written code it references changed, or its assembly is missing |
<RootNamespace>.TaskRunner |
Same, and only when TaskRunnerExecutionMode is IsolatedProcess |
A project is modified when the generation wrote one of its files, or a file of one of the projects of its group (see How modified projects are tracked). neos generate --rebuild marks every project as modified. The hand-written code is the code of the business assemblies, which the generation does not write (see How hand-written code changes are detected).
When both the AspNetCore and TaskRunner projects have to be built, they reference the same projects. Instead of two builds, the generator writes a solution containing only these two projects, server/bin/neos-build.slnx, and builds it once: the projects they share are evaluated and checked only once. When only one of them has to be built, it is built on its own.
The generation never builds the cluster solution itself. A project of the solution is restored and built only when one of the built projects references it, directly or indirectly: the business assemblies, and the projects declared under Csprojs:BusinessCode that they reference, are therefore built with the server host projects. A project of the solution that none of them references, a test project for example, is only built when the solution is built, from an IDE or a pipeline.
How modified projects are tracked
Only a few projects are built directly. Each of them heads a group of projects whose changes require it to be built again:
| Built project | Group |
|---|---|
AspNetCore, TaskRunner |
Application, Application.Abstractions, AutoGenerated.Application, the persistence projects, and the business assemblies of the cluster |
CSharpAbstractions |
Application.Abstractions (data objects) and Domain (resources) |
The modified state of a built project is stored as two files in its directory, .debug.modified and .release.modified, one per build configuration:
- When the generation writes a file, the project whose directory contains that file is marked as modified, and so is every built project whose group contains it. Marking a built project writes a new random token into both of its files.
- Before building a project, the generator reads the token of the build configuration. A project with no
.<configuration>.modifiedfile, and whose assembly exists, is not built. - After a successful build, the file is deleted, unless its token changed during the build: a file written by a generation running at the same time keeps the project marked for the next build.
Other generated projects, such as Application.Abstractions or the database persistence project, can also hold these files for other tools. They do not decide what this build compiles.
By default, the generation only reports the files whose content changed, so an unchanged project keeps no marker. With --rebuild, the generation reports every file it generates, even unchanged ones: every project containing generated files is marked, and every built project is therefore built.
How hand-written code changes are detected
The files of the business assemblies, written by hand or by an agent, do not mark any project. After each successful build of a built project, the generator records the sources of the hand-written projects it references in the server/bin directory of the cluster, in a file named <ProjectFileName>.<configuration>.sources, for example Sales.AspNetCore.csproj.debug.sources.
The hand-written projects are the projects located under the modules directory of the cluster that the built project references, directly or indirectly, read from its restore results like the restore markers. Their sources are:
- every file of their directories and subdirectories, except the
bin,objandnode_modulesdirectories and the directories whose name starts with a dot; - the files that change their restore result, as listed in a restore marker: a
Directory.Build.propsfile of thebusinessAssemblydirectory, for example.
The record has one line per source, sorted alphabetically without regard to case, with its full path, its size in bytes and its last write date (UTC), separated by |:
C:\Work\Sales\modules\Orders\businessAssembly\Application\EventRules\OrderEditView\Retrieving.cs|1834|2026-10-20T08:12:41.5310000Z
C:\Work\Sales\modules\Orders\businessAssembly\Application\Orders.Application.csproj|1203|2026-10-02T14:03:10.0000000Z
At the next generation, the generator lists the sources again. A project that is neither modified nor missing its assembly is built when the list differs from its record in any way: a source added, removed, renamed, resized, or with another date. MSBuild then compiles again only the projects whose files are newer than their compiled assembly. A project whose restore results are missing, or with no record, is built, and so is every built project at the first generation that creates the records.
Note
A file located outside the directory of a hand-written project, such as a linked file, and a project located outside the modules directory are not watched. Build the server host projects yourself, or run neos generate --rebuild, after changing them. A source replaced by a version with an older date, for example copied from a backup with its date, triggers the build, but MSBuild does not compile it again: delete the obj directory of its project first.
When packages are restored
A build target (a project, or neos-build.slnx) is restored with dotnet restore <target> -p:Configuration=<configuration> --ignore-failed-sources when at least one of the following holds:
- the generation runs with
--rebuild; - the generation wrote a file that changes the restore result: a
.csproj,.props,.targets,packages.lock.json, ornuget.configfile; - one of the projects concerned has no restore results (
project.assets.json); - the restore marker of the build target is missing, or one of the files it watches was added, removed, or modified since the last restore, or the restore results of a project were rewritten since then.
The configuration is the build configuration (--build-configuration option, or the BuildConfiguration setting of the cluster, Debug by default), so that package references conditioned on the configuration are restored as they are built.
The projects concerned are the projects to build and every project they reference, directly or indirectly. They are read from the libraries of type project in the restore results of the projects to build. For each of them, the restore results are looked up in the default obj directory of the project, then in the obj/<ProjectName> directory shared by the business assemblies next to their projects.
Note
On a cluster whose dependency modules are referenced by a local path, the generation deletes the bin and obj directories of the business assemblies of these modules. Their restore results are therefore missing at each generation, and the server host projects are restored every time. Dependency modules installed from packages are not affected.
Restore markers
After each successful restore, a marker file is written in the server/bin directory of the cluster:
| Marker | Written after |
|---|---|
<ProjectFileName>.<configuration>.restore |
The restore of this project, or of a solution containing it |
neos-build.slnx.<configuration>.restore |
The restore of the solution of the server host projects |
For example, Sales.CSharpAbstractions.csproj.debug.restore. Markers are kept per configuration, since a restore evaluates the projects for a given configuration. After a restore of neos-build.slnx, the markers of the AspNetCore and TaskRunner projects are written too, so that a later build of one of them alone does not restore it again.
What a marker represents
A marker is a snapshot taken right after a successful restore of its build target. It records two things:
- when the build target was last restored: the last write date of the marker file;
- against which files: its content, the list of the files that changed the restore result at that time.
A restore result depends on the project files and on the files MSBuild and NuGet read while restoring them. The marker lists these files for the projects of the build target and for every project they reference, directly or indirectly:
- each project file;
- in the directory of each project, in each of its parent directories, and in the
buildsubdirectory of each of these, every.props,.targets,packages.lock.json, andnuget.configfile, whatever the case of its name.
This covers, for example, a package reference added by hand in a business assembly project, a Directory.Build.props or Directory.Packages.props file, the generated server/build/<Cluster>.props file, or a props file of the framework changed by a pull.
Format
A marker is a UTF-8 text file with one full path per line, sorted alphabetically without regard to case. Only files that existed at the time of the restore are listed. An empty marker means the restore results were still missing right after the restore: the next generation restores again.
For example, the marker of the CSharpAbstractions project of a Sales cluster located in C:\Work\Sales, which consumes the framework as packages:
C:\Work\Sales\server\build\Sales.props
C:\Work\Sales\server\nuget.config
C:\Work\Sales\server\Sales.Application.Abstractions\packages.lock.json
C:\Work\Sales\server\Sales.Application.Abstractions\Sales.Application.Abstractions.csproj
C:\Work\Sales\server\Sales.CSharpAbstractions\packages.lock.json
C:\Work\Sales\server\Sales.CSharpAbstractions\Sales.CSharpAbstractions.csproj
C:\Work\Sales\server\Sales.Domain\packages.lock.json
C:\Work\Sales\server\Sales.Domain\Sales.Domain.csproj
Reading it line by line:
Sales.CSharpAbstractions.csprojis the build target.Sales.Application.Abstractions.csprojandSales.Domain.csprojare the projects it references, read from its restore results.- Each project comes with its
packages.lock.jsonfile, from the same directory. server/build/Sales.propsis found in thebuildsubdirectory ofserver, a parent directory of the three projects. It is imported by the generated projects.server/nuget.configis found in the same parent directory. It gives the package sources.
A Directory.Build.props file in C:\Work\Sales, or in C:\Work, would appear in this list as well. The framework projects do not appear, since they are consumed as packages: a framework version change goes through the project or props files listed here.
The markers of the server host projects follow the same format, with many more lines: the business assemblies of the cluster and of its dependency modules, and the framework projects when the cluster is built from the framework source code. Each line names a file whose change triggers a restore of the build target.
How a marker is compared
At the next generation, the generator lists the same files again, reads the marker, and restores the build target when one of the following differs:
| Change since the last restore | Example | Detected by |
|---|---|---|
| A listed file was modified | A package version changed in Sales.Domain\packages.lock.json |
The file is newer than the marker |
| A file was added | A Directory.Packages.props file created in C:\Work\Sales |
The current list contains a line the marker lacks |
| A listed file was removed | server\nuget.config deleted |
The marker contains a line the current list lacks |
| The restore results of a project were rewritten | A restore for the Release configuration rewrote project.assets.json |
The restore results are newer than the marker |
| The restore results of a project are missing | The obj directory of a business assembly was deleted |
No project.assets.json found for the project |
When nothing differs, the restore is skipped and the build runs directly with --no-restore.
Tip
To know whether a file is watched for a build target, search for its path in the marker of that build target. To force the next restore of a build target, delete its marker.
Caution
An MSBuild file imported from any other directory is not watched: listing the files actually imported would require evaluating every project, which is what the markers avoid. If such a file changes the package references, run neos generate --rebuild or delete the markers.
Retry after a restore error
When the restore was skipped and the build then fails with an error showing that the projects were built against missing or outdated restore results, the build target is restored and built again, once. The recognized errors are NETSDK1004, NETSDK1005, NETSDK1047, NETSDK1064, NU1xxx, and any error mentioning project.assets.json.
Only error lines are taken into account: a package vulnerability warning (NU1902, NU1903) displayed next to a real compilation error does not trigger a retry. A build that fails again, or for another reason, is reported as a compilation failure.
Troubleshooting
See what the generation built and restored
Each dotnet restore and dotnet build command run by the generation is logged with its process identifier and full command line, followed by its exit code:
INF ProcessRunner - [PID47988] dotnet restore ".../server/bin/neos-build.slnx" -p:Configuration=Debug -v Minimal --ignore-failed-sources
INF ProcessRunner - [PID47988] exit code: 0
INF ProcessRunner - [PID48012] dotnet build ".../server/bin/neos-build.slnx" -c Debug -v Minimal --no-restore
- No
dotnet restoreline for a build target means its restore was skipped. - No
dotnet buildline for a project means it was not modified, with no.<configuration>.modifiedfile in its directory (see How modified projects are tracked), its hand-written code did not change since its record (see How hand-written code changes are detected), and its assembly exists. Runneos [log:verbose] generateto see the message<project> (configuration: <configuration>) is not modified and will not be built. - The message
<project> (configuration: <configuration>) is built because the hand-written code it references changedmeans that its.sourcesrecord differs from the current sources. To know which source differs, compare the record with the files listed. To force the next build of a project, delete its record. - A
dotnet buildline repeated for the same target, with adotnet restoreline in between, means the build was retried after a restore error.
The MSBuild verbosity of these commands is Minimal by default. Set the ServerBuildVerbosity setting of the cluster (for example Normal or Detailed) to get more output. The raw output of a failing build is logged at the Verbose level, see .NET compilation diagnostics.
A build fails with missing types or packages
If a build fails with errors such as CS0246 after a package change that the generation did not notice, the restore results are outdated. Force a restore with one of these:
- run
neos generate --rebuild, which restores every build target; - delete the
*.restorefiles ofserver/bin, then runneos generate; - run
dotnet restoreon the project, thenneos generate.
Then check whether the file that changed the package references is in a watched location (see Restore markers).
The restore runs at every generation
Open the marker of the build target in server/bin, and check the following:
- The marker is missing or empty: the previous restore failed, or its restore results were missing just after the restore.
- A project of the build target has no
project.assets.json: on a cluster with dependency modules referenced by a local path, this is expected (see When packages are restored). - A watched file keeps changing: a tool or a build step rewrites a
.props,.targets,packages.lock.json, ornuget.configfile at each run, or rewrites the restore results. - The configuration alternates between generations: each configuration has its own markers, and both share the restore results of the projects.
Continuous integration
A pipeline agent starts without markers, so every build target is restored. Pipelines usually generate with --rebuild, which restores in any case. On a pipeline, the generation mainly benefits from the single build of the server host projects; the conditional restore pays off on a developer machine, across successive generations.