Repository files navigation

𓍋 Chisel

CIReleaseLatest releaseNuGetLicense: MIT.NET 10Sponsor

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Given a fully-qualified type name and a .sln, chisel walks the Roslyn semantic graph starting from that type and pulls in every .cs file in the codebase required to compile it — across project boundaries. Code that lives outside the codebase (the BCL, NuGet packages) is treated as a leaf: it is never vendored in, but every external assembly / NuGet package the slice touches is recorded so a follow-on step can resolve references.

flowchart LR
A["Seed type<br/>e.g. MyNS.IFoo"] --> B["TypeResolver"]
B -->|"walk the semantic graph:<br/>base · interfaces · members · bodies<br/><i>(in-source only)</i>"| C["The slice<br/>(.cs files)"]
B --> D["External refs<br/><i>(BCL / NuGet — recorded as leaves)</i>"]
Loading

📖 New here? The Guide is the deep dive — the dependency walk in detail, every output format, source generators, multi-targeting, global usings, and embedding the Core library.


What it carves out

By default the walk keeps the declared shape of the seed and its implementations — the contract and the data types — but not what method bodies use. Starting from the seed type it follows:

  • Base types, interfaces (including inherited ones), and generic constraints / type arguments.
  • Member signatures — field/property/event types, method return & parameter types, indexer parameters, nested types, and the enclosing type chain of a nested seed.
  • Attributes — the attribute class plus any typeof(SomeType) in constructor or named arguments (including arrays of typeof).
  • Seed implementations — when the seed is an interface or abstract class, its concrete implementations (and, for an interface seed, the base interfaces it derives from) are pulled in. Interfaces/classes encountered deeper in the graph (e.g. a property's type) are included as declarations only — their implementations are not expanded.
  • Authored global using files — dedicated global-usings files are kept even though they declare no types.

What it deliberately leaves behind by default: method-body usages (what the kept types call or instantiate) and implementations of interfaces reached deep in the graph. This keeps the slice a focused "contract + shape" extract rather than the whole reachable call graph.

Two knobs widen the walk:

  • --walk-depth bodies — also follow every type referenced inside method bodies, transitively. Produces a self-compilable slice, but pulls in far more. (Default signatures does not, so a default slice may not compile standalone — body-referenced in-source types are treated as external.)
  • --expand-impls all — expand every interface/class reached to its implementations, not just the seed. (--expand-impls none, aliased --no-derived, never expands.)

Anything whose containing assembly is not one of the solution's projects (BCL, NuGet) is a leaf: it is recorded in references.json and as a <PackageReference>, but its source is never pulled into the slice.


Requirements

  • .NET 10 SDK (pinned via global.json to 10.0.300).
  • The target solution should be restored so package metadata resolves — either run dotnet restore yourself, or pass --restore to have chisel do it first.

chisel loads and evaluates your solution with MSBuild, which ships with the .NET SDK. The SDK is therefore required at run time, not just to build chisel — see the note under pre-built binaries.


Install

As a .NET tool (recommended)

chisel is packaged as a .NET tool, so it installs by name and is invoked as dotnet chisel …:

dotnet tool install --global Bennewitz.Ninja.Chisel
dotnet chisel --version

Pre-built binaries

Each release also publishes self-contained, per-platform archives:

PlatformFile
Windows (x64)chisel-<version>-win-x64.zip
Windows (ARM64)chisel-<version>-win-arm64.zip
Linux (x64)chisel-<version>-linux-x64.tar.gz
Linux (ARM64)chisel-<version>-linux-arm64.tar.gz
macOS (Intel)chisel-<version>-osx-x64.tar.gz
macOS (Apple Silicon)chisel-<version>-osx-arm64.tar.gz

These binaries still require a .NET 10 SDK on the machine. Unlike a typical self-contained app, chisel locates and drives the installed SDK's MSBuild at run time (via MSBuildLocator); the Microsoft.Build.* engine assemblies are deliberately not bundled. The archives only save you the dotnet tool install step. Since any machine that can build the solution you're slicing already has the SDK, the .NET tool above is the recommended path. If no SDK is found, chisel exits 7 with an actionable message.

Build from source

git clone https://github.com/JanusMael/chisel.git
cd chisel
dotnet build
dotnet run --project src/Chisel.Cli -- --help

To install your local build as a tool:

dotnet pack src/Chisel.Cli -c Release -o ./nupkg
dotnet tool install --global --add-source ./nupkg --prerelease Bennewitz.Ninja.Chisel
dotnet chisel --version

Quick start

dotnet chisel \
--type MyNS.IFoo \
--solution path/to/MySolution.sln \
--output ./out

This writes the artifacts into ./out (see Outputs) and prints a summary:

Seed type: global::MyNS.IFoo
Files: 6
External refs: 3 (1 NuGet packages)
Projects: 4
files.json → .../out/files.json
references.json → .../out/references.json
Slice.csproj → .../out/Slice.csproj
copied sources → .../out/src
.gitignore → .../out/.gitignore

To build the extracted slice on its own:

dotnet build ./out/Slice.csproj

Command-line reference

dotnet chisel --type <FQN> --solution <path.sln> --output <dir> [options]

Required

FlagDescription
--type, -t <FQN>Fully-qualified type name. See type-name formats.
--solution, -s <path>Path to the .sln / .slnx file.
--output, -o <dir>Output directory (created if missing).

Options

FlagDefaultDescription
--project <name>Disambiguate when the FQN matches types in multiple projects.
--tfm <name>first TFMPreferred target framework when a project multi-targets.
--walk-depth <d>signaturessignatures (declared shape only) | bodies (also follow method-body usages transitively — self-compilable, larger).
--expand-impls <s>seedseed (expand only the seed + an interface seed's base interfaces) | all (every interface/class reached) | none.
--no-derivedAlias for --expand-impls none.
--source-generators <p>referenceskip | materialize | reference — how to treat generator output.
--exclude, -x <path>Directory subtree to drop from the slice (repeatable). Any collected file under <path> is logged and left out — handy for vendored/generated/out-of-scope regions. The slice may then be incomplete; each drop is reported as an Exclude warning.
--exclude-from <file>Read exclusion directories from a file, one path per line (repeatable; merged with --exclude). Use - to read from stdin ($paths | chisel --exclude-from -). Blank lines and # comments are ignored; relative paths resolve against the file's directory (the working directory for stdin).
--allow-partialoffContinue when MSBuild reports project-load failures (otherwise fail fast).
--restoreoffRun dotnet restore on the solution before analyzing (best-effort; a failed restore warns and continues).
--format <f>texttext (human summary on stdout) | json (the run manifest on stdout). result.json is always written to --output regardless.
--strictoffExit nonzero (6) if any error-severity diagnostic occurred (default stays 0 on a best-effort run).
--verbose, -vList every diagnostic instead of grouping by stage.
--quiet, -qConsole shows only warnings/errors; the full run log is still written.
--no-colorDisable ANSI color (also honored via the NO_COLOR env var).
--version, -VPrint version and exit.
-h, --helpShow usage.

Type-name formats

You typeResolves to
MyNS.Widgetthe non-generic Widget
MyNS.Repository<T> or MyNS.Repository<>the open generic Repository<T> (arity 1)
MyNS.Map<,>the open generic with arity 2
MyNS.Outer.Inner or MyNS.Outer+Innerthe nested type Inner

Exit codes

CodeMeaning
0Success
1No arguments (usage printed)
2Invalid arguments
3Type not found / ambiguous (pass --project)
4Workspace failed to load (try --allow-partial)
5Solution file not found
6Completed with error-severity diagnostics (only under --strict)
7No .NET SDK / MSBuild found (install the .NET 10 SDK — see Requirements)
130Canceled (Ctrl+C)

Scripting (PowerShell)

Designed for PowerShell Core 7+. Streams are split for clean capture — stdout carries the result (the text summary, or the manifest under --format json), stderr carries progress/diagnostics — and a stable result.json is always written. Read it with fully-qualified .NET (no cmdlets, no piping):

& dotnet chisel -t 'Contracts.IShape'-s $sln-o $out--strict
if ($LASTEXITCODE-ne0) { throw"chisel failed ($LASTEXITCODE)" }
$doc= [System.Text.Json.JsonDocument]::Parse(
[System.IO.File]::ReadAllText([System.IO.Path]::Combine($out,'result.json')))
$root=$doc.RootElement$root.GetProperty('counts').GetProperty('files').GetInt32()
foreach ($pin$root.GetProperty('packages').EnumerateArray()) {
"$($p.GetProperty('id').GetString())$($p.GetProperty('version').GetString())"
}

Or capture the manifest straight off stdout: $json = & dotnet chisel … --format json (progress still shows on stderr).

Generic types in PowerShell: single-quote any type name containing <, >, or a backtick — 'MyNS.Repository<T>' or 'MyNS.Repository`1' (both resolve). < is a reserved PS operator and the backtick is the PS escape char; single quotes pass them through literally.

See Exit codes to branch on $LASTEXITCODE; --strict turns any error-severity diagnostic into a nonzero exit.


Outputs

All of these are written into --output.

Slice.csproj — a flat, buildable project

Explicit <Compile Include> per collected file (no ProjectReferences — the slice is flattened), plus a <PackageReference> per detected NuGet package. Compilation settings (TargetFramework, LangVersion, Nullable, ImplicitUsings, AllowUnsafeBlocks, user DefineConstants) are hoisted from the contributing projects — taking the highest/strictest value when projects disagree, and warning you when they do.

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<CompileInclude="src/ExternalPackage/MyClass.cs" />
</ItemGroup>
<ItemGroup>
<PackageReferenceInclude="newtonsoft.json"Version="13.0.3" />
</ItemGroup>
</Project>

files.json — the file manifest

{
"files": [
{
"path": "C:\\...\\Shapes\\Composite\\Group.cs",
"project": "Composite",
"targetFramework": "net10.0",
"isGenerated": false,
"containsSymbols": [ "global::Composite.Group" ]
}
]
}

references.json — external references (the follow-on step's input)

NuGet packages and framework assemblies the slice depends on but does not vendor:

{
"packages": [
{
"id": "newtonsoft.json",
"version": "13.0.3",
"assemblyName": "Newtonsoft.Json",
"assemblyVersion": "13.0.0.0"
}
],
"frameworkAssemblies": [
{
"name": "System.Runtime",
"version": "10.0.0.0",
"path": "C:\\Program Files\\dotnet\\packs\\Microsoft.NETCore.App.Ref\\...\\System.Runtime.dll"
}
]
}

result.json — the machine-readable run manifest

Written on every run (success or fatal), and also printed to stdout under --format json. A single, stable, camelCase object — success flag, exit code, seed, mode, counts, the output paths, the NuGet packages, and the full diagnostics list — designed to be read directly with System.Text.Json (see Scripting).

{
"schemaVersion": 1,
"tool": { "name": "chisel", "version": "2026.2.624" },
"success": true, "exitCode": 0, "elapsedSeconds": 3.1,
"seed": { "displayName": "global::MyNS.IFoo", "filePath": ".../IFoo.cs" },
"mode": { "walkDepth": "signatures", "expansion": "seed", "sourceGenerators": "reference" },
"counts": { "inSourceTypes": 7, "files": 6, "projects": 4, "externalReferences": 3, "packages": 1 },
"packages": [ { "id": "newtonsoft.json", "version": "13.0.3" } ],
"diagnostics": [ { "severity": "Warning", "stage": "Walk", "message": "", "item": "" } ]
}

src/… — copied sources

Every collected .cs file, copied under src/<ProjectName>/… preserving the path relative to its project. Files outside their project directory (e.g. <Link> items) go under _linked/<hash>/; materialized generator output goes under _generated/.

.gitignore

A .gitignore is written into the output root so the slice behaves like a normal, self-contained repo (build artifacts under bin//obj/ stay untracked). It propagates the analyzed solution's own .gitignore (the nearest one found walking up from the .sln); if none exists, a minimal .NET default is written instead.

chisel.log — the run log

A full, timestamped copy of the run (every phase, diagnostic, and the final summary) is written to <output>/chisel.log via Serilog, reset on each run. The console shows the same information with a clean layout; --quiet restricts the console to warnings/errors while the log file still captures everything, and --verbose lists every diagnostic instead of grouping them by stage.


How it works

chisel is a pipeline over Roslyn's semantic model (not text search). It opens the solution with MSBuildWorkspace, classifies which assemblies belong to the codebase, resolves the seed type, walks the dependency graph, collects the contributing files + settings, and emits the slice.

src/Chisel.Core/ library — all slicing logic
src/Chisel.Cli/ thin console host (chisel)
tests/Chisel.Core.Tests/ xUnit tests
tests/Fixtures/ worked-example solutions (also used as tests)

For the full walk-through — every stage, the body/signature distinction, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ — see docs/GUIDE.md.


Error handling

chisel is best-effort: it would rather hand you a mostly-complete slice than nothing. A problem with one file, symbol, or reference is reported and skipped — it does not abort the run. Diagnostics are streamed to stderr as they happen and recapped in an end-of-run summary:

Diagnostics: 0 error(s), 1 warning(s) — the slice was still produced.
[Warning] TargetFramework: Project 'MultiTarget.csproj' multi-targets (net8.0, net10.0); slicing against 'net8.0'. Pass --tfm to choose.

Only four conditions are fatal (there is genuinely nothing to produce): no .NET SDK / MSBuild installed (exit 7), a missing solution file (exit 5), a workspace that fails to load without --allow-partial (exit 4), and an unresolvable/ambiguous seed type (exit 3). Everything else — a file that won't bind, a reference that won't resolve, a file that can't be copied — becomes a non-fatal diagnostic and the run still exits 0 with the slice written.

Failing to open a non-C# project (.proj, .vcxproj, .fsproj, .vbproj, …) is not fatal even without --allow-partial: those projects hold no C# to collect, so the failure is reported as a warning and the C# projects load normally.


Known limitations

  • dynamic and reflection-by-string (Type.GetType("…"), DI string registrations) are not statically traceable and are not followed; a warning is emitted when dynamic is encountered.
  • Single target framework per run. Multi-targeted projects are sliced against one TFM (--tfm to choose); code in #if regions for other TFMs is preserved in the copied file but not analyzed.
  • Source generators default to reference (the generated files are skipped and a warning tells you the generator must run downstream). Use --source-generators materialize to write the generated code into the slice for a self-contained result. See the Guide.
  • file-scoped types cannot be used as the seed (they have no addressable metadata name).

Troubleshooting

SymptomFix
No .NET SDK was found (exit 7)Install the .NET 10 SDK and ensure dotnet is on your PATH. A self-contained binary still needs it.
Type ... not foundCheck the FQN and arity (Foo<> not Foo); ensure the solution is restored.
... is ambiguous (exit 3)Pass --project <name> to pick the declaring project.
Workspace load failure (exit 4)dotnet restore the solution; if one project is broken, try --allow-partial.
Slice misses a type referenced via dynamic/reflectionExpected — add it manually (see limitations).
Slice won't compile due to a missing generated typeRe-run with --source-generators materialize.

Documentation

DocumentPurpose
docs/GUIDE.mdThe deep dive: how the walk works, every output format, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ.
CONTRIBUTING.mdDev setup, the test-fixture step, coding conventions, and the PR checklist.
LICENSEMIT license text.

Contributing

Contributions welcome — see CONTRIBUTING.md for the full guide (dev setup, the test-fixture step, coding conventions, and the PR checklist). Please open an issue before submitting a pull request for non-trivial changes.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes, including tests
  4. Prepare the test fixtures, then run the suite:
    pwsh build/restore-test-fixtures.ps1 # restores the fixture solutions + builds the SourceGen generator
    dotnet test
  5. Submit a pull request

The example solutions under tests/Fixtures/ are not part of Chisel.slnx, so they must be restored before dotnet test (the restore-test-fixtures.ps1 helper does this). See docs/GUIDE.md for the developer guide.

If you find this tool useful, I accept tips / donations:

❤️ ~B Sponsor


License

MIT © 2026 Brian Bennewitz — see LICENSE.

About

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

𓍋 Chisel

CIReleaseLatest releaseNuGetLicense: MIT.NET 10Sponsor

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Given a fully-qualified type name and a .sln, chisel walks the Roslyn semantic graph starting from that type and pulls in every .cs file in the codebase required to compile it — across project boundaries. Code that lives outside the codebase (the BCL, NuGet packages) is treated as a leaf: it is never vendored in, but every external assembly / NuGet package the slice touches is recorded so a follow-on step can resolve references.

flowchart LR
A["Seed type<br/>e.g. MyNS.IFoo"] --> B["TypeResolver"]
B -->|"walk the semantic graph:<br/>base · interfaces · members · bodies<br/><i>(in-source only)</i>"| C["The slice<br/>(.cs files)"]
B --> D["External refs<br/><i>(BCL / NuGet — recorded as leaves)</i>"]
Loading

📖 New here? The Guide is the deep dive — the dependency walk in detail, every output format, source generators, multi-targeting, global usings, and embedding the Core library.


What it carves out

By default the walk keeps the declared shape of the seed and its implementations — the contract and the data types — but not what method bodies use. Starting from the seed type it follows:

  • Base types, interfaces (including inherited ones), and generic constraints / type arguments.
  • Member signatures — field/property/event types, method return & parameter types, indexer parameters, nested types, and the enclosing type chain of a nested seed.
  • Attributes — the attribute class plus any typeof(SomeType) in constructor or named arguments (including arrays of typeof).
  • Seed implementations — when the seed is an interface or abstract class, its concrete implementations (and, for an interface seed, the base interfaces it derives from) are pulled in. Interfaces/classes encountered deeper in the graph (e.g. a property's type) are included as declarations only — their implementations are not expanded.
  • Authored global using files — dedicated global-usings files are kept even though they declare no types.

What it deliberately leaves behind by default: method-body usages (what the kept types call or instantiate) and implementations of interfaces reached deep in the graph. This keeps the slice a focused "contract + shape" extract rather than the whole reachable call graph.

Two knobs widen the walk:

  • --walk-depth bodies — also follow every type referenced inside method bodies, transitively. Produces a self-compilable slice, but pulls in far more. (Default signatures does not, so a default slice may not compile standalone — body-referenced in-source types are treated as external.)
  • --expand-impls all — expand every interface/class reached to its implementations, not just the seed. (--expand-impls none, aliased --no-derived, never expands.)

Anything whose containing assembly is not one of the solution's projects (BCL, NuGet) is a leaf: it is recorded in references.json and as a <PackageReference>, but its source is never pulled into the slice.


Requirements

  • .NET 10 SDK (pinned via global.json to 10.0.300).
  • The target solution should be restored so package metadata resolves — either run dotnet restore yourself, or pass --restore to have chisel do it first.

chisel loads and evaluates your solution with MSBuild, which ships with the .NET SDK. The SDK is therefore required at run time, not just to build chisel — see the note under pre-built binaries.


Install

As a .NET tool (recommended)

chisel is packaged as a .NET tool, so it installs by name and is invoked as dotnet chisel …:

dotnet tool install --global Bennewitz.Ninja.Chisel
dotnet chisel --version

Pre-built binaries

Each release also publishes self-contained, per-platform archives:

PlatformFile
Windows (x64)chisel-<version>-win-x64.zip
Windows (ARM64)chisel-<version>-win-arm64.zip
Linux (x64)chisel-<version>-linux-x64.tar.gz
Linux (ARM64)chisel-<version>-linux-arm64.tar.gz
macOS (Intel)chisel-<version>-osx-x64.tar.gz
macOS (Apple Silicon)chisel-<version>-osx-arm64.tar.gz

These binaries still require a .NET 10 SDK on the machine. Unlike a typical self-contained app, chisel locates and drives the installed SDK's MSBuild at run time (via MSBuildLocator); the Microsoft.Build.* engine assemblies are deliberately not bundled. The archives only save you the dotnet tool install step. Since any machine that can build the solution you're slicing already has the SDK, the .NET tool above is the recommended path. If no SDK is found, chisel exits 7 with an actionable message.

Build from source

git clone https://github.com/JanusMael/chisel.git
cd chisel
dotnet build
dotnet run --project src/Chisel.Cli -- --help

To install your local build as a tool:

dotnet pack src/Chisel.Cli -c Release -o ./nupkg
dotnet tool install --global --add-source ./nupkg --prerelease Bennewitz.Ninja.Chisel
dotnet chisel --version

Quick start

dotnet chisel \
--type MyNS.IFoo \
--solution path/to/MySolution.sln \
--output ./out

This writes the artifacts into ./out (see Outputs) and prints a summary:

Seed type: global::MyNS.IFoo
Files: 6
External refs: 3 (1 NuGet packages)
Projects: 4
files.json → .../out/files.json
references.json → .../out/references.json
Slice.csproj → .../out/Slice.csproj
copied sources → .../out/src
.gitignore → .../out/.gitignore

To build the extracted slice on its own:

dotnet build ./out/Slice.csproj

Command-line reference

dotnet chisel --type <FQN> --solution <path.sln> --output <dir> [options]

Required

FlagDescription
--type, -t <FQN>Fully-qualified type name. See type-name formats.
--solution, -s <path>Path to the .sln / .slnx file.
--output, -o <dir>Output directory (created if missing).

Options

FlagDefaultDescription
--project <name>Disambiguate when the FQN matches types in multiple projects.
--tfm <name>first TFMPreferred target framework when a project multi-targets.
--walk-depth <d>signaturessignatures (declared shape only) | bodies (also follow method-body usages transitively — self-compilable, larger).
--expand-impls <s>seedseed (expand only the seed + an interface seed's base interfaces) | all (every interface/class reached) | none.
--no-derivedAlias for --expand-impls none.
--source-generators <p>referenceskip | materialize | reference — how to treat generator output.
--exclude, -x <path>Directory subtree to drop from the slice (repeatable). Any collected file under <path> is logged and left out — handy for vendored/generated/out-of-scope regions. The slice may then be incomplete; each drop is reported as an Exclude warning.
--exclude-from <file>Read exclusion directories from a file, one path per line (repeatable; merged with --exclude). Use - to read from stdin ($paths | chisel --exclude-from -). Blank lines and # comments are ignored; relative paths resolve against the file's directory (the working directory for stdin).
--allow-partialoffContinue when MSBuild reports project-load failures (otherwise fail fast).
--restoreoffRun dotnet restore on the solution before analyzing (best-effort; a failed restore warns and continues).
--format <f>texttext (human summary on stdout) | json (the run manifest on stdout). result.json is always written to --output regardless.
--strictoffExit nonzero (6) if any error-severity diagnostic occurred (default stays 0 on a best-effort run).
--verbose, -vList every diagnostic instead of grouping by stage.
--quiet, -qConsole shows only warnings/errors; the full run log is still written.
--no-colorDisable ANSI color (also honored via the NO_COLOR env var).
--version, -VPrint version and exit.
-h, --helpShow usage.

Type-name formats

You typeResolves to
MyNS.Widgetthe non-generic Widget
MyNS.Repository<T> or MyNS.Repository<>the open generic Repository<T> (arity 1)
MyNS.Map<,>the open generic with arity 2
MyNS.Outer.Inner or MyNS.Outer+Innerthe nested type Inner

Exit codes

CodeMeaning
0Success
1No arguments (usage printed)
2Invalid arguments
3Type not found / ambiguous (pass --project)
4Workspace failed to load (try --allow-partial)
5Solution file not found
6Completed with error-severity diagnostics (only under --strict)
7No .NET SDK / MSBuild found (install the .NET 10 SDK — see Requirements)
130Canceled (Ctrl+C)

Scripting (PowerShell)

Designed for PowerShell Core 7+. Streams are split for clean capture — stdout carries the result (the text summary, or the manifest under --format json), stderr carries progress/diagnostics — and a stable result.json is always written. Read it with fully-qualified .NET (no cmdlets, no piping):

& dotnet chisel -t 'Contracts.IShape'-s $sln-o $out--strict
if ($LASTEXITCODE-ne0) { throw"chisel failed ($LASTEXITCODE)" }
$doc= [System.Text.Json.JsonDocument]::Parse(
[System.IO.File]::ReadAllText([System.IO.Path]::Combine($out,'result.json')))
$root=$doc.RootElement$root.GetProperty('counts').GetProperty('files').GetInt32()
foreach ($pin$root.GetProperty('packages').EnumerateArray()) {
"$($p.GetProperty('id').GetString())$($p.GetProperty('version').GetString())"
}

Or capture the manifest straight off stdout: $json = & dotnet chisel … --format json (progress still shows on stderr).

Generic types in PowerShell: single-quote any type name containing <, >, or a backtick — 'MyNS.Repository<T>' or 'MyNS.Repository`1' (both resolve). < is a reserved PS operator and the backtick is the PS escape char; single quotes pass them through literally.

See Exit codes to branch on $LASTEXITCODE; --strict turns any error-severity diagnostic into a nonzero exit.


Outputs

All of these are written into --output.

Slice.csproj — a flat, buildable project

Explicit <Compile Include> per collected file (no ProjectReferences — the slice is flattened), plus a <PackageReference> per detected NuGet package. Compilation settings (TargetFramework, LangVersion, Nullable, ImplicitUsings, AllowUnsafeBlocks, user DefineConstants) are hoisted from the contributing projects — taking the highest/strictest value when projects disagree, and warning you when they do.

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<CompileInclude="src/ExternalPackage/MyClass.cs" />
</ItemGroup>
<ItemGroup>
<PackageReferenceInclude="newtonsoft.json"Version="13.0.3" />
</ItemGroup>
</Project>

files.json — the file manifest

{
"files": [
{
"path": "C:\\...\\Shapes\\Composite\\Group.cs",
"project": "Composite",
"targetFramework": "net10.0",
"isGenerated": false,
"containsSymbols": [ "global::Composite.Group" ]
}
]
}

references.json — external references (the follow-on step's input)

NuGet packages and framework assemblies the slice depends on but does not vendor:

{
"packages": [
{
"id": "newtonsoft.json",
"version": "13.0.3",
"assemblyName": "Newtonsoft.Json",
"assemblyVersion": "13.0.0.0"
}
],
"frameworkAssemblies": [
{
"name": "System.Runtime",
"version": "10.0.0.0",
"path": "C:\\Program Files\\dotnet\\packs\\Microsoft.NETCore.App.Ref\\...\\System.Runtime.dll"
}
]
}

result.json — the machine-readable run manifest

Written on every run (success or fatal), and also printed to stdout under --format json. A single, stable, camelCase object — success flag, exit code, seed, mode, counts, the output paths, the NuGet packages, and the full diagnostics list — designed to be read directly with System.Text.Json (see Scripting).

{
"schemaVersion": 1,
"tool": { "name": "chisel", "version": "2026.2.624" },
"success": true, "exitCode": 0, "elapsedSeconds": 3.1,
"seed": { "displayName": "global::MyNS.IFoo", "filePath": ".../IFoo.cs" },
"mode": { "walkDepth": "signatures", "expansion": "seed", "sourceGenerators": "reference" },
"counts": { "inSourceTypes": 7, "files": 6, "projects": 4, "externalReferences": 3, "packages": 1 },
"packages": [ { "id": "newtonsoft.json", "version": "13.0.3" } ],
"diagnostics": [ { "severity": "Warning", "stage": "Walk", "message": "", "item": "" } ]
}

src/… — copied sources

Every collected .cs file, copied under src/<ProjectName>/… preserving the path relative to its project. Files outside their project directory (e.g. <Link> items) go under _linked/<hash>/; materialized generator output goes under _generated/.

.gitignore

A .gitignore is written into the output root so the slice behaves like a normal, self-contained repo (build artifacts under bin//obj/ stay untracked). It propagates the analyzed solution's own .gitignore (the nearest one found walking up from the .sln); if none exists, a minimal .NET default is written instead.

chisel.log — the run log

A full, timestamped copy of the run (every phase, diagnostic, and the final summary) is written to <output>/chisel.log via Serilog, reset on each run. The console shows the same information with a clean layout; --quiet restricts the console to warnings/errors while the log file still captures everything, and --verbose lists every diagnostic instead of grouping them by stage.


How it works

chisel is a pipeline over Roslyn's semantic model (not text search). It opens the solution with MSBuildWorkspace, classifies which assemblies belong to the codebase, resolves the seed type, walks the dependency graph, collects the contributing files + settings, and emits the slice.

src/Chisel.Core/ library — all slicing logic
src/Chisel.Cli/ thin console host (chisel)
tests/Chisel.Core.Tests/ xUnit tests
tests/Fixtures/ worked-example solutions (also used as tests)

For the full walk-through — every stage, the body/signature distinction, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ — see docs/GUIDE.md.


Error handling

chisel is best-effort: it would rather hand you a mostly-complete slice than nothing. A problem with one file, symbol, or reference is reported and skipped — it does not abort the run. Diagnostics are streamed to stderr as they happen and recapped in an end-of-run summary:

Diagnostics: 0 error(s), 1 warning(s) — the slice was still produced.
[Warning] TargetFramework: Project 'MultiTarget.csproj' multi-targets (net8.0, net10.0); slicing against 'net8.0'. Pass --tfm to choose.

Only four conditions are fatal (there is genuinely nothing to produce): no .NET SDK / MSBuild installed (exit 7), a missing solution file (exit 5), a workspace that fails to load without --allow-partial (exit 4), and an unresolvable/ambiguous seed type (exit 3). Everything else — a file that won't bind, a reference that won't resolve, a file that can't be copied — becomes a non-fatal diagnostic and the run still exits 0 with the slice written.

Failing to open a non-C# project (.proj, .vcxproj, .fsproj, .vbproj, …) is not fatal even without --allow-partial: those projects hold no C# to collect, so the failure is reported as a warning and the C# projects load normally.


Known limitations

  • dynamic and reflection-by-string (Type.GetType("…"), DI string registrations) are not statically traceable and are not followed; a warning is emitted when dynamic is encountered.
  • Single target framework per run. Multi-targeted projects are sliced against one TFM (--tfm to choose); code in #if regions for other TFMs is preserved in the copied file but not analyzed.
  • Source generators default to reference (the generated files are skipped and a warning tells you the generator must run downstream). Use --source-generators materialize to write the generated code into the slice for a self-contained result. See the Guide.
  • file-scoped types cannot be used as the seed (they have no addressable metadata name).

Troubleshooting

SymptomFix
No .NET SDK was found (exit 7)Install the .NET 10 SDK and ensure dotnet is on your PATH. A self-contained binary still needs it.
Type ... not foundCheck the FQN and arity (Foo<> not Foo); ensure the solution is restored.
... is ambiguous (exit 3)Pass --project <name> to pick the declaring project.
Workspace load failure (exit 4)dotnet restore the solution; if one project is broken, try --allow-partial.
Slice misses a type referenced via dynamic/reflectionExpected — add it manually (see limitations).
Slice won't compile due to a missing generated typeRe-run with --source-generators materialize.

Documentation

DocumentPurpose
docs/GUIDE.mdThe deep dive: how the walk works, every output format, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ.
CONTRIBUTING.mdDev setup, the test-fixture step, coding conventions, and the PR checklist.
LICENSEMIT license text.

Contributing

Contributions welcome — see CONTRIBUTING.md for the full guide (dev setup, the test-fixture step, coding conventions, and the PR checklist). Please open an issue before submitting a pull request for non-trivial changes.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes, including tests
  4. Prepare the test fixtures, then run the suite:
    pwsh build/restore-test-fixtures.ps1 # restores the fixture solutions + builds the SourceGen generator
    dotnet test
  5. Submit a pull request

The example solutions under tests/Fixtures/ are not part of Chisel.slnx, so they must be restored before dotnet test (the restore-test-fixtures.ps1 helper does this). See docs/GUIDE.md for the developer guide.

If you find this tool useful, I accept tips / donations:

❤️ ~B Sponsor


License

MIT © 2026 Brian Bennewitz — see LICENSE.

About

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

𓍋 Chisel

CIReleaseLatest releaseNuGetLicense: MIT.NET 10Sponsor

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Given a fully-qualified type name and a .sln, chisel walks the Roslyn semantic graph starting from that type and pulls in every .cs file in the codebase required to compile it — across project boundaries. Code that lives outside the codebase (the BCL, NuGet packages) is treated as a leaf: it is never vendored in, but every external assembly / NuGet package the slice touches is recorded so a follow-on step can resolve references.

flowchart LR
A["Seed type<br/>e.g. MyNS.IFoo"] --> B["TypeResolver"]
B -->|"walk the semantic graph:<br/>base · interfaces · members · bodies<br/><i>(in-source only)</i>"| C["The slice<br/>(.cs files)"]
B --> D["External refs<br/><i>(BCL / NuGet — recorded as leaves)</i>"]
Loading

📖 New here? The Guide is the deep dive — the dependency walk in detail, every output format, source generators, multi-targeting, global usings, and embedding the Core library.


What it carves out

By default the walk keeps the declared shape of the seed and its implementations — the contract and the data types — but not what method bodies use. Starting from the seed type it follows:

  • Base types, interfaces (including inherited ones), and generic constraints / type arguments.
  • Member signatures — field/property/event types, method return & parameter types, indexer parameters, nested types, and the enclosing type chain of a nested seed.
  • Attributes — the attribute class plus any typeof(SomeType) in constructor or named arguments (including arrays of typeof).
  • Seed implementations — when the seed is an interface or abstract class, its concrete implementations (and, for an interface seed, the base interfaces it derives from) are pulled in. Interfaces/classes encountered deeper in the graph (e.g. a property's type) are included as declarations only — their implementations are not expanded.
  • Authored global using files — dedicated global-usings files are kept even though they declare no types.

What it deliberately leaves behind by default: method-body usages (what the kept types call or instantiate) and implementations of interfaces reached deep in the graph. This keeps the slice a focused "contract + shape" extract rather than the whole reachable call graph.

Two knobs widen the walk:

  • --walk-depth bodies — also follow every type referenced inside method bodies, transitively. Produces a self-compilable slice, but pulls in far more. (Default signatures does not, so a default slice may not compile standalone — body-referenced in-source types are treated as external.)
  • --expand-impls all — expand every interface/class reached to its implementations, not just the seed. (--expand-impls none, aliased --no-derived, never expands.)

Anything whose containing assembly is not one of the solution's projects (BCL, NuGet) is a leaf: it is recorded in references.json and as a <PackageReference>, but its source is never pulled into the slice.


Requirements

  • .NET 10 SDK (pinned via global.json to 10.0.300).
  • The target solution should be restored so package metadata resolves — either run dotnet restore yourself, or pass --restore to have chisel do it first.

chisel loads and evaluates your solution with MSBuild, which ships with the .NET SDK. The SDK is therefore required at run time, not just to build chisel — see the note under pre-built binaries.


Install

As a .NET tool (recommended)

chisel is packaged as a .NET tool, so it installs by name and is invoked as dotnet chisel …:

dotnet tool install --global Bennewitz.Ninja.Chisel
dotnet chisel --version

Pre-built binaries

Each release also publishes self-contained, per-platform archives:

PlatformFile
Windows (x64)chisel-<version>-win-x64.zip
Windows (ARM64)chisel-<version>-win-arm64.zip
Linux (x64)chisel-<version>-linux-x64.tar.gz
Linux (ARM64)chisel-<version>-linux-arm64.tar.gz
macOS (Intel)chisel-<version>-osx-x64.tar.gz
macOS (Apple Silicon)chisel-<version>-osx-arm64.tar.gz

These binaries still require a .NET 10 SDK on the machine. Unlike a typical self-contained app, chisel locates and drives the installed SDK's MSBuild at run time (via MSBuildLocator); the Microsoft.Build.* engine assemblies are deliberately not bundled. The archives only save you the dotnet tool install step. Since any machine that can build the solution you're slicing already has the SDK, the .NET tool above is the recommended path. If no SDK is found, chisel exits 7 with an actionable message.

Build from source

git clone https://github.com/JanusMael/chisel.git
cd chisel
dotnet build
dotnet run --project src/Chisel.Cli -- --help

To install your local build as a tool:

dotnet pack src/Chisel.Cli -c Release -o ./nupkg
dotnet tool install --global --add-source ./nupkg --prerelease Bennewitz.Ninja.Chisel
dotnet chisel --version

Quick start

dotnet chisel \
--type MyNS.IFoo \
--solution path/to/MySolution.sln \
--output ./out

This writes the artifacts into ./out (see Outputs) and prints a summary:

Seed type: global::MyNS.IFoo
Files: 6
External refs: 3 (1 NuGet packages)
Projects: 4
files.json → .../out/files.json
references.json → .../out/references.json
Slice.csproj → .../out/Slice.csproj
copied sources → .../out/src
.gitignore → .../out/.gitignore

To build the extracted slice on its own:

dotnet build ./out/Slice.csproj

Command-line reference

dotnet chisel --type <FQN> --solution <path.sln> --output <dir> [options]

Required

FlagDescription
--type, -t <FQN>Fully-qualified type name. See type-name formats.
--solution, -s <path>Path to the .sln / .slnx file.
--output, -o <dir>Output directory (created if missing).

Options

FlagDefaultDescription
--project <name>Disambiguate when the FQN matches types in multiple projects.
--tfm <name>first TFMPreferred target framework when a project multi-targets.
--walk-depth <d>signaturessignatures (declared shape only) | bodies (also follow method-body usages transitively — self-compilable, larger).
--expand-impls <s>seedseed (expand only the seed + an interface seed's base interfaces) | all (every interface/class reached) | none.
--no-derivedAlias for --expand-impls none.
--source-generators <p>referenceskip | materialize | reference — how to treat generator output.
--exclude, -x <path>Directory subtree to drop from the slice (repeatable). Any collected file under <path> is logged and left out — handy for vendored/generated/out-of-scope regions. The slice may then be incomplete; each drop is reported as an Exclude warning.
--exclude-from <file>Read exclusion directories from a file, one path per line (repeatable; merged with --exclude). Use - to read from stdin ($paths | chisel --exclude-from -). Blank lines and # comments are ignored; relative paths resolve against the file's directory (the working directory for stdin).
--allow-partialoffContinue when MSBuild reports project-load failures (otherwise fail fast).
--restoreoffRun dotnet restore on the solution before analyzing (best-effort; a failed restore warns and continues).
--format <f>texttext (human summary on stdout) | json (the run manifest on stdout). result.json is always written to --output regardless.
--strictoffExit nonzero (6) if any error-severity diagnostic occurred (default stays 0 on a best-effort run).
--verbose, -vList every diagnostic instead of grouping by stage.
--quiet, -qConsole shows only warnings/errors; the full run log is still written.
--no-colorDisable ANSI color (also honored via the NO_COLOR env var).
--version, -VPrint version and exit.
-h, --helpShow usage.

Type-name formats

You typeResolves to
MyNS.Widgetthe non-generic Widget
MyNS.Repository<T> or MyNS.Repository<>the open generic Repository<T> (arity 1)
MyNS.Map<,>the open generic with arity 2
MyNS.Outer.Inner or MyNS.Outer+Innerthe nested type Inner

Exit codes

CodeMeaning
0Success
1No arguments (usage printed)
2Invalid arguments
3Type not found / ambiguous (pass --project)
4Workspace failed to load (try --allow-partial)
5Solution file not found
6Completed with error-severity diagnostics (only under --strict)
7No .NET SDK / MSBuild found (install the .NET 10 SDK — see Requirements)
130Canceled (Ctrl+C)

Scripting (PowerShell)

Designed for PowerShell Core 7+. Streams are split for clean capture — stdout carries the result (the text summary, or the manifest under --format json), stderr carries progress/diagnostics — and a stable result.json is always written. Read it with fully-qualified .NET (no cmdlets, no piping):

& dotnet chisel -t 'Contracts.IShape'-s $sln-o $out--strict
if ($LASTEXITCODE-ne0) { throw"chisel failed ($LASTEXITCODE)" }
$doc= [System.Text.Json.JsonDocument]::Parse(
[System.IO.File]::ReadAllText([System.IO.Path]::Combine($out,'result.json')))
$root=$doc.RootElement$root.GetProperty('counts').GetProperty('files').GetInt32()
foreach ($pin$root.GetProperty('packages').EnumerateArray()) {
"$($p.GetProperty('id').GetString())$($p.GetProperty('version').GetString())"
}

Or capture the manifest straight off stdout: $json = & dotnet chisel … --format json (progress still shows on stderr).

Generic types in PowerShell: single-quote any type name containing <, >, or a backtick — 'MyNS.Repository<T>' or 'MyNS.Repository`1' (both resolve). < is a reserved PS operator and the backtick is the PS escape char; single quotes pass them through literally.

See Exit codes to branch on $LASTEXITCODE; --strict turns any error-severity diagnostic into a nonzero exit.


Outputs

All of these are written into --output.

Slice.csproj — a flat, buildable project

Explicit <Compile Include> per collected file (no ProjectReferences — the slice is flattened), plus a <PackageReference> per detected NuGet package. Compilation settings (TargetFramework, LangVersion, Nullable, ImplicitUsings, AllowUnsafeBlocks, user DefineConstants) are hoisted from the contributing projects — taking the highest/strictest value when projects disagree, and warning you when they do.

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<CompileInclude="src/ExternalPackage/MyClass.cs" />
</ItemGroup>
<ItemGroup>
<PackageReferenceInclude="newtonsoft.json"Version="13.0.3" />
</ItemGroup>
</Project>

files.json — the file manifest

{
"files": [
{
"path": "C:\\...\\Shapes\\Composite\\Group.cs",
"project": "Composite",
"targetFramework": "net10.0",
"isGenerated": false,
"containsSymbols": [ "global::Composite.Group" ]
}
]
}

references.json — external references (the follow-on step's input)

NuGet packages and framework assemblies the slice depends on but does not vendor:

{
"packages": [
{
"id": "newtonsoft.json",
"version": "13.0.3",
"assemblyName": "Newtonsoft.Json",
"assemblyVersion": "13.0.0.0"
}
],
"frameworkAssemblies": [
{
"name": "System.Runtime",
"version": "10.0.0.0",
"path": "C:\\Program Files\\dotnet\\packs\\Microsoft.NETCore.App.Ref\\...\\System.Runtime.dll"
}
]
}

result.json — the machine-readable run manifest

Written on every run (success or fatal), and also printed to stdout under --format json. A single, stable, camelCase object — success flag, exit code, seed, mode, counts, the output paths, the NuGet packages, and the full diagnostics list — designed to be read directly with System.Text.Json (see Scripting).

{
"schemaVersion": 1,
"tool": { "name": "chisel", "version": "2026.2.624" },
"success": true, "exitCode": 0, "elapsedSeconds": 3.1,
"seed": { "displayName": "global::MyNS.IFoo", "filePath": ".../IFoo.cs" },
"mode": { "walkDepth": "signatures", "expansion": "seed", "sourceGenerators": "reference" },
"counts": { "inSourceTypes": 7, "files": 6, "projects": 4, "externalReferences": 3, "packages": 1 },
"packages": [ { "id": "newtonsoft.json", "version": "13.0.3" } ],
"diagnostics": [ { "severity": "Warning", "stage": "Walk", "message": "", "item": "" } ]
}

src/… — copied sources

Every collected .cs file, copied under src/<ProjectName>/… preserving the path relative to its project. Files outside their project directory (e.g. <Link> items) go under _linked/<hash>/; materialized generator output goes under _generated/.

.gitignore

A .gitignore is written into the output root so the slice behaves like a normal, self-contained repo (build artifacts under bin//obj/ stay untracked). It propagates the analyzed solution's own .gitignore (the nearest one found walking up from the .sln); if none exists, a minimal .NET default is written instead.

chisel.log — the run log

A full, timestamped copy of the run (every phase, diagnostic, and the final summary) is written to <output>/chisel.log via Serilog, reset on each run. The console shows the same information with a clean layout; --quiet restricts the console to warnings/errors while the log file still captures everything, and --verbose lists every diagnostic instead of grouping them by stage.


How it works

chisel is a pipeline over Roslyn's semantic model (not text search). It opens the solution with MSBuildWorkspace, classifies which assemblies belong to the codebase, resolves the seed type, walks the dependency graph, collects the contributing files + settings, and emits the slice.

src/Chisel.Core/ library — all slicing logic
src/Chisel.Cli/ thin console host (chisel)
tests/Chisel.Core.Tests/ xUnit tests
tests/Fixtures/ worked-example solutions (also used as tests)

For the full walk-through — every stage, the body/signature distinction, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ — see docs/GUIDE.md.


Error handling

chisel is best-effort: it would rather hand you a mostly-complete slice than nothing. A problem with one file, symbol, or reference is reported and skipped — it does not abort the run. Diagnostics are streamed to stderr as they happen and recapped in an end-of-run summary:

Diagnostics: 0 error(s), 1 warning(s) — the slice was still produced.
[Warning] TargetFramework: Project 'MultiTarget.csproj' multi-targets (net8.0, net10.0); slicing against 'net8.0'. Pass --tfm to choose.

Only four conditions are fatal (there is genuinely nothing to produce): no .NET SDK / MSBuild installed (exit 7), a missing solution file (exit 5), a workspace that fails to load without --allow-partial (exit 4), and an unresolvable/ambiguous seed type (exit 3). Everything else — a file that won't bind, a reference that won't resolve, a file that can't be copied — becomes a non-fatal diagnostic and the run still exits 0 with the slice written.

Failing to open a non-C# project (.proj, .vcxproj, .fsproj, .vbproj, …) is not fatal even without --allow-partial: those projects hold no C# to collect, so the failure is reported as a warning and the C# projects load normally.


Known limitations

  • dynamic and reflection-by-string (Type.GetType("…"), DI string registrations) are not statically traceable and are not followed; a warning is emitted when dynamic is encountered.
  • Single target framework per run. Multi-targeted projects are sliced against one TFM (--tfm to choose); code in #if regions for other TFMs is preserved in the copied file but not analyzed.
  • Source generators default to reference (the generated files are skipped and a warning tells you the generator must run downstream). Use --source-generators materialize to write the generated code into the slice for a self-contained result. See the Guide.
  • file-scoped types cannot be used as the seed (they have no addressable metadata name).

Troubleshooting

SymptomFix
No .NET SDK was found (exit 7)Install the .NET 10 SDK and ensure dotnet is on your PATH. A self-contained binary still needs it.
Type ... not foundCheck the FQN and arity (Foo<> not Foo); ensure the solution is restored.
... is ambiguous (exit 3)Pass --project <name> to pick the declaring project.
Workspace load failure (exit 4)dotnet restore the solution; if one project is broken, try --allow-partial.
Slice misses a type referenced via dynamic/reflectionExpected — add it manually (see limitations).
Slice won't compile due to a missing generated typeRe-run with --source-generators materialize.

Documentation

DocumentPurpose
docs/GUIDE.mdThe deep dive: how the walk works, every output format, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ.
CONTRIBUTING.mdDev setup, the test-fixture step, coding conventions, and the PR checklist.
LICENSEMIT license text.

Contributing

Contributions welcome — see CONTRIBUTING.md for the full guide (dev setup, the test-fixture step, coding conventions, and the PR checklist). Please open an issue before submitting a pull request for non-trivial changes.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes, including tests
  4. Prepare the test fixtures, then run the suite:
    pwsh build/restore-test-fixtures.ps1 # restores the fixture solutions + builds the SourceGen generator
    dotnet test
  5. Submit a pull request

The example solutions under tests/Fixtures/ are not part of Chisel.slnx, so they must be restored before dotnet test (the restore-test-fixtures.ps1 helper does this). See docs/GUIDE.md for the developer guide.

If you find this tool useful, I accept tips / donations:

❤️ ~B Sponsor


License

MIT © 2026 Brian Bennewitz — see LICENSE.

About

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

𓍋 Chisel

CIReleaseLatest releaseNuGetLicense: MIT.NET 10Sponsor

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Given a fully-qualified type name and a .sln, chisel walks the Roslyn semantic graph starting from that type and pulls in every .cs file in the codebase required to compile it — across project boundaries. Code that lives outside the codebase (the BCL, NuGet packages) is treated as a leaf: it is never vendored in, but every external assembly / NuGet package the slice touches is recorded so a follow-on step can resolve references.

flowchart LR
A["Seed type<br/>e.g. MyNS.IFoo"] --> B["TypeResolver"]
B -->|"walk the semantic graph:<br/>base · interfaces · members · bodies<br/><i>(in-source only)</i>"| C["The slice<br/>(.cs files)"]
B --> D["External refs<br/><i>(BCL / NuGet — recorded as leaves)</i>"]
Loading

📖 New here? The Guide is the deep dive — the dependency walk in detail, every output format, source generators, multi-targeting, global usings, and embedding the Core library.


What it carves out

By default the walk keeps the declared shape of the seed and its implementations — the contract and the data types — but not what method bodies use. Starting from the seed type it follows:

  • Base types, interfaces (including inherited ones), and generic constraints / type arguments.
  • Member signatures — field/property/event types, method return & parameter types, indexer parameters, nested types, and the enclosing type chain of a nested seed.
  • Attributes — the attribute class plus any typeof(SomeType) in constructor or named arguments (including arrays of typeof).
  • Seed implementations — when the seed is an interface or abstract class, its concrete implementations (and, for an interface seed, the base interfaces it derives from) are pulled in. Interfaces/classes encountered deeper in the graph (e.g. a property's type) are included as declarations only — their implementations are not expanded.
  • Authored global using files — dedicated global-usings files are kept even though they declare no types.

What it deliberately leaves behind by default: method-body usages (what the kept types call or instantiate) and implementations of interfaces reached deep in the graph. This keeps the slice a focused "contract + shape" extract rather than the whole reachable call graph.

Two knobs widen the walk:

  • --walk-depth bodies — also follow every type referenced inside method bodies, transitively. Produces a self-compilable slice, but pulls in far more. (Default signatures does not, so a default slice may not compile standalone — body-referenced in-source types are treated as external.)
  • --expand-impls all — expand every interface/class reached to its implementations, not just the seed. (--expand-impls none, aliased --no-derived, never expands.)

Anything whose containing assembly is not one of the solution's projects (BCL, NuGet) is a leaf: it is recorded in references.json and as a <PackageReference>, but its source is never pulled into the slice.


Requirements

  • .NET 10 SDK (pinned via global.json to 10.0.300).
  • The target solution should be restored so package metadata resolves — either run dotnet restore yourself, or pass --restore to have chisel do it first.

chisel loads and evaluates your solution with MSBuild, which ships with the .NET SDK. The SDK is therefore required at run time, not just to build chisel — see the note under pre-built binaries.


Install

As a .NET tool (recommended)

chisel is packaged as a .NET tool, so it installs by name and is invoked as dotnet chisel …:

dotnet tool install --global Bennewitz.Ninja.Chisel
dotnet chisel --version

Pre-built binaries

Each release also publishes self-contained, per-platform archives:

PlatformFile
Windows (x64)chisel-<version>-win-x64.zip
Windows (ARM64)chisel-<version>-win-arm64.zip
Linux (x64)chisel-<version>-linux-x64.tar.gz
Linux (ARM64)chisel-<version>-linux-arm64.tar.gz
macOS (Intel)chisel-<version>-osx-x64.tar.gz
macOS (Apple Silicon)chisel-<version>-osx-arm64.tar.gz

These binaries still require a .NET 10 SDK on the machine. Unlike a typical self-contained app, chisel locates and drives the installed SDK's MSBuild at run time (via MSBuildLocator); the Microsoft.Build.* engine assemblies are deliberately not bundled. The archives only save you the dotnet tool install step. Since any machine that can build the solution you're slicing already has the SDK, the .NET tool above is the recommended path. If no SDK is found, chisel exits 7 with an actionable message.

Build from source

git clone https://github.com/JanusMael/chisel.git
cd chisel
dotnet build
dotnet run --project src/Chisel.Cli -- --help

To install your local build as a tool:

dotnet pack src/Chisel.Cli -c Release -o ./nupkg
dotnet tool install --global --add-source ./nupkg --prerelease Bennewitz.Ninja.Chisel
dotnet chisel --version

Quick start

dotnet chisel \
--type MyNS.IFoo \
--solution path/to/MySolution.sln \
--output ./out

This writes the artifacts into ./out (see Outputs) and prints a summary:

Seed type: global::MyNS.IFoo
Files: 6
External refs: 3 (1 NuGet packages)
Projects: 4
files.json → .../out/files.json
references.json → .../out/references.json
Slice.csproj → .../out/Slice.csproj
copied sources → .../out/src
.gitignore → .../out/.gitignore

To build the extracted slice on its own:

dotnet build ./out/Slice.csproj

Command-line reference

dotnet chisel --type <FQN> --solution <path.sln> --output <dir> [options]

Required

FlagDescription
--type, -t <FQN>Fully-qualified type name. See type-name formats.
--solution, -s <path>Path to the .sln / .slnx file.
--output, -o <dir>Output directory (created if missing).

Options

FlagDefaultDescription
--project <name>Disambiguate when the FQN matches types in multiple projects.
--tfm <name>first TFMPreferred target framework when a project multi-targets.
--walk-depth <d>signaturessignatures (declared shape only) | bodies (also follow method-body usages transitively — self-compilable, larger).
--expand-impls <s>seedseed (expand only the seed + an interface seed's base interfaces) | all (every interface/class reached) | none.
--no-derivedAlias for --expand-impls none.
--source-generators <p>referenceskip | materialize | reference — how to treat generator output.
--exclude, -x <path>Directory subtree to drop from the slice (repeatable). Any collected file under <path> is logged and left out — handy for vendored/generated/out-of-scope regions. The slice may then be incomplete; each drop is reported as an Exclude warning.
--exclude-from <file>Read exclusion directories from a file, one path per line (repeatable; merged with --exclude). Use - to read from stdin ($paths | chisel --exclude-from -). Blank lines and # comments are ignored; relative paths resolve against the file's directory (the working directory for stdin).
--allow-partialoffContinue when MSBuild reports project-load failures (otherwise fail fast).
--restoreoffRun dotnet restore on the solution before analyzing (best-effort; a failed restore warns and continues).
--format <f>texttext (human summary on stdout) | json (the run manifest on stdout). result.json is always written to --output regardless.
--strictoffExit nonzero (6) if any error-severity diagnostic occurred (default stays 0 on a best-effort run).
--verbose, -vList every diagnostic instead of grouping by stage.
--quiet, -qConsole shows only warnings/errors; the full run log is still written.
--no-colorDisable ANSI color (also honored via the NO_COLOR env var).
--version, -VPrint version and exit.
-h, --helpShow usage.

Type-name formats

You typeResolves to
MyNS.Widgetthe non-generic Widget
MyNS.Repository<T> or MyNS.Repository<>the open generic Repository<T> (arity 1)
MyNS.Map<,>the open generic with arity 2
MyNS.Outer.Inner or MyNS.Outer+Innerthe nested type Inner

Exit codes

CodeMeaning
0Success
1No arguments (usage printed)
2Invalid arguments
3Type not found / ambiguous (pass --project)
4Workspace failed to load (try --allow-partial)
5Solution file not found
6Completed with error-severity diagnostics (only under --strict)
7No .NET SDK / MSBuild found (install the .NET 10 SDK — see Requirements)
130Canceled (Ctrl+C)

Scripting (PowerShell)

Designed for PowerShell Core 7+. Streams are split for clean capture — stdout carries the result (the text summary, or the manifest under --format json), stderr carries progress/diagnostics — and a stable result.json is always written. Read it with fully-qualified .NET (no cmdlets, no piping):

& dotnet chisel -t 'Contracts.IShape'-s $sln-o $out--strict
if ($LASTEXITCODE-ne0) { throw"chisel failed ($LASTEXITCODE)" }
$doc= [System.Text.Json.JsonDocument]::Parse(
[System.IO.File]::ReadAllText([System.IO.Path]::Combine($out,'result.json')))
$root=$doc.RootElement$root.GetProperty('counts').GetProperty('files').GetInt32()
foreach ($pin$root.GetProperty('packages').EnumerateArray()) {
"$($p.GetProperty('id').GetString())$($p.GetProperty('version').GetString())"
}

Or capture the manifest straight off stdout: $json = & dotnet chisel … --format json (progress still shows on stderr).

Generic types in PowerShell: single-quote any type name containing <, >, or a backtick — 'MyNS.Repository<T>' or 'MyNS.Repository`1' (both resolve). < is a reserved PS operator and the backtick is the PS escape char; single quotes pass them through literally.

See Exit codes to branch on $LASTEXITCODE; --strict turns any error-severity diagnostic into a nonzero exit.


Outputs

All of these are written into --output.

Slice.csproj — a flat, buildable project

Explicit <Compile Include> per collected file (no ProjectReferences — the slice is flattened), plus a <PackageReference> per detected NuGet package. Compilation settings (TargetFramework, LangVersion, Nullable, ImplicitUsings, AllowUnsafeBlocks, user DefineConstants) are hoisted from the contributing projects — taking the highest/strictest value when projects disagree, and warning you when they do.

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<CompileInclude="src/ExternalPackage/MyClass.cs" />
</ItemGroup>
<ItemGroup>
<PackageReferenceInclude="newtonsoft.json"Version="13.0.3" />
</ItemGroup>
</Project>

files.json — the file manifest

{
"files": [
{
"path": "C:\\...\\Shapes\\Composite\\Group.cs",
"project": "Composite",
"targetFramework": "net10.0",
"isGenerated": false,
"containsSymbols": [ "global::Composite.Group" ]
}
]
}

references.json — external references (the follow-on step's input)

NuGet packages and framework assemblies the slice depends on but does not vendor:

{
"packages": [
{
"id": "newtonsoft.json",
"version": "13.0.3",
"assemblyName": "Newtonsoft.Json",
"assemblyVersion": "13.0.0.0"
}
],
"frameworkAssemblies": [
{
"name": "System.Runtime",
"version": "10.0.0.0",
"path": "C:\\Program Files\\dotnet\\packs\\Microsoft.NETCore.App.Ref\\...\\System.Runtime.dll"
}
]
}

result.json — the machine-readable run manifest

Written on every run (success or fatal), and also printed to stdout under --format json. A single, stable, camelCase object — success flag, exit code, seed, mode, counts, the output paths, the NuGet packages, and the full diagnostics list — designed to be read directly with System.Text.Json (see Scripting).

{
"schemaVersion": 1,
"tool": { "name": "chisel", "version": "2026.2.624" },
"success": true, "exitCode": 0, "elapsedSeconds": 3.1,
"seed": { "displayName": "global::MyNS.IFoo", "filePath": ".../IFoo.cs" },
"mode": { "walkDepth": "signatures", "expansion": "seed", "sourceGenerators": "reference" },
"counts": { "inSourceTypes": 7, "files": 6, "projects": 4, "externalReferences": 3, "packages": 1 },
"packages": [ { "id": "newtonsoft.json", "version": "13.0.3" } ],
"diagnostics": [ { "severity": "Warning", "stage": "Walk", "message": "", "item": "" } ]
}

src/… — copied sources

Every collected .cs file, copied under src/<ProjectName>/… preserving the path relative to its project. Files outside their project directory (e.g. <Link> items) go under _linked/<hash>/; materialized generator output goes under _generated/.

.gitignore

A .gitignore is written into the output root so the slice behaves like a normal, self-contained repo (build artifacts under bin//obj/ stay untracked). It propagates the analyzed solution's own .gitignore (the nearest one found walking up from the .sln); if none exists, a minimal .NET default is written instead.

chisel.log — the run log

A full, timestamped copy of the run (every phase, diagnostic, and the final summary) is written to <output>/chisel.log via Serilog, reset on each run. The console shows the same information with a clean layout; --quiet restricts the console to warnings/errors while the log file still captures everything, and --verbose lists every diagnostic instead of grouping them by stage.


How it works

chisel is a pipeline over Roslyn's semantic model (not text search). It opens the solution with MSBuildWorkspace, classifies which assemblies belong to the codebase, resolves the seed type, walks the dependency graph, collects the contributing files + settings, and emits the slice.

src/Chisel.Core/ library — all slicing logic
src/Chisel.Cli/ thin console host (chisel)
tests/Chisel.Core.Tests/ xUnit tests
tests/Fixtures/ worked-example solutions (also used as tests)

For the full walk-through — every stage, the body/signature distinction, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ — see docs/GUIDE.md.


Error handling

chisel is best-effort: it would rather hand you a mostly-complete slice than nothing. A problem with one file, symbol, or reference is reported and skipped — it does not abort the run. Diagnostics are streamed to stderr as they happen and recapped in an end-of-run summary:

Diagnostics: 0 error(s), 1 warning(s) — the slice was still produced.
[Warning] TargetFramework: Project 'MultiTarget.csproj' multi-targets (net8.0, net10.0); slicing against 'net8.0'. Pass --tfm to choose.

Only four conditions are fatal (there is genuinely nothing to produce): no .NET SDK / MSBuild installed (exit 7), a missing solution file (exit 5), a workspace that fails to load without --allow-partial (exit 4), and an unresolvable/ambiguous seed type (exit 3). Everything else — a file that won't bind, a reference that won't resolve, a file that can't be copied — becomes a non-fatal diagnostic and the run still exits 0 with the slice written.

Failing to open a non-C# project (.proj, .vcxproj, .fsproj, .vbproj, …) is not fatal even without --allow-partial: those projects hold no C# to collect, so the failure is reported as a warning and the C# projects load normally.


Known limitations

  • dynamic and reflection-by-string (Type.GetType("…"), DI string registrations) are not statically traceable and are not followed; a warning is emitted when dynamic is encountered.
  • Single target framework per run. Multi-targeted projects are sliced against one TFM (--tfm to choose); code in #if regions for other TFMs is preserved in the copied file but not analyzed.
  • Source generators default to reference (the generated files are skipped and a warning tells you the generator must run downstream). Use --source-generators materialize to write the generated code into the slice for a self-contained result. See the Guide.
  • file-scoped types cannot be used as the seed (they have no addressable metadata name).

Troubleshooting

SymptomFix
No .NET SDK was found (exit 7)Install the .NET 10 SDK and ensure dotnet is on your PATH. A self-contained binary still needs it.
Type ... not foundCheck the FQN and arity (Foo<> not Foo); ensure the solution is restored.
... is ambiguous (exit 3)Pass --project <name> to pick the declaring project.
Workspace load failure (exit 4)dotnet restore the solution; if one project is broken, try --allow-partial.
Slice misses a type referenced via dynamic/reflectionExpected — add it manually (see limitations).
Slice won't compile due to a missing generated typeRe-run with --source-generators materialize.

Documentation

DocumentPurpose
docs/GUIDE.mdThe deep dive: how the walk works, every output format, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ.
CONTRIBUTING.mdDev setup, the test-fixture step, coding conventions, and the PR checklist.
LICENSEMIT license text.

Contributing

Contributions welcome — see CONTRIBUTING.md for the full guide (dev setup, the test-fixture step, coding conventions, and the PR checklist). Please open an issue before submitting a pull request for non-trivial changes.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes, including tests
  4. Prepare the test fixtures, then run the suite:
    pwsh build/restore-test-fixtures.ps1 # restores the fixture solutions + builds the SourceGen generator
    dotnet test
  5. Submit a pull request

The example solutions under tests/Fixtures/ are not part of Chisel.slnx, so they must be restored before dotnet test (the restore-test-fixtures.ps1 helper does this). See docs/GUIDE.md for the developer guide.

If you find this tool useful, I accept tips / donations:

❤️ ~B Sponsor


License

MIT © 2026 Brian Bennewitz — see LICENSE.

About

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

𓍋 Chisel

CIReleaseLatest releaseNuGetLicense: MIT.NET 10Sponsor

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Given a fully-qualified type name and a .sln, chisel walks the Roslyn semantic graph starting from that type and pulls in every .cs file in the codebase required to compile it — across project boundaries. Code that lives outside the codebase (the BCL, NuGet packages) is treated as a leaf: it is never vendored in, but every external assembly / NuGet package the slice touches is recorded so a follow-on step can resolve references.

flowchart LR
A["Seed type<br/>e.g. MyNS.IFoo"] --> B["TypeResolver"]
B -->|"walk the semantic graph:<br/>base · interfaces · members · bodies<br/><i>(in-source only)</i>"| C["The slice<br/>(.cs files)"]
B --> D["External refs<br/><i>(BCL / NuGet — recorded as leaves)</i>"]
Loading

📖 New here? The Guide is the deep dive — the dependency walk in detail, every output format, source generators, multi-targeting, global usings, and embedding the Core library.


What it carves out

By default the walk keeps the declared shape of the seed and its implementations — the contract and the data types — but not what method bodies use. Starting from the seed type it follows:

  • Base types, interfaces (including inherited ones), and generic constraints / type arguments.
  • Member signatures — field/property/event types, method return & parameter types, indexer parameters, nested types, and the enclosing type chain of a nested seed.
  • Attributes — the attribute class plus any typeof(SomeType) in constructor or named arguments (including arrays of typeof).
  • Seed implementations — when the seed is an interface or abstract class, its concrete implementations (and, for an interface seed, the base interfaces it derives from) are pulled in. Interfaces/classes encountered deeper in the graph (e.g. a property's type) are included as declarations only — their implementations are not expanded.
  • Authored global using files — dedicated global-usings files are kept even though they declare no types.

What it deliberately leaves behind by default: method-body usages (what the kept types call or instantiate) and implementations of interfaces reached deep in the graph. This keeps the slice a focused "contract + shape" extract rather than the whole reachable call graph.

Two knobs widen the walk:

  • --walk-depth bodies — also follow every type referenced inside method bodies, transitively. Produces a self-compilable slice, but pulls in far more. (Default signatures does not, so a default slice may not compile standalone — body-referenced in-source types are treated as external.)
  • --expand-impls all — expand every interface/class reached to its implementations, not just the seed. (--expand-impls none, aliased --no-derived, never expands.)

Anything whose containing assembly is not one of the solution's projects (BCL, NuGet) is a leaf: it is recorded in references.json and as a <PackageReference>, but its source is never pulled into the slice.


Requirements

  • .NET 10 SDK (pinned via global.json to 10.0.300).
  • The target solution should be restored so package metadata resolves — either run dotnet restore yourself, or pass --restore to have chisel do it first.

chisel loads and evaluates your solution with MSBuild, which ships with the .NET SDK. The SDK is therefore required at run time, not just to build chisel — see the note under pre-built binaries.


Install

As a .NET tool (recommended)

chisel is packaged as a .NET tool, so it installs by name and is invoked as dotnet chisel …:

dotnet tool install --global Bennewitz.Ninja.Chisel
dotnet chisel --version

Pre-built binaries

Each release also publishes self-contained, per-platform archives:

PlatformFile
Windows (x64)chisel-<version>-win-x64.zip
Windows (ARM64)chisel-<version>-win-arm64.zip
Linux (x64)chisel-<version>-linux-x64.tar.gz
Linux (ARM64)chisel-<version>-linux-arm64.tar.gz
macOS (Intel)chisel-<version>-osx-x64.tar.gz
macOS (Apple Silicon)chisel-<version>-osx-arm64.tar.gz

These binaries still require a .NET 10 SDK on the machine. Unlike a typical self-contained app, chisel locates and drives the installed SDK's MSBuild at run time (via MSBuildLocator); the Microsoft.Build.* engine assemblies are deliberately not bundled. The archives only save you the dotnet tool install step. Since any machine that can build the solution you're slicing already has the SDK, the .NET tool above is the recommended path. If no SDK is found, chisel exits 7 with an actionable message.

Build from source

git clone https://github.com/JanusMael/chisel.git
cd chisel
dotnet build
dotnet run --project src/Chisel.Cli -- --help

To install your local build as a tool:

dotnet pack src/Chisel.Cli -c Release -o ./nupkg
dotnet tool install --global --add-source ./nupkg --prerelease Bennewitz.Ninja.Chisel
dotnet chisel --version

Quick start

dotnet chisel \
--type MyNS.IFoo \
--solution path/to/MySolution.sln \
--output ./out

This writes the artifacts into ./out (see Outputs) and prints a summary:

Seed type: global::MyNS.IFoo
Files: 6
External refs: 3 (1 NuGet packages)
Projects: 4
files.json → .../out/files.json
references.json → .../out/references.json
Slice.csproj → .../out/Slice.csproj
copied sources → .../out/src
.gitignore → .../out/.gitignore

To build the extracted slice on its own:

dotnet build ./out/Slice.csproj

Command-line reference

dotnet chisel --type <FQN> --solution <path.sln> --output <dir> [options]

Required

FlagDescription
--type, -t <FQN>Fully-qualified type name. See type-name formats.
--solution, -s <path>Path to the .sln / .slnx file.
--output, -o <dir>Output directory (created if missing).

Options

FlagDefaultDescription
--project <name>Disambiguate when the FQN matches types in multiple projects.
--tfm <name>first TFMPreferred target framework when a project multi-targets.
--walk-depth <d>signaturessignatures (declared shape only) | bodies (also follow method-body usages transitively — self-compilable, larger).
--expand-impls <s>seedseed (expand only the seed + an interface seed's base interfaces) | all (every interface/class reached) | none.
--no-derivedAlias for --expand-impls none.
--source-generators <p>referenceskip | materialize | reference — how to treat generator output.
--exclude, -x <path>Directory subtree to drop from the slice (repeatable). Any collected file under <path> is logged and left out — handy for vendored/generated/out-of-scope regions. The slice may then be incomplete; each drop is reported as an Exclude warning.
--exclude-from <file>Read exclusion directories from a file, one path per line (repeatable; merged with --exclude). Use - to read from stdin ($paths | chisel --exclude-from -). Blank lines and # comments are ignored; relative paths resolve against the file's directory (the working directory for stdin).
--allow-partialoffContinue when MSBuild reports project-load failures (otherwise fail fast).
--restoreoffRun dotnet restore on the solution before analyzing (best-effort; a failed restore warns and continues).
--format <f>texttext (human summary on stdout) | json (the run manifest on stdout). result.json is always written to --output regardless.
--strictoffExit nonzero (6) if any error-severity diagnostic occurred (default stays 0 on a best-effort run).
--verbose, -vList every diagnostic instead of grouping by stage.
--quiet, -qConsole shows only warnings/errors; the full run log is still written.
--no-colorDisable ANSI color (also honored via the NO_COLOR env var).
--version, -VPrint version and exit.
-h, --helpShow usage.

Type-name formats

You typeResolves to
MyNS.Widgetthe non-generic Widget
MyNS.Repository<T> or MyNS.Repository<>the open generic Repository<T> (arity 1)
MyNS.Map<,>the open generic with arity 2
MyNS.Outer.Inner or MyNS.Outer+Innerthe nested type Inner

Exit codes

CodeMeaning
0Success
1No arguments (usage printed)
2Invalid arguments
3Type not found / ambiguous (pass --project)
4Workspace failed to load (try --allow-partial)
5Solution file not found
6Completed with error-severity diagnostics (only under --strict)
7No .NET SDK / MSBuild found (install the .NET 10 SDK — see Requirements)
130Canceled (Ctrl+C)

Scripting (PowerShell)

Designed for PowerShell Core 7+. Streams are split for clean capture — stdout carries the result (the text summary, or the manifest under --format json), stderr carries progress/diagnostics — and a stable result.json is always written. Read it with fully-qualified .NET (no cmdlets, no piping):

& dotnet chisel -t 'Contracts.IShape'-s $sln-o $out--strict
if ($LASTEXITCODE-ne0) { throw"chisel failed ($LASTEXITCODE)" }
$doc= [System.Text.Json.JsonDocument]::Parse(
[System.IO.File]::ReadAllText([System.IO.Path]::Combine($out,'result.json')))
$root=$doc.RootElement$root.GetProperty('counts').GetProperty('files').GetInt32()
foreach ($pin$root.GetProperty('packages').EnumerateArray()) {
"$($p.GetProperty('id').GetString())$($p.GetProperty('version').GetString())"
}

Or capture the manifest straight off stdout: $json = & dotnet chisel … --format json (progress still shows on stderr).

Generic types in PowerShell: single-quote any type name containing <, >, or a backtick — 'MyNS.Repository<T>' or 'MyNS.Repository`1' (both resolve). < is a reserved PS operator and the backtick is the PS escape char; single quotes pass them through literally.

See Exit codes to branch on $LASTEXITCODE; --strict turns any error-severity diagnostic into a nonzero exit.


Outputs

All of these are written into --output.

Slice.csproj — a flat, buildable project

Explicit <Compile Include> per collected file (no ProjectReferences — the slice is flattened), plus a <PackageReference> per detected NuGet package. Compilation settings (TargetFramework, LangVersion, Nullable, ImplicitUsings, AllowUnsafeBlocks, user DefineConstants) are hoisted from the contributing projects — taking the highest/strictest value when projects disagree, and warning you when they do.

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<CompileInclude="src/ExternalPackage/MyClass.cs" />
</ItemGroup>
<ItemGroup>
<PackageReferenceInclude="newtonsoft.json"Version="13.0.3" />
</ItemGroup>
</Project>

files.json — the file manifest

{
"files": [
{
"path": "C:\\...\\Shapes\\Composite\\Group.cs",
"project": "Composite",
"targetFramework": "net10.0",
"isGenerated": false,
"containsSymbols": [ "global::Composite.Group" ]
}
]
}

references.json — external references (the follow-on step's input)

NuGet packages and framework assemblies the slice depends on but does not vendor:

{
"packages": [
{
"id": "newtonsoft.json",
"version": "13.0.3",
"assemblyName": "Newtonsoft.Json",
"assemblyVersion": "13.0.0.0"
}
],
"frameworkAssemblies": [
{
"name": "System.Runtime",
"version": "10.0.0.0",
"path": "C:\\Program Files\\dotnet\\packs\\Microsoft.NETCore.App.Ref\\...\\System.Runtime.dll"
}
]
}

result.json — the machine-readable run manifest

Written on every run (success or fatal), and also printed to stdout under --format json. A single, stable, camelCase object — success flag, exit code, seed, mode, counts, the output paths, the NuGet packages, and the full diagnostics list — designed to be read directly with System.Text.Json (see Scripting).

{
"schemaVersion": 1,
"tool": { "name": "chisel", "version": "2026.2.624" },
"success": true, "exitCode": 0, "elapsedSeconds": 3.1,
"seed": { "displayName": "global::MyNS.IFoo", "filePath": ".../IFoo.cs" },
"mode": { "walkDepth": "signatures", "expansion": "seed", "sourceGenerators": "reference" },
"counts": { "inSourceTypes": 7, "files": 6, "projects": 4, "externalReferences": 3, "packages": 1 },
"packages": [ { "id": "newtonsoft.json", "version": "13.0.3" } ],
"diagnostics": [ { "severity": "Warning", "stage": "Walk", "message": "", "item": "" } ]
}

src/… — copied sources

Every collected .cs file, copied under src/<ProjectName>/… preserving the path relative to its project. Files outside their project directory (e.g. <Link> items) go under _linked/<hash>/; materialized generator output goes under _generated/.

.gitignore

A .gitignore is written into the output root so the slice behaves like a normal, self-contained repo (build artifacts under bin//obj/ stay untracked). It propagates the analyzed solution's own .gitignore (the nearest one found walking up from the .sln); if none exists, a minimal .NET default is written instead.

chisel.log — the run log

A full, timestamped copy of the run (every phase, diagnostic, and the final summary) is written to <output>/chisel.log via Serilog, reset on each run. The console shows the same information with a clean layout; --quiet restricts the console to warnings/errors while the log file still captures everything, and --verbose lists every diagnostic instead of grouping them by stage.


How it works

chisel is a pipeline over Roslyn's semantic model (not text search). It opens the solution with MSBuildWorkspace, classifies which assemblies belong to the codebase, resolves the seed type, walks the dependency graph, collects the contributing files + settings, and emits the slice.

src/Chisel.Core/ library — all slicing logic
src/Chisel.Cli/ thin console host (chisel)
tests/Chisel.Core.Tests/ xUnit tests
tests/Fixtures/ worked-example solutions (also used as tests)

For the full walk-through — every stage, the body/signature distinction, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ — see docs/GUIDE.md.


Error handling

chisel is best-effort: it would rather hand you a mostly-complete slice than nothing. A problem with one file, symbol, or reference is reported and skipped — it does not abort the run. Diagnostics are streamed to stderr as they happen and recapped in an end-of-run summary:

Diagnostics: 0 error(s), 1 warning(s) — the slice was still produced.
[Warning] TargetFramework: Project 'MultiTarget.csproj' multi-targets (net8.0, net10.0); slicing against 'net8.0'. Pass --tfm to choose.

Only four conditions are fatal (there is genuinely nothing to produce): no .NET SDK / MSBuild installed (exit 7), a missing solution file (exit 5), a workspace that fails to load without --allow-partial (exit 4), and an unresolvable/ambiguous seed type (exit 3). Everything else — a file that won't bind, a reference that won't resolve, a file that can't be copied — becomes a non-fatal diagnostic and the run still exits 0 with the slice written.

Failing to open a non-C# project (.proj, .vcxproj, .fsproj, .vbproj, …) is not fatal even without --allow-partial: those projects hold no C# to collect, so the failure is reported as a warning and the C# projects load normally.


Known limitations

  • dynamic and reflection-by-string (Type.GetType("…"), DI string registrations) are not statically traceable and are not followed; a warning is emitted when dynamic is encountered.
  • Single target framework per run. Multi-targeted projects are sliced against one TFM (--tfm to choose); code in #if regions for other TFMs is preserved in the copied file but not analyzed.
  • Source generators default to reference (the generated files are skipped and a warning tells you the generator must run downstream). Use --source-generators materialize to write the generated code into the slice for a self-contained result. See the Guide.
  • file-scoped types cannot be used as the seed (they have no addressable metadata name).

Troubleshooting

SymptomFix
No .NET SDK was found (exit 7)Install the .NET 10 SDK and ensure dotnet is on your PATH. A self-contained binary still needs it.
Type ... not foundCheck the FQN and arity (Foo<> not Foo); ensure the solution is restored.
... is ambiguous (exit 3)Pass --project <name> to pick the declaring project.
Workspace load failure (exit 4)dotnet restore the solution; if one project is broken, try --allow-partial.
Slice misses a type referenced via dynamic/reflectionExpected — add it manually (see limitations).
Slice won't compile due to a missing generated typeRe-run with --source-generators materialize.

Documentation

DocumentPurpose
docs/GUIDE.mdThe deep dive: how the walk works, every output format, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ.
CONTRIBUTING.mdDev setup, the test-fixture step, coding conventions, and the PR checklist.
LICENSEMIT license text.

Contributing

Contributions welcome — see CONTRIBUTING.md for the full guide (dev setup, the test-fixture step, coding conventions, and the PR checklist). Please open an issue before submitting a pull request for non-trivial changes.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes, including tests
  4. Prepare the test fixtures, then run the suite:
    pwsh build/restore-test-fixtures.ps1 # restores the fixture solutions + builds the SourceGen generator
    dotnet test
  5. Submit a pull request

The example solutions under tests/Fixtures/ are not part of Chisel.slnx, so they must be restored before dotnet test (the restore-test-fixtures.ps1 helper does this). See docs/GUIDE.md for the developer guide.

If you find this tool useful, I accept tips / donations:

❤️ ~B Sponsor


License

MIT © 2026 Brian Bennewitz — see LICENSE.

About

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

𓍋 Chisel

CIReleaseLatest releaseNuGetLicense: MIT.NET 10Sponsor

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Given a fully-qualified type name and a .sln, chisel walks the Roslyn semantic graph starting from that type and pulls in every .cs file in the codebase required to compile it — across project boundaries. Code that lives outside the codebase (the BCL, NuGet packages) is treated as a leaf: it is never vendored in, but every external assembly / NuGet package the slice touches is recorded so a follow-on step can resolve references.

flowchart LR
A["Seed type<br/>e.g. MyNS.IFoo"] --> B["TypeResolver"]
B -->|"walk the semantic graph:<br/>base · interfaces · members · bodies<br/><i>(in-source only)</i>"| C["The slice<br/>(.cs files)"]
B --> D["External refs<br/><i>(BCL / NuGet — recorded as leaves)</i>"]
Loading

📖 New here? The Guide is the deep dive — the dependency walk in detail, every output format, source generators, multi-targeting, global usings, and embedding the Core library.


What it carves out

By default the walk keeps the declared shape of the seed and its implementations — the contract and the data types — but not what method bodies use. Starting from the seed type it follows:

  • Base types, interfaces (including inherited ones), and generic constraints / type arguments.
  • Member signatures — field/property/event types, method return & parameter types, indexer parameters, nested types, and the enclosing type chain of a nested seed.
  • Attributes — the attribute class plus any typeof(SomeType) in constructor or named arguments (including arrays of typeof).
  • Seed implementations — when the seed is an interface or abstract class, its concrete implementations (and, for an interface seed, the base interfaces it derives from) are pulled in. Interfaces/classes encountered deeper in the graph (e.g. a property's type) are included as declarations only — their implementations are not expanded.
  • Authored global using files — dedicated global-usings files are kept even though they declare no types.

What it deliberately leaves behind by default: method-body usages (what the kept types call or instantiate) and implementations of interfaces reached deep in the graph. This keeps the slice a focused "contract + shape" extract rather than the whole reachable call graph.

Two knobs widen the walk:

  • --walk-depth bodies — also follow every type referenced inside method bodies, transitively. Produces a self-compilable slice, but pulls in far more. (Default signatures does not, so a default slice may not compile standalone — body-referenced in-source types are treated as external.)
  • --expand-impls all — expand every interface/class reached to its implementations, not just the seed. (--expand-impls none, aliased --no-derived, never expands.)

Anything whose containing assembly is not one of the solution's projects (BCL, NuGet) is a leaf: it is recorded in references.json and as a <PackageReference>, but its source is never pulled into the slice.


Requirements

  • .NET 10 SDK (pinned via global.json to 10.0.300).
  • The target solution should be restored so package metadata resolves — either run dotnet restore yourself, or pass --restore to have chisel do it first.

chisel loads and evaluates your solution with MSBuild, which ships with the .NET SDK. The SDK is therefore required at run time, not just to build chisel — see the note under pre-built binaries.


Install

As a .NET tool (recommended)

chisel is packaged as a .NET tool, so it installs by name and is invoked as dotnet chisel …:

dotnet tool install --global Bennewitz.Ninja.Chisel
dotnet chisel --version

Pre-built binaries

Each release also publishes self-contained, per-platform archives:

PlatformFile
Windows (x64)chisel-<version>-win-x64.zip
Windows (ARM64)chisel-<version>-win-arm64.zip
Linux (x64)chisel-<version>-linux-x64.tar.gz
Linux (ARM64)chisel-<version>-linux-arm64.tar.gz
macOS (Intel)chisel-<version>-osx-x64.tar.gz
macOS (Apple Silicon)chisel-<version>-osx-arm64.tar.gz

These binaries still require a .NET 10 SDK on the machine. Unlike a typical self-contained app, chisel locates and drives the installed SDK's MSBuild at run time (via MSBuildLocator); the Microsoft.Build.* engine assemblies are deliberately not bundled. The archives only save you the dotnet tool install step. Since any machine that can build the solution you're slicing already has the SDK, the .NET tool above is the recommended path. If no SDK is found, chisel exits 7 with an actionable message.

Build from source

git clone https://github.com/JanusMael/chisel.git
cd chisel
dotnet build
dotnet run --project src/Chisel.Cli -- --help

To install your local build as a tool:

dotnet pack src/Chisel.Cli -c Release -o ./nupkg
dotnet tool install --global --add-source ./nupkg --prerelease Bennewitz.Ninja.Chisel
dotnet chisel --version

Quick start

dotnet chisel \
--type MyNS.IFoo \
--solution path/to/MySolution.sln \
--output ./out

This writes the artifacts into ./out (see Outputs) and prints a summary:

Seed type: global::MyNS.IFoo
Files: 6
External refs: 3 (1 NuGet packages)
Projects: 4
files.json → .../out/files.json
references.json → .../out/references.json
Slice.csproj → .../out/Slice.csproj
copied sources → .../out/src
.gitignore → .../out/.gitignore

To build the extracted slice on its own:

dotnet build ./out/Slice.csproj

Command-line reference

dotnet chisel --type <FQN> --solution <path.sln> --output <dir> [options]

Required

FlagDescription
--type, -t <FQN>Fully-qualified type name. See type-name formats.
--solution, -s <path>Path to the .sln / .slnx file.
--output, -o <dir>Output directory (created if missing).

Options

FlagDefaultDescription
--project <name>Disambiguate when the FQN matches types in multiple projects.
--tfm <name>first TFMPreferred target framework when a project multi-targets.
--walk-depth <d>signaturessignatures (declared shape only) | bodies (also follow method-body usages transitively — self-compilable, larger).
--expand-impls <s>seedseed (expand only the seed + an interface seed's base interfaces) | all (every interface/class reached) | none.
--no-derivedAlias for --expand-impls none.
--source-generators <p>referenceskip | materialize | reference — how to treat generator output.
--exclude, -x <path>Directory subtree to drop from the slice (repeatable). Any collected file under <path> is logged and left out — handy for vendored/generated/out-of-scope regions. The slice may then be incomplete; each drop is reported as an Exclude warning.
--exclude-from <file>Read exclusion directories from a file, one path per line (repeatable; merged with --exclude). Use - to read from stdin ($paths | chisel --exclude-from -). Blank lines and # comments are ignored; relative paths resolve against the file's directory (the working directory for stdin).
--allow-partialoffContinue when MSBuild reports project-load failures (otherwise fail fast).
--restoreoffRun dotnet restore on the solution before analyzing (best-effort; a failed restore warns and continues).
--format <f>texttext (human summary on stdout) | json (the run manifest on stdout). result.json is always written to --output regardless.
--strictoffExit nonzero (6) if any error-severity diagnostic occurred (default stays 0 on a best-effort run).
--verbose, -vList every diagnostic instead of grouping by stage.
--quiet, -qConsole shows only warnings/errors; the full run log is still written.
--no-colorDisable ANSI color (also honored via the NO_COLOR env var).
--version, -VPrint version and exit.
-h, --helpShow usage.

Type-name formats

You typeResolves to
MyNS.Widgetthe non-generic Widget
MyNS.Repository<T> or MyNS.Repository<>the open generic Repository<T> (arity 1)
MyNS.Map<,>the open generic with arity 2
MyNS.Outer.Inner or MyNS.Outer+Innerthe nested type Inner

Exit codes

CodeMeaning
0Success
1No arguments (usage printed)
2Invalid arguments
3Type not found / ambiguous (pass --project)
4Workspace failed to load (try --allow-partial)
5Solution file not found
6Completed with error-severity diagnostics (only under --strict)
7No .NET SDK / MSBuild found (install the .NET 10 SDK — see Requirements)
130Canceled (Ctrl+C)

Scripting (PowerShell)

Designed for PowerShell Core 7+. Streams are split for clean capture — stdout carries the result (the text summary, or the manifest under --format json), stderr carries progress/diagnostics — and a stable result.json is always written. Read it with fully-qualified .NET (no cmdlets, no piping):

& dotnet chisel -t 'Contracts.IShape'-s $sln-o $out--strict
if ($LASTEXITCODE-ne0) { throw"chisel failed ($LASTEXITCODE)" }
$doc= [System.Text.Json.JsonDocument]::Parse(
[System.IO.File]::ReadAllText([System.IO.Path]::Combine($out,'result.json')))
$root=$doc.RootElement$root.GetProperty('counts').GetProperty('files').GetInt32()
foreach ($pin$root.GetProperty('packages').EnumerateArray()) {
"$($p.GetProperty('id').GetString())$($p.GetProperty('version').GetString())"
}

Or capture the manifest straight off stdout: $json = & dotnet chisel … --format json (progress still shows on stderr).

Generic types in PowerShell: single-quote any type name containing <, >, or a backtick — 'MyNS.Repository<T>' or 'MyNS.Repository`1' (both resolve). < is a reserved PS operator and the backtick is the PS escape char; single quotes pass them through literally.

See Exit codes to branch on $LASTEXITCODE; --strict turns any error-severity diagnostic into a nonzero exit.


Outputs

All of these are written into --output.

Slice.csproj — a flat, buildable project

Explicit <Compile Include> per collected file (no ProjectReferences — the slice is flattened), plus a <PackageReference> per detected NuGet package. Compilation settings (TargetFramework, LangVersion, Nullable, ImplicitUsings, AllowUnsafeBlocks, user DefineConstants) are hoisted from the contributing projects — taking the highest/strictest value when projects disagree, and warning you when they do.

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<CompileInclude="src/ExternalPackage/MyClass.cs" />
</ItemGroup>
<ItemGroup>
<PackageReferenceInclude="newtonsoft.json"Version="13.0.3" />
</ItemGroup>
</Project>

files.json — the file manifest

{
"files": [
{
"path": "C:\\...\\Shapes\\Composite\\Group.cs",
"project": "Composite",
"targetFramework": "net10.0",
"isGenerated": false,
"containsSymbols": [ "global::Composite.Group" ]
}
]
}

references.json — external references (the follow-on step's input)

NuGet packages and framework assemblies the slice depends on but does not vendor:

{
"packages": [
{
"id": "newtonsoft.json",
"version": "13.0.3",
"assemblyName": "Newtonsoft.Json",
"assemblyVersion": "13.0.0.0"
}
],
"frameworkAssemblies": [
{
"name": "System.Runtime",
"version": "10.0.0.0",
"path": "C:\\Program Files\\dotnet\\packs\\Microsoft.NETCore.App.Ref\\...\\System.Runtime.dll"
}
]
}

result.json — the machine-readable run manifest

Written on every run (success or fatal), and also printed to stdout under --format json. A single, stable, camelCase object — success flag, exit code, seed, mode, counts, the output paths, the NuGet packages, and the full diagnostics list — designed to be read directly with System.Text.Json (see Scripting).

{
"schemaVersion": 1,
"tool": { "name": "chisel", "version": "2026.2.624" },
"success": true, "exitCode": 0, "elapsedSeconds": 3.1,
"seed": { "displayName": "global::MyNS.IFoo", "filePath": ".../IFoo.cs" },
"mode": { "walkDepth": "signatures", "expansion": "seed", "sourceGenerators": "reference" },
"counts": { "inSourceTypes": 7, "files": 6, "projects": 4, "externalReferences": 3, "packages": 1 },
"packages": [ { "id": "newtonsoft.json", "version": "13.0.3" } ],
"diagnostics": [ { "severity": "Warning", "stage": "Walk", "message": "", "item": "" } ]
}

src/… — copied sources

Every collected .cs file, copied under src/<ProjectName>/… preserving the path relative to its project. Files outside their project directory (e.g. <Link> items) go under _linked/<hash>/; materialized generator output goes under _generated/.

.gitignore

A .gitignore is written into the output root so the slice behaves like a normal, self-contained repo (build artifacts under bin//obj/ stay untracked). It propagates the analyzed solution's own .gitignore (the nearest one found walking up from the .sln); if none exists, a minimal .NET default is written instead.

chisel.log — the run log

A full, timestamped copy of the run (every phase, diagnostic, and the final summary) is written to <output>/chisel.log via Serilog, reset on each run. The console shows the same information with a clean layout; --quiet restricts the console to warnings/errors while the log file still captures everything, and --verbose lists every diagnostic instead of grouping them by stage.


How it works

chisel is a pipeline over Roslyn's semantic model (not text search). It opens the solution with MSBuildWorkspace, classifies which assemblies belong to the codebase, resolves the seed type, walks the dependency graph, collects the contributing files + settings, and emits the slice.

src/Chisel.Core/ library — all slicing logic
src/Chisel.Cli/ thin console host (chisel)
tests/Chisel.Core.Tests/ xUnit tests
tests/Fixtures/ worked-example solutions (also used as tests)

For the full walk-through — every stage, the body/signature distinction, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ — see docs/GUIDE.md.


Error handling

chisel is best-effort: it would rather hand you a mostly-complete slice than nothing. A problem with one file, symbol, or reference is reported and skipped — it does not abort the run. Diagnostics are streamed to stderr as they happen and recapped in an end-of-run summary:

Diagnostics: 0 error(s), 1 warning(s) — the slice was still produced.
[Warning] TargetFramework: Project 'MultiTarget.csproj' multi-targets (net8.0, net10.0); slicing against 'net8.0'. Pass --tfm to choose.

Only four conditions are fatal (there is genuinely nothing to produce): no .NET SDK / MSBuild installed (exit 7), a missing solution file (exit 5), a workspace that fails to load without --allow-partial (exit 4), and an unresolvable/ambiguous seed type (exit 3). Everything else — a file that won't bind, a reference that won't resolve, a file that can't be copied — becomes a non-fatal diagnostic and the run still exits 0 with the slice written.

Failing to open a non-C# project (.proj, .vcxproj, .fsproj, .vbproj, …) is not fatal even without --allow-partial: those projects hold no C# to collect, so the failure is reported as a warning and the C# projects load normally.


Known limitations

  • dynamic and reflection-by-string (Type.GetType("…"), DI string registrations) are not statically traceable and are not followed; a warning is emitted when dynamic is encountered.
  • Single target framework per run. Multi-targeted projects are sliced against one TFM (--tfm to choose); code in #if regions for other TFMs is preserved in the copied file but not analyzed.
  • Source generators default to reference (the generated files are skipped and a warning tells you the generator must run downstream). Use --source-generators materialize to write the generated code into the slice for a self-contained result. See the Guide.
  • file-scoped types cannot be used as the seed (they have no addressable metadata name).

Troubleshooting

SymptomFix
No .NET SDK was found (exit 7)Install the .NET 10 SDK and ensure dotnet is on your PATH. A self-contained binary still needs it.
Type ... not foundCheck the FQN and arity (Foo<> not Foo); ensure the solution is restored.
... is ambiguous (exit 3)Pass --project <name> to pick the declaring project.
Workspace load failure (exit 4)dotnet restore the solution; if one project is broken, try --allow-partial.
Slice misses a type referenced via dynamic/reflectionExpected — add it manually (see limitations).
Slice won't compile due to a missing generated typeRe-run with --source-generators materialize.

Documentation

DocumentPurpose
docs/GUIDE.mdThe deep dive: how the walk works, every output format, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ.
CONTRIBUTING.mdDev setup, the test-fixture step, coding conventions, and the PR checklist.
LICENSEMIT license text.

Contributing

Contributions welcome — see CONTRIBUTING.md for the full guide (dev setup, the test-fixture step, coding conventions, and the PR checklist). Please open an issue before submitting a pull request for non-trivial changes.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes, including tests
  4. Prepare the test fixtures, then run the suite:
    pwsh build/restore-test-fixtures.ps1 # restores the fixture solutions + builds the SourceGen generator
    dotnet test
  5. Submit a pull request

The example solutions under tests/Fixtures/ are not part of Chisel.slnx, so they must be restored before dotnet test (the restore-test-fixtures.ps1 helper does this). See docs/GUIDE.md for the developer guide.

If you find this tool useful, I accept tips / donations:

❤️ ~B Sponsor


License

MIT © 2026 Brian Bennewitz — see LICENSE.

About

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

𓍋 Chisel

CIReleaseLatest releaseNuGetLicense: MIT.NET 10Sponsor

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Given a fully-qualified type name and a .sln, chisel walks the Roslyn semantic graph starting from that type and pulls in every .cs file in the codebase required to compile it — across project boundaries. Code that lives outside the codebase (the BCL, NuGet packages) is treated as a leaf: it is never vendored in, but every external assembly / NuGet package the slice touches is recorded so a follow-on step can resolve references.

flowchart LR
A["Seed type<br/>e.g. MyNS.IFoo"] --> B["TypeResolver"]
B -->|"walk the semantic graph:<br/>base · interfaces · members · bodies<br/><i>(in-source only)</i>"| C["The slice<br/>(.cs files)"]
B --> D["External refs<br/><i>(BCL / NuGet — recorded as leaves)</i>"]
Loading

📖 New here? The Guide is the deep dive — the dependency walk in detail, every output format, source generators, multi-targeting, global usings, and embedding the Core library.


What it carves out

By default the walk keeps the declared shape of the seed and its implementations — the contract and the data types — but not what method bodies use. Starting from the seed type it follows:

  • Base types, interfaces (including inherited ones), and generic constraints / type arguments.
  • Member signatures — field/property/event types, method return & parameter types, indexer parameters, nested types, and the enclosing type chain of a nested seed.
  • Attributes — the attribute class plus any typeof(SomeType) in constructor or named arguments (including arrays of typeof).
  • Seed implementations — when the seed is an interface or abstract class, its concrete implementations (and, for an interface seed, the base interfaces it derives from) are pulled in. Interfaces/classes encountered deeper in the graph (e.g. a property's type) are included as declarations only — their implementations are not expanded.
  • Authored global using files — dedicated global-usings files are kept even though they declare no types.

What it deliberately leaves behind by default: method-body usages (what the kept types call or instantiate) and implementations of interfaces reached deep in the graph. This keeps the slice a focused "contract + shape" extract rather than the whole reachable call graph.

Two knobs widen the walk:

  • --walk-depth bodies — also follow every type referenced inside method bodies, transitively. Produces a self-compilable slice, but pulls in far more. (Default signatures does not, so a default slice may not compile standalone — body-referenced in-source types are treated as external.)
  • --expand-impls all — expand every interface/class reached to its implementations, not just the seed. (--expand-impls none, aliased --no-derived, never expands.)

Anything whose containing assembly is not one of the solution's projects (BCL, NuGet) is a leaf: it is recorded in references.json and as a <PackageReference>, but its source is never pulled into the slice.


Requirements

  • .NET 10 SDK (pinned via global.json to 10.0.300).
  • The target solution should be restored so package metadata resolves — either run dotnet restore yourself, or pass --restore to have chisel do it first.

chisel loads and evaluates your solution with MSBuild, which ships with the .NET SDK. The SDK is therefore required at run time, not just to build chisel — see the note under pre-built binaries.


Install

As a .NET tool (recommended)

chisel is packaged as a .NET tool, so it installs by name and is invoked as dotnet chisel …:

dotnet tool install --global Bennewitz.Ninja.Chisel
dotnet chisel --version

Pre-built binaries

Each release also publishes self-contained, per-platform archives:

PlatformFile
Windows (x64)chisel-<version>-win-x64.zip
Windows (ARM64)chisel-<version>-win-arm64.zip
Linux (x64)chisel-<version>-linux-x64.tar.gz
Linux (ARM64)chisel-<version>-linux-arm64.tar.gz
macOS (Intel)chisel-<version>-osx-x64.tar.gz
macOS (Apple Silicon)chisel-<version>-osx-arm64.tar.gz

These binaries still require a .NET 10 SDK on the machine. Unlike a typical self-contained app, chisel locates and drives the installed SDK's MSBuild at run time (via MSBuildLocator); the Microsoft.Build.* engine assemblies are deliberately not bundled. The archives only save you the dotnet tool install step. Since any machine that can build the solution you're slicing already has the SDK, the .NET tool above is the recommended path. If no SDK is found, chisel exits 7 with an actionable message.

Build from source

git clone https://github.com/JanusMael/chisel.git
cd chisel
dotnet build
dotnet run --project src/Chisel.Cli -- --help

To install your local build as a tool:

dotnet pack src/Chisel.Cli -c Release -o ./nupkg
dotnet tool install --global --add-source ./nupkg --prerelease Bennewitz.Ninja.Chisel
dotnet chisel --version

Quick start

dotnet chisel \
--type MyNS.IFoo \
--solution path/to/MySolution.sln \
--output ./out

This writes the artifacts into ./out (see Outputs) and prints a summary:

Seed type: global::MyNS.IFoo
Files: 6
External refs: 3 (1 NuGet packages)
Projects: 4
files.json → .../out/files.json
references.json → .../out/references.json
Slice.csproj → .../out/Slice.csproj
copied sources → .../out/src
.gitignore → .../out/.gitignore

To build the extracted slice on its own:

dotnet build ./out/Slice.csproj

Command-line reference

dotnet chisel --type <FQN> --solution <path.sln> --output <dir> [options]

Required

FlagDescription
--type, -t <FQN>Fully-qualified type name. See type-name formats.
--solution, -s <path>Path to the .sln / .slnx file.
--output, -o <dir>Output directory (created if missing).

Options

FlagDefaultDescription
--project <name>Disambiguate when the FQN matches types in multiple projects.
--tfm <name>first TFMPreferred target framework when a project multi-targets.
--walk-depth <d>signaturessignatures (declared shape only) | bodies (also follow method-body usages transitively — self-compilable, larger).
--expand-impls <s>seedseed (expand only the seed + an interface seed's base interfaces) | all (every interface/class reached) | none.
--no-derivedAlias for --expand-impls none.
--source-generators <p>referenceskip | materialize | reference — how to treat generator output.
--exclude, -x <path>Directory subtree to drop from the slice (repeatable). Any collected file under <path> is logged and left out — handy for vendored/generated/out-of-scope regions. The slice may then be incomplete; each drop is reported as an Exclude warning.
--exclude-from <file>Read exclusion directories from a file, one path per line (repeatable; merged with --exclude). Use - to read from stdin ($paths | chisel --exclude-from -). Blank lines and # comments are ignored; relative paths resolve against the file's directory (the working directory for stdin).
--allow-partialoffContinue when MSBuild reports project-load failures (otherwise fail fast).
--restoreoffRun dotnet restore on the solution before analyzing (best-effort; a failed restore warns and continues).
--format <f>texttext (human summary on stdout) | json (the run manifest on stdout). result.json is always written to --output regardless.
--strictoffExit nonzero (6) if any error-severity diagnostic occurred (default stays 0 on a best-effort run).
--verbose, -vList every diagnostic instead of grouping by stage.
--quiet, -qConsole shows only warnings/errors; the full run log is still written.
--no-colorDisable ANSI color (also honored via the NO_COLOR env var).
--version, -VPrint version and exit.
-h, --helpShow usage.

Type-name formats

You typeResolves to
MyNS.Widgetthe non-generic Widget
MyNS.Repository<T> or MyNS.Repository<>the open generic Repository<T> (arity 1)
MyNS.Map<,>the open generic with arity 2
MyNS.Outer.Inner or MyNS.Outer+Innerthe nested type Inner

Exit codes

CodeMeaning
0Success
1No arguments (usage printed)
2Invalid arguments
3Type not found / ambiguous (pass --project)
4Workspace failed to load (try --allow-partial)
5Solution file not found
6Completed with error-severity diagnostics (only under --strict)
7No .NET SDK / MSBuild found (install the .NET 10 SDK — see Requirements)
130Canceled (Ctrl+C)

Scripting (PowerShell)

Designed for PowerShell Core 7+. Streams are split for clean capture — stdout carries the result (the text summary, or the manifest under --format json), stderr carries progress/diagnostics — and a stable result.json is always written. Read it with fully-qualified .NET (no cmdlets, no piping):

& dotnet chisel -t 'Contracts.IShape'-s $sln-o $out--strict
if ($LASTEXITCODE-ne0) { throw"chisel failed ($LASTEXITCODE)" }
$doc= [System.Text.Json.JsonDocument]::Parse(
[System.IO.File]::ReadAllText([System.IO.Path]::Combine($out,'result.json')))
$root=$doc.RootElement$root.GetProperty('counts').GetProperty('files').GetInt32()
foreach ($pin$root.GetProperty('packages').EnumerateArray()) {
"$($p.GetProperty('id').GetString())$($p.GetProperty('version').GetString())"
}

Or capture the manifest straight off stdout: $json = & dotnet chisel … --format json (progress still shows on stderr).

Generic types in PowerShell: single-quote any type name containing <, >, or a backtick — 'MyNS.Repository<T>' or 'MyNS.Repository`1' (both resolve). < is a reserved PS operator and the backtick is the PS escape char; single quotes pass them through literally.

See Exit codes to branch on $LASTEXITCODE; --strict turns any error-severity diagnostic into a nonzero exit.


Outputs

All of these are written into --output.

Slice.csproj — a flat, buildable project

Explicit <Compile Include> per collected file (no ProjectReferences — the slice is flattened), plus a <PackageReference> per detected NuGet package. Compilation settings (TargetFramework, LangVersion, Nullable, ImplicitUsings, AllowUnsafeBlocks, user DefineConstants) are hoisted from the contributing projects — taking the highest/strictest value when projects disagree, and warning you when they do.

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<CompileInclude="src/ExternalPackage/MyClass.cs" />
</ItemGroup>
<ItemGroup>
<PackageReferenceInclude="newtonsoft.json"Version="13.0.3" />
</ItemGroup>
</Project>

files.json — the file manifest

{
"files": [
{
"path": "C:\\...\\Shapes\\Composite\\Group.cs",
"project": "Composite",
"targetFramework": "net10.0",
"isGenerated": false,
"containsSymbols": [ "global::Composite.Group" ]
}
]
}

references.json — external references (the follow-on step's input)

NuGet packages and framework assemblies the slice depends on but does not vendor:

{
"packages": [
{
"id": "newtonsoft.json",
"version": "13.0.3",
"assemblyName": "Newtonsoft.Json",
"assemblyVersion": "13.0.0.0"
}
],
"frameworkAssemblies": [
{
"name": "System.Runtime",
"version": "10.0.0.0",
"path": "C:\\Program Files\\dotnet\\packs\\Microsoft.NETCore.App.Ref\\...\\System.Runtime.dll"
}
]
}

result.json — the machine-readable run manifest

Written on every run (success or fatal), and also printed to stdout under --format json. A single, stable, camelCase object — success flag, exit code, seed, mode, counts, the output paths, the NuGet packages, and the full diagnostics list — designed to be read directly with System.Text.Json (see Scripting).

{
"schemaVersion": 1,
"tool": { "name": "chisel", "version": "2026.2.624" },
"success": true, "exitCode": 0, "elapsedSeconds": 3.1,
"seed": { "displayName": "global::MyNS.IFoo", "filePath": ".../IFoo.cs" },
"mode": { "walkDepth": "signatures", "expansion": "seed", "sourceGenerators": "reference" },
"counts": { "inSourceTypes": 7, "files": 6, "projects": 4, "externalReferences": 3, "packages": 1 },
"packages": [ { "id": "newtonsoft.json", "version": "13.0.3" } ],
"diagnostics": [ { "severity": "Warning", "stage": "Walk", "message": "", "item": "" } ]
}

src/… — copied sources

Every collected .cs file, copied under src/<ProjectName>/… preserving the path relative to its project. Files outside their project directory (e.g. <Link> items) go under _linked/<hash>/; materialized generator output goes under _generated/.

.gitignore

A .gitignore is written into the output root so the slice behaves like a normal, self-contained repo (build artifacts under bin//obj/ stay untracked). It propagates the analyzed solution's own .gitignore (the nearest one found walking up from the .sln); if none exists, a minimal .NET default is written instead.

chisel.log — the run log

A full, timestamped copy of the run (every phase, diagnostic, and the final summary) is written to <output>/chisel.log via Serilog, reset on each run. The console shows the same information with a clean layout; --quiet restricts the console to warnings/errors while the log file still captures everything, and --verbose lists every diagnostic instead of grouping them by stage.


How it works

chisel is a pipeline over Roslyn's semantic model (not text search). It opens the solution with MSBuildWorkspace, classifies which assemblies belong to the codebase, resolves the seed type, walks the dependency graph, collects the contributing files + settings, and emits the slice.

src/Chisel.Core/ library — all slicing logic
src/Chisel.Cli/ thin console host (chisel)
tests/Chisel.Core.Tests/ xUnit tests
tests/Fixtures/ worked-example solutions (also used as tests)

For the full walk-through — every stage, the body/signature distinction, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ — see docs/GUIDE.md.


Error handling

chisel is best-effort: it would rather hand you a mostly-complete slice than nothing. A problem with one file, symbol, or reference is reported and skipped — it does not abort the run. Diagnostics are streamed to stderr as they happen and recapped in an end-of-run summary:

Diagnostics: 0 error(s), 1 warning(s) — the slice was still produced.
[Warning] TargetFramework: Project 'MultiTarget.csproj' multi-targets (net8.0, net10.0); slicing against 'net8.0'. Pass --tfm to choose.

Only four conditions are fatal (there is genuinely nothing to produce): no .NET SDK / MSBuild installed (exit 7), a missing solution file (exit 5), a workspace that fails to load without --allow-partial (exit 4), and an unresolvable/ambiguous seed type (exit 3). Everything else — a file that won't bind, a reference that won't resolve, a file that can't be copied — becomes a non-fatal diagnostic and the run still exits 0 with the slice written.

Failing to open a non-C# project (.proj, .vcxproj, .fsproj, .vbproj, …) is not fatal even without --allow-partial: those projects hold no C# to collect, so the failure is reported as a warning and the C# projects load normally.


Known limitations

  • dynamic and reflection-by-string (Type.GetType("…"), DI string registrations) are not statically traceable and are not followed; a warning is emitted when dynamic is encountered.
  • Single target framework per run. Multi-targeted projects are sliced against one TFM (--tfm to choose); code in #if regions for other TFMs is preserved in the copied file but not analyzed.
  • Source generators default to reference (the generated files are skipped and a warning tells you the generator must run downstream). Use --source-generators materialize to write the generated code into the slice for a self-contained result. See the Guide.
  • file-scoped types cannot be used as the seed (they have no addressable metadata name).

Troubleshooting

SymptomFix
No .NET SDK was found (exit 7)Install the .NET 10 SDK and ensure dotnet is on your PATH. A self-contained binary still needs it.
Type ... not foundCheck the FQN and arity (Foo<> not Foo); ensure the solution is restored.
... is ambiguous (exit 3)Pass --project <name> to pick the declaring project.
Workspace load failure (exit 4)dotnet restore the solution; if one project is broken, try --allow-partial.
Slice misses a type referenced via dynamic/reflectionExpected — add it manually (see limitations).
Slice won't compile due to a missing generated typeRe-run with --source-generators materialize.

Documentation

DocumentPurpose
docs/GUIDE.mdThe deep dive: how the walk works, every output format, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ.
CONTRIBUTING.mdDev setup, the test-fixture step, coding conventions, and the PR checklist.
LICENSEMIT license text.

Contributing

Contributions welcome — see CONTRIBUTING.md for the full guide (dev setup, the test-fixture step, coding conventions, and the PR checklist). Please open an issue before submitting a pull request for non-trivial changes.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes, including tests
  4. Prepare the test fixtures, then run the suite:
    pwsh build/restore-test-fixtures.ps1 # restores the fixture solutions + builds the SourceGen generator
    dotnet test
  5. Submit a pull request

The example solutions under tests/Fixtures/ are not part of Chisel.slnx, so they must be restored before dotnet test (the restore-test-fixtures.ps1 helper does this). See docs/GUIDE.md for the developer guide.

If you find this tool useful, I accept tips / donations:

❤️ ~B Sponsor


License

MIT © 2026 Brian Bennewitz — see LICENSE.

About

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

𓍋 Chisel

CIReleaseLatest releaseNuGetLicense: MIT.NET 10Sponsor

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Given a fully-qualified type name and a .sln, chisel walks the Roslyn semantic graph starting from that type and pulls in every .cs file in the codebase required to compile it — across project boundaries. Code that lives outside the codebase (the BCL, NuGet packages) is treated as a leaf: it is never vendored in, but every external assembly / NuGet package the slice touches is recorded so a follow-on step can resolve references.

flowchart LR
A["Seed type<br/>e.g. MyNS.IFoo"] --> B["TypeResolver"]
B -->|"walk the semantic graph:<br/>base · interfaces · members · bodies<br/><i>(in-source only)</i>"| C["The slice<br/>(.cs files)"]
B --> D["External refs<br/><i>(BCL / NuGet — recorded as leaves)</i>"]
Loading

📖 New here? The Guide is the deep dive — the dependency walk in detail, every output format, source generators, multi-targeting, global usings, and embedding the Core library.


What it carves out

By default the walk keeps the declared shape of the seed and its implementations — the contract and the data types — but not what method bodies use. Starting from the seed type it follows:

  • Base types, interfaces (including inherited ones), and generic constraints / type arguments.
  • Member signatures — field/property/event types, method return & parameter types, indexer parameters, nested types, and the enclosing type chain of a nested seed.
  • Attributes — the attribute class plus any typeof(SomeType) in constructor or named arguments (including arrays of typeof).
  • Seed implementations — when the seed is an interface or abstract class, its concrete implementations (and, for an interface seed, the base interfaces it derives from) are pulled in. Interfaces/classes encountered deeper in the graph (e.g. a property's type) are included as declarations only — their implementations are not expanded.
  • Authored global using files — dedicated global-usings files are kept even though they declare no types.

What it deliberately leaves behind by default: method-body usages (what the kept types call or instantiate) and implementations of interfaces reached deep in the graph. This keeps the slice a focused "contract + shape" extract rather than the whole reachable call graph.

Two knobs widen the walk:

  • --walk-depth bodies — also follow every type referenced inside method bodies, transitively. Produces a self-compilable slice, but pulls in far more. (Default signatures does not, so a default slice may not compile standalone — body-referenced in-source types are treated as external.)
  • --expand-impls all — expand every interface/class reached to its implementations, not just the seed. (--expand-impls none, aliased --no-derived, never expands.)

Anything whose containing assembly is not one of the solution's projects (BCL, NuGet) is a leaf: it is recorded in references.json and as a <PackageReference>, but its source is never pulled into the slice.


Requirements

  • .NET 10 SDK (pinned via global.json to 10.0.300).
  • The target solution should be restored so package metadata resolves — either run dotnet restore yourself, or pass --restore to have chisel do it first.

chisel loads and evaluates your solution with MSBuild, which ships with the .NET SDK. The SDK is therefore required at run time, not just to build chisel — see the note under pre-built binaries.


Install

As a .NET tool (recommended)

chisel is packaged as a .NET tool, so it installs by name and is invoked as dotnet chisel …:

dotnet tool install --global Bennewitz.Ninja.Chisel
dotnet chisel --version

Pre-built binaries

Each release also publishes self-contained, per-platform archives:

PlatformFile
Windows (x64)chisel-<version>-win-x64.zip
Windows (ARM64)chisel-<version>-win-arm64.zip
Linux (x64)chisel-<version>-linux-x64.tar.gz
Linux (ARM64)chisel-<version>-linux-arm64.tar.gz
macOS (Intel)chisel-<version>-osx-x64.tar.gz
macOS (Apple Silicon)chisel-<version>-osx-arm64.tar.gz

These binaries still require a .NET 10 SDK on the machine. Unlike a typical self-contained app, chisel locates and drives the installed SDK's MSBuild at run time (via MSBuildLocator); the Microsoft.Build.* engine assemblies are deliberately not bundled. The archives only save you the dotnet tool install step. Since any machine that can build the solution you're slicing already has the SDK, the .NET tool above is the recommended path. If no SDK is found, chisel exits 7 with an actionable message.

Build from source

git clone https://github.com/JanusMael/chisel.git
cd chisel
dotnet build
dotnet run --project src/Chisel.Cli -- --help

To install your local build as a tool:

dotnet pack src/Chisel.Cli -c Release -o ./nupkg
dotnet tool install --global --add-source ./nupkg --prerelease Bennewitz.Ninja.Chisel
dotnet chisel --version

Quick start

dotnet chisel \
--type MyNS.IFoo \
--solution path/to/MySolution.sln \
--output ./out

This writes the artifacts into ./out (see Outputs) and prints a summary:

Seed type: global::MyNS.IFoo
Files: 6
External refs: 3 (1 NuGet packages)
Projects: 4
files.json → .../out/files.json
references.json → .../out/references.json
Slice.csproj → .../out/Slice.csproj
copied sources → .../out/src
.gitignore → .../out/.gitignore

To build the extracted slice on its own:

dotnet build ./out/Slice.csproj

Command-line reference

dotnet chisel --type <FQN> --solution <path.sln> --output <dir> [options]

Required

FlagDescription
--type, -t <FQN>Fully-qualified type name. See type-name formats.
--solution, -s <path>Path to the .sln / .slnx file.
--output, -o <dir>Output directory (created if missing).

Options

FlagDefaultDescription
--project <name>Disambiguate when the FQN matches types in multiple projects.
--tfm <name>first TFMPreferred target framework when a project multi-targets.
--walk-depth <d>signaturessignatures (declared shape only) | bodies (also follow method-body usages transitively — self-compilable, larger).
--expand-impls <s>seedseed (expand only the seed + an interface seed's base interfaces) | all (every interface/class reached) | none.
--no-derivedAlias for --expand-impls none.
--source-generators <p>referenceskip | materialize | reference — how to treat generator output.
--exclude, -x <path>Directory subtree to drop from the slice (repeatable). Any collected file under <path> is logged and left out — handy for vendored/generated/out-of-scope regions. The slice may then be incomplete; each drop is reported as an Exclude warning.
--exclude-from <file>Read exclusion directories from a file, one path per line (repeatable; merged with --exclude). Use - to read from stdin ($paths | chisel --exclude-from -). Blank lines and # comments are ignored; relative paths resolve against the file's directory (the working directory for stdin).
--allow-partialoffContinue when MSBuild reports project-load failures (otherwise fail fast).
--restoreoffRun dotnet restore on the solution before analyzing (best-effort; a failed restore warns and continues).
--format <f>texttext (human summary on stdout) | json (the run manifest on stdout). result.json is always written to --output regardless.
--strictoffExit nonzero (6) if any error-severity diagnostic occurred (default stays 0 on a best-effort run).
--verbose, -vList every diagnostic instead of grouping by stage.
--quiet, -qConsole shows only warnings/errors; the full run log is still written.
--no-colorDisable ANSI color (also honored via the NO_COLOR env var).
--version, -VPrint version and exit.
-h, --helpShow usage.

Type-name formats

You typeResolves to
MyNS.Widgetthe non-generic Widget
MyNS.Repository<T> or MyNS.Repository<>the open generic Repository<T> (arity 1)
MyNS.Map<,>the open generic with arity 2
MyNS.Outer.Inner or MyNS.Outer+Innerthe nested type Inner

Exit codes

CodeMeaning
0Success
1No arguments (usage printed)
2Invalid arguments
3Type not found / ambiguous (pass --project)
4Workspace failed to load (try --allow-partial)
5Solution file not found
6Completed with error-severity diagnostics (only under --strict)
7No .NET SDK / MSBuild found (install the .NET 10 SDK — see Requirements)
130Canceled (Ctrl+C)

Scripting (PowerShell)

Designed for PowerShell Core 7+. Streams are split for clean capture — stdout carries the result (the text summary, or the manifest under --format json), stderr carries progress/diagnostics — and a stable result.json is always written. Read it with fully-qualified .NET (no cmdlets, no piping):

& dotnet chisel -t 'Contracts.IShape'-s $sln-o $out--strict
if ($LASTEXITCODE-ne0) { throw"chisel failed ($LASTEXITCODE)" }
$doc= [System.Text.Json.JsonDocument]::Parse(
[System.IO.File]::ReadAllText([System.IO.Path]::Combine($out,'result.json')))
$root=$doc.RootElement$root.GetProperty('counts').GetProperty('files').GetInt32()
foreach ($pin$root.GetProperty('packages').EnumerateArray()) {
"$($p.GetProperty('id').GetString())$($p.GetProperty('version').GetString())"
}

Or capture the manifest straight off stdout: $json = & dotnet chisel … --format json (progress still shows on stderr).

Generic types in PowerShell: single-quote any type name containing <, >, or a backtick — 'MyNS.Repository<T>' or 'MyNS.Repository`1' (both resolve). < is a reserved PS operator and the backtick is the PS escape char; single quotes pass them through literally.

See Exit codes to branch on $LASTEXITCODE; --strict turns any error-severity diagnostic into a nonzero exit.


Outputs

All of these are written into --output.

Slice.csproj — a flat, buildable project

Explicit <Compile Include> per collected file (no ProjectReferences — the slice is flattened), plus a <PackageReference> per detected NuGet package. Compilation settings (TargetFramework, LangVersion, Nullable, ImplicitUsings, AllowUnsafeBlocks, user DefineConstants) are hoisted from the contributing projects — taking the highest/strictest value when projects disagree, and warning you when they do.

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<CompileInclude="src/ExternalPackage/MyClass.cs" />
</ItemGroup>
<ItemGroup>
<PackageReferenceInclude="newtonsoft.json"Version="13.0.3" />
</ItemGroup>
</Project>

files.json — the file manifest

{
"files": [
{
"path": "C:\\...\\Shapes\\Composite\\Group.cs",
"project": "Composite",
"targetFramework": "net10.0",
"isGenerated": false,
"containsSymbols": [ "global::Composite.Group" ]
}
]
}

references.json — external references (the follow-on step's input)

NuGet packages and framework assemblies the slice depends on but does not vendor:

{
"packages": [
{
"id": "newtonsoft.json",
"version": "13.0.3",
"assemblyName": "Newtonsoft.Json",
"assemblyVersion": "13.0.0.0"
}
],
"frameworkAssemblies": [
{
"name": "System.Runtime",
"version": "10.0.0.0",
"path": "C:\\Program Files\\dotnet\\packs\\Microsoft.NETCore.App.Ref\\...\\System.Runtime.dll"
}
]
}

result.json — the machine-readable run manifest

Written on every run (success or fatal), and also printed to stdout under --format json. A single, stable, camelCase object — success flag, exit code, seed, mode, counts, the output paths, the NuGet packages, and the full diagnostics list — designed to be read directly with System.Text.Json (see Scripting).

{
"schemaVersion": 1,
"tool": { "name": "chisel", "version": "2026.2.624" },
"success": true, "exitCode": 0, "elapsedSeconds": 3.1,
"seed": { "displayName": "global::MyNS.IFoo", "filePath": ".../IFoo.cs" },
"mode": { "walkDepth": "signatures", "expansion": "seed", "sourceGenerators": "reference" },
"counts": { "inSourceTypes": 7, "files": 6, "projects": 4, "externalReferences": 3, "packages": 1 },
"packages": [ { "id": "newtonsoft.json", "version": "13.0.3" } ],
"diagnostics": [ { "severity": "Warning", "stage": "Walk", "message": "", "item": "" } ]
}

src/… — copied sources

Every collected .cs file, copied under src/<ProjectName>/… preserving the path relative to its project. Files outside their project directory (e.g. <Link> items) go under _linked/<hash>/; materialized generator output goes under _generated/.

.gitignore

A .gitignore is written into the output root so the slice behaves like a normal, self-contained repo (build artifacts under bin//obj/ stay untracked). It propagates the analyzed solution's own .gitignore (the nearest one found walking up from the .sln); if none exists, a minimal .NET default is written instead.

chisel.log — the run log

A full, timestamped copy of the run (every phase, diagnostic, and the final summary) is written to <output>/chisel.log via Serilog, reset on each run. The console shows the same information with a clean layout; --quiet restricts the console to warnings/errors while the log file still captures everything, and --verbose lists every diagnostic instead of grouping them by stage.


How it works

chisel is a pipeline over Roslyn's semantic model (not text search). It opens the solution with MSBuildWorkspace, classifies which assemblies belong to the codebase, resolves the seed type, walks the dependency graph, collects the contributing files + settings, and emits the slice.

src/Chisel.Core/ library — all slicing logic
src/Chisel.Cli/ thin console host (chisel)
tests/Chisel.Core.Tests/ xUnit tests
tests/Fixtures/ worked-example solutions (also used as tests)

For the full walk-through — every stage, the body/signature distinction, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ — see docs/GUIDE.md.


Error handling

chisel is best-effort: it would rather hand you a mostly-complete slice than nothing. A problem with one file, symbol, or reference is reported and skipped — it does not abort the run. Diagnostics are streamed to stderr as they happen and recapped in an end-of-run summary:

Diagnostics: 0 error(s), 1 warning(s) — the slice was still produced.
[Warning] TargetFramework: Project 'MultiTarget.csproj' multi-targets (net8.0, net10.0); slicing against 'net8.0'. Pass --tfm to choose.

Only four conditions are fatal (there is genuinely nothing to produce): no .NET SDK / MSBuild installed (exit 7), a missing solution file (exit 5), a workspace that fails to load without --allow-partial (exit 4), and an unresolvable/ambiguous seed type (exit 3). Everything else — a file that won't bind, a reference that won't resolve, a file that can't be copied — becomes a non-fatal diagnostic and the run still exits 0 with the slice written.

Failing to open a non-C# project (.proj, .vcxproj, .fsproj, .vbproj, …) is not fatal even without --allow-partial: those projects hold no C# to collect, so the failure is reported as a warning and the C# projects load normally.


Known limitations

  • dynamic and reflection-by-string (Type.GetType("…"), DI string registrations) are not statically traceable and are not followed; a warning is emitted when dynamic is encountered.
  • Single target framework per run. Multi-targeted projects are sliced against one TFM (--tfm to choose); code in #if regions for other TFMs is preserved in the copied file but not analyzed.
  • Source generators default to reference (the generated files are skipped and a warning tells you the generator must run downstream). Use --source-generators materialize to write the generated code into the slice for a self-contained result. See the Guide.
  • file-scoped types cannot be used as the seed (they have no addressable metadata name).

Troubleshooting

SymptomFix
No .NET SDK was found (exit 7)Install the .NET 10 SDK and ensure dotnet is on your PATH. A self-contained binary still needs it.
Type ... not foundCheck the FQN and arity (Foo<> not Foo); ensure the solution is restored.
... is ambiguous (exit 3)Pass --project <name> to pick the declaring project.
Workspace load failure (exit 4)dotnet restore the solution; if one project is broken, try --allow-partial.
Slice misses a type referenced via dynamic/reflectionExpected — add it manually (see limitations).
Slice won't compile due to a missing generated typeRe-run with --source-generators materialize.

Documentation

DocumentPurpose
docs/GUIDE.mdThe deep dive: how the walk works, every output format, source generators, multi-targeting, global usings, mixed project settings, embedding the Core library, and a fuller FAQ.
CONTRIBUTING.mdDev setup, the test-fixture step, coding conventions, and the PR checklist.
LICENSEMIT license text.

Contributing

Contributions welcome — see CONTRIBUTING.md for the full guide (dev setup, the test-fixture step, coding conventions, and the PR checklist). Please open an issue before submitting a pull request for non-trivial changes.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes, including tests
  4. Prepare the test fixtures, then run the suite:
    pwsh build/restore-test-fixtures.ps1 # restores the fixture solutions + builds the SourceGen generator
    dotnet test
  5. Submit a pull request

The example solutions under tests/Fixtures/ are not part of Chisel.slnx, so they must be restored before dotnet test (the restore-test-fixtures.ps1 helper does this). See docs/GUIDE.md for the developer guide.

If you find this tool useful, I accept tips / donations:

❤️ ~B Sponsor


License

MIT © 2026 Brian Bennewitz — see LICENSE.

About

Carve out the minimal set of C# source files needed to compile a single type — a clean slice of a larger solution.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages