[NOTE: temporarily references the original package, not this fork's package]
dotnet-subset is a .NET tool that copies a subset of files from a repository to a directory.
The tool is mainly used in Dockerfiles to optimize the docker build caching for "dotnet restore" instructions.
To learn more about the motivation behind dotnet-subset, please read the related blog post.
- Two commands:
restore(files needed todotnet restore) andcopy(all files from the involved projects, plus everythingrestorewould copy). - Accepts a single project (
.csproj), a solution (.sln), a solution filter (.slnf) or the new XML solution format (.slnx) as input. - Copies all the required files for the root projects, including their project dependencies transitively.
- Copies imported MSBuild files, including Directory.Build.props and Directory.Build.targets.
- For each required project, copies all NuGet configuration files involved in computing its effective NuGet settings. See How NuGet settings are applied.
dotnet-subsetalso supports custom NuGet configuration filepath defined in the project's csproj. - For each required project, copies the NuGet lock file and support the
NuGetLockFilePathproperty. Seehttps://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#lock-file-extensibility. - Only copies files under the specified root, while maintaining their relative path to it.
dotnet tool install --global dotnet-subset
Prerequisite: .NET SDK 10.0 or newer
- Clone this repository
- Open a terminal in the repository's root
dotnet pack --configuration Release --version-suffix localdotnet tool update dotnet-subset --global --prerelease --configfile .config/nuget-local-install-release.config
Description:
Create a subset for the restore operation.
Commands
- `restore`: creates a subset of the files required to `dotnet restore` (project files, imported MSBuild files, NuGet configs and lock files).
- `copy`: creates a subset of all files from the involved projects' directories, plus everything `restore` would copy (imports, NuGet configs/lock files, the solution file). So `copy` is a superset of `restore`.
Usage:
dotnet-subset <command> <projectOrSolution> [options]
Arguments:
<command> `restore` or `copy`
<projectOrSolution> Project (.csproj), solution (.sln/.slnx) or solution filter (.slnf) to process.
Options:
--root-directory <root-directory> (REQUIRED) Directory from where the files will be copied, usually the
repository's root.
--output <output> Directory where the subset files will be copied,
preserving the original hierarchy. Required unless
--dry-run is specified.
--dry-run Do not copy files, just print the list of files
that would be copied.
-?, -h, --help Show help and usage information
Example with a project:
dotnet subset restore /source/complexapp/complexapp.csproj --root-directory /source/ --output /tmp/restore_subset/
Example with a solution (.sln or .slnx):
dotnet subset restore /source/complexapp.sln --root-directory /source/ --output /tmp/restore_subset/
Example with copy to grab every file from the involved projects:
dotnet subset copy /source/complexapp.sln --root-directory /source/ --output /tmp/copy_subset/
A solution filter only defines the root projects of the traversal - it does not restrict the result to just those projects. The tool still walks each root project's ProjectReference recursively and pulls in transitive dependencies, even ones not listed in the filter, as long as they live under --root-directory:
copied = (projects from the filter) plus (their transitive ProjectReference dependencies under root)
Everything that ends up in the result must be located under --root-directory. If the root is narrower than needed, transitive dependencies whose path is outside the root are skipped, and "upper" imports/configs (Directory.Build.props/.targets, nuget.config) above the root are cut off. The root should cover all dependent projects and all required imports/configs - usually the solution directory, not a single project's directory.
Please check these pull requests to see how to use dotnet-subset in your Dockerfile:
- https://github.com/othmane-kinane-nw/eShopOnContainers/pull/1/files?diff=unified&w=0
- https://github.com/othmane-kinane-nw/modular-monolith-with-ddd/pull/1/files?diff=unified&w=0
- https://github.com/othmane-kinane-nw/dotnet-docker/pull/1/files?diff=unified&w=0
Using both commands together lets you cache two layers independently:
- The restore subset (
restore) is small - project files, imports and NuGet configs. It feeds thedotnet restorelayer, which stays cached until your dependencies change. - The build subset (
copy) is all the source of the involved projects, transitively. It feeds the build/publish layer, which stays cached until those projects' files change - changes to unrelated projects (tests, other services) don't touch it.
A single up-front subset stage extracts both; the later stages each pull in only the subset they need, so each cache is invalidated only by what genuinely affects it. The example below is generalized from a real production Dockerfile:
# syntax=docker/dockerfile:1.7.0-labs
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS subset
WORKDIR /src
# local folder works as a package source, so the tool (or any package) installs without a
# NuGet feed - handy for this fork, which is not published to NuGet as of 2026-06-14.
COPY custom-packages custom-packages
RUN dotnet tool install dotnet-subset --global --no-cache --version 0.4.0 --add-source custom-packages
ENV PATH="$PATH:/root/.dotnet/tools"
COPY . .
RUN dotnet subset restore "./Services/MyApp.Service/MyApp.Service.csproj" --root-directory /src --output /restore_subset
RUN dotnet subset copy "./Services/MyApp.Service/MyApp.Service.csproj" --root-directory /src --output /build_subset
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# copy shared files (binaries, referenced via HintPath (e.g. ..\lib\), etc.) explicitly - dotnet-subset can't see them.
COPY libs libs
COPY --from=subset /restore_subset/ .
# or use nuget.config - up to you
RUN --mount=type=secret,id=feed_pat \
dotnet nuget add source \
"https://pkgs.dev.azure.com/your-org/your-project/_packaging/YourFeed/nuget/v3/index.json" \
--name YourFeed --username az --password "$(cat /run/secrets/feed_pat)" --store-password-in-clear-text \
&& dotnet restore "./Services/MyApp.Service/MyApp.Service.csproj" -r linux-x64
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
COPY --from=subset /build_subset/ .
WORKDIR "/src/Services/MyApp.Service"
RUN dotnet publish "./MyApp.Service.csproj" -c $BUILD_CONFIGURATION -o /app/publish -r linux-x64 --no-self-contained --no-restore /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApp.Service.dll"]This is a fork of nimbleways/dotnet-subset. It merges two upstream pull requests and adds a few features on top. Changes compared to the original:
copycommand (from upstream PR #3, reworked): alongsiderestore, copies every file from the involved projects' directories plus everythingrestorewould copy. The command logic is factored into a sharedSubsetCommandBasewithRestoreSubsetandCopySubsetderiving from it.- --dry-run option: Performs the full analysis and prints the files that would be copied, but writes nothing to the output directory.
--outputis optional in this mode: when omitted, there is no destination to compare against, so the summary counts every file as to-be-copied. - Solution filter (
.slnf) support (from upstream PR #26): a.slnfis parsed for its root projects, then dependencies are resolved transitively (see Solution filters above). - XML solution (
.slnx) support: the new XML solution format is handled natively, behaving the same as a classic.sln. copyincludes restore files:copyalso pulls in the NuGet configs thatrestorecopies, so its output stays restore-ready (copyis a superset ofrestore).- Modernized to .NET 10 / current SDK: target framework moved to
net10.0, MSBuild and related packages upgraded, the project switched to a strongly-typedFileInfo/DirectoryInfostyle with dedicated exceptions. - Functional tests covering the
restore/copycommands,.slnf/.slnxinputs, and the transitive-dependency behavior of solution filters.
WARNING: the LLM was used to help me combine two upstream PRs and write new functional tests.
A few subtleties of the additions above (copy, .slnf, .slnx), verified empirically while building this fork:
- The solution file itself is copied for
.sln/.slnx, but not for.slnf. When the input is a solution (.slnor.slnx), the solution file is added to the output. When the input is a solution filter (.slnf), the.slnfis not copied - its extension is not.sln, so it is not treated as a solution file. - Only the projects listed in the solution are traversed. A project that sits in the directory tree but is not referenced by the solution (and is not a transitive dependency of a listed project) is not copied.
copyis a strict superset ofrestorefor the same input: it copies every file from the involved projects' directories plus everythingrestorewould copy (imports, NuGet configs/lock files, the solution file)..slnxis handled natively,.slnfis not. After the MSBuild upgrade,SolutionFile.Parsereads.slnxcorrectly and returns the solution's projects, so.slnxis treated exactly like.sln. The.slnfsupport, taken from upstream PR #26, keeps a dedicated JSON parser on purpose: the sameSolutionFile.Parseexpands a.slnfto its base.slnbut does not apply the project mask - it returns all projects of the base solution rather than the filtered subset. (Centralized.slnfsupport is still an open request: vs-solutionpersistence#85.)
Copyright © Nimbleways, Othmane Kinane, snipervld and contributors.
dotnet-subset is provided as-is under the MIT license. For more information see LICENSE.
- For Microsoft.Build, see https://github.com/dotnet/msbuild/blob/main/LICENSE
- For Microsoft.Build.Locator, see https://github.com/microsoft/MSBuildLocator/blob/master/LICENSE
- For System.CommandLine, see https://github.com/dotnet/command-line-api/blob/main/LICENSE.md
