How it works
The pipeline
Section titled “The pipeline”flowchart TD
A["Git diff<br/>--from → working tree"] --> B["Changed files"]
B --> C["Discover projects<br/>minus --exclude-discovery"]
C --> C2["Evaluate into ProjectGraph"]
C2 --> D["Map files to projects"]
D --> E["Changed projects"]
B --> F["Directory.Packages.props<br/>changed?"]
F --> G["Changed NuGet packages"]
E --> H["Walk the graph<br/>find dependents"]
G --> H
H --> I["Affected projects"]
E --> J["Output<br/>traversal / text / json / slnf"]
I --> J
- Diff. The working tree is compared with Git against the
--frombaseline, producing the list of changed files.--uncommitteddecides whether staged changes, unstaged changes and untracked files count on top of the commits. With nothing specified, that isHEADagainst a working tree where everything counts. - Discover and evaluate projects. All
.csproj,.fsprojand.vbprojunder the repository path are found — or just the ones a filter file lists — then--exclude-discoveryremoves any that should never be loaded, and MSBuild evaluates the rest into aProjectGraphthat records which project depends on which. - Map files to projects. Every changed file is attributed to the projects that reference it. This is broader than
“files in the project folder”: it covers any item MSBuild pulls in, and imported files like
Directory.Build.props, which belong to every project importing them. - Diff NuGet packages. If
Directory.Packages.propschanged, the package sets are compared across the same two revisions the file diff used — see NuGet package changes. - Walk the graph. From the changed projects and changed packages, dependents are followed transitively.
- Format the output. The changed and affected projects are written in the requested formats.
Changed vs. affected
Section titled “Changed vs. affected”A project is changed when one of its own files changed. A project is affected when it depends, directly or transitively, on something that changed — another project, or a NuGet package whose version moved.
flowchart BT
IS["Inventory.Shared"]
I["Inventory"]
IT["Inventory.Tests"]
IST["Inventory.Shared.Tests"]
I --> IS
IT --> I
IST --> IS
When Inventory changes, Inventory.Tests is affected, so both are built and tested. Inventory.Shared.Tests is
left alone, because Inventory.Shared did not change.
When Inventory.Shared changes, everything is built and tested — transitively, they all depend on it.
Architecture
Section titled “Architecture”Three packages, plus the MSBuild SDK, layered so that the analysis has no idea how it is being driven.
flowchart TD
subgraph entry["Entry points"]
direction LR
CLI["dotnet-affected<br/>CLI"]
TASKS["DotnetAffected.Tasks<br/>MSBuild SDK"]
end
EXEC["AffectedExecutor"]
PROC["AffectedProcessor"]
subgraph core["DotnetAffected.Core"]
direction LR
DISC["Discovery<br/>ProjectDiscoveryManager<br/>ProjectGraphFactory"]
CHG["Change detection<br/>GitChangesProvider<br/>PredictionChangedProjectsProvider"]
PKG["Package diff<br/>NugetHelper"]
end
ABS["DotnetAffected.Abstractions<br/>interfaces + AffectedSummary"]
CLI --> EXEC
TASKS --> EXEC
EXEC --> PROC
PROC --> DISC
PROC --> CHG
PROC --> PKG
PROC --> ABS
entry -.consumes.-> ABS
DotnetAffected.Abstractions
Section titled “DotnetAffected.Abstractions”The contracts, and nothing else: IAffectedExecutor, IChangesProvider, IChangedProjectsProvider,
IProjectDiscoverer, IDiscoveryOptions, plus the result types AffectedSummary and PackageChange. Both entry
points and the whole of Core depend on this and not on each other.
DotnetAffected.Core
Section titled “DotnetAffected.Core”Where the work happens.
| Piece | Responsibility |
|---|---|
AffectedExecutor |
The public entry point. Takes options and optional providers, returns an AffectedSummary |
AffectedProcessor |
The pipeline above, step by step, over a context that carries state between steps |
ProjectDiscoveryManager |
Picks a discoverer from the filter file’s extension — directory, .sln/.slnx, .slnf or .proj — and applies --exclude-discovery |
ProjectGraphFactory |
Evaluates the discovered projects into an MSBuild ProjectGraph, optionally through an overlay |
GitChangesProvider |
Diffs with LibGit2Sharp, and reads file contents and project files at a given commit |
PredictionChangedProjectsProvider |
Maps changed files to projects using Microsoft.Build.Prediction’s input predictors |
NugetHelper |
Parses and diffs PackageVersion/PackageReference items from both sides of the range |
SolutionFilter |
Reads and writes .slnf documents, used both as discovery input and as output format |
TraversalProject |
Authors the Traversal project XML directly, without evaluating it |
The graph itself is built lazily by the processor’s context, which is what allows the diff to run first. Discovery runs there too, rather than inside the factory, because it is the only step that knows what was excluded — and an excluded project cannot be reported through the graph it was deliberately kept out of.
dotnet-affected (CLI)
Section titled “dotnet-affected (CLI)”A System.CommandLine front end. It parses options into
AffectedOptions, calls the executor, renders the tables you see with --verbose or describe, and hands the result
to the output formatters. It adds no analysis of its own — with one exception: exit code
166 is raised here, not in Core.
DotnetAffected.Tasks (MSBuild SDK)
Section titled “DotnetAffected.Tasks (MSBuild SDK)”An MSBuild task wrapping the same executor. Instead of writing files, it replaces the ProjectReference items of the
project being built, so Traversal builds exactly the affected set. See the MSBuild SDK section.
Output formatting
Section titled “Output formatting”Formatters are a small strategy interface — a Type name, a file extension, and a method turning the project list
into text:
flowchart LR
R["Changed + affected<br/>projects"] --> E["OutputFormatterExecutor"]
E --> T["traversal<br/>.proj"]
E --> X["text<br/>.txt"]
E --> J["json<br/>.json"]
E --> S["slnf<br/>.slnf"]
The executor deduplicates projects by path, resolves each requested --format value to a formatter, and writes
<output-name><extension> into the output directory — or prints it, under --dry-run. Formats are independent, so
asking for several costs one extra file each and no extra analysis.
Each formatter also receives an OutputFormatterContext carrying the path of the file being written and the filter
file projects were discovered from. That is what lets slnf emit paths relative to the right directories and name the
solution it filters; formats that only list projects ignore it.
Extension points
Section titled “Extension points”Because Core takes its collaborators as interfaces, the analysis can be driven from something other than Git or the
file system. AffectedExecutor accepts an IChangesProvider — the source of changed files and of file contents at a
commit — and an IChangedProjectsProvider, which decides how changed files map onto projects. The built-in
implementations are GitChangesProvider and PredictionChangedProjectsProvider; the test suite substitutes its own,
and --assume-changes works by skipping the change discovery step entirely.
Comparing something other than HEAD
Section titled “Comparing something other than HEAD”The comparison always ends at your working tree, since that is where projects are discovered and evaluated.
--from chooses the baseline, and --uncommitted chooses how much of the working tree
counts on top of the commits since then.