Skip to content

Add draft of combined tool + library packages#337

Open
baronfel wants to merge 3 commits into
mainfrom
make-combined-package-spec
Open

Add draft of combined tool + library packages#337
baronfel wants to merge 3 commits into
mainfrom
make-combined-package-spec

Conversation

@baronfel

Copy link
Copy Markdown
Member

This adds a spec for an idea I've been discussing with @joelverhagen and @DamianEdwards now that we have a very lightweight tool-manifest package pattern with .NET 10's RID-specific tool packages.

The short form is that users should be able to author one package that can be used as a tool and a PackageReference, without

a) impacting the behavior of either mode,
b) incurring large download size increase costs, and
c) sacrificing simplicity of project files

Comment thread proposed/combined-tool-library-packages.md Outdated
Comment thread proposed/combined-tool-library-packages.md Outdated
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
```

However, ideally none of the _entrypoint_ (or code only used by the entrypoint) of the application would be included in the library portion of the package. To achieve this, we can condition the removal of the Entrypoint-related code from the project:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it so bad to include Program.cs in the library? I guess it would lead some untrimmed code?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, exactly - it's not required per se, but I want to aim for the moon here in terms of not influencing the library code from the program if possible.

<!-- JsonSchemaValidator.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does multi-targetting play in here? If I had <TargetFrameworks>net10.0;net9.0</TargetFrameworks> would this impact both the library and the tool assets?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! the answer depends on the kind of tool. framework-dependent, platform-agnostic tools would bundle the N TFM variants of the tool in the package. framework-dependent, platform-specific tools could make the N platform-specific tools, but our existing model actually doesn't handle TFM variants of platform-specific tools at this time. I don't actually know what would happen here. For self-contained tools, there is no need to have TFM-specific variants at all, and so we should strongly consider taking the 'newest' TFM and only making the platform-specific variants for that TFM.

Comment thread proposed/combined-tool-library-packages.md
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PackAsTool>true</PackAsTool>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like there should be a way to opt in to the new experience. This .csproj looks just like an existing tool project, but now it's magically adding Dependency assets? Or am I missing something?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does look like a tool package - but the key signal here is the lack of OutputType Exe. This project is actually a library - that's our signal. "This library wants to be packed like a tool, too!"

We can of course have a signal, but I'm trying to angle towards as light of a touch as possible on the project file.

- Combined packages can be consumed as either tools or libraries by consumers unaware of the dual nature
- NuGet clients that don't understand combined packages treat them as either tools or libraries based on which assets they recognize

## Q & A

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does dotnet publish do on this project?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

publishes the project as a library, by default. This is because the project has no OutputType set, so the defaults it to a Library OutputType.

Co-authored-by: Joel Verhagen <joel.verhagen@gmail.com>
└── [package metadata files]
```

### Compatibility Considerations

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many tools are going to be confused when they see a library that is executable with an entry point.

For example, NativeAOT compiler will choke when it sees multiple binaries with entrypoint on the command line here: https://github.com/dotnet/runtime/blob/1e860a4bd3915f35d51cc079cff212322a6991e7/src/coreclr/tools/aot/ILCompiler/Program.cs#L218 . I am pretty sure that diagnostic tools like debuggers will get confused in similar way. it would be a very long tail to find and fix all these.

You really want to have separate binary for the tool and for the library.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's good confirmation - we do today, and will continue to have in this proposal, separate binaries for the tool usage and the library usage. This layout is intended to show this separation. The idea would be that the entrypoint would not be part of the library-use-case dlls in any way - only the tool-use-case dlls.


.NET tools provide a convenient way to distribute and consume command-line utilities, but they exist in isolation from the libraries they might be built upon. This means that a developer must create multiple packages, push multiple package ids to feeds, consumers must search for and use seemingly-random package names, etc. Our current model forces a package author to choose which _modality_ of package consumption (tool or reference) gets the 'nice' package name, while the other unused modality gets whatever is left. Allowing for the nice/concise/expected package name to fill both needs helps with name recognition and makes a tool/library appear more cohesive. This mirrors patterns common in Node.js (`npm run <command>` / `npm bin <command>`) and Go (`go install` for CLI, regular import for library usage), bringing similar convenience to the .NET ecosystem.

## Scenarios and User Experience

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking at this space. I'd like to see the package format expand to allow tools and libraries in the same package. However, I think the idea of doing that with one project should be a separate proposal. Combining the two ideas is confusing.

@javiercn

javiercn commented Jul 3, 2026

Copy link
Copy Markdown
Member

We have a concrete, shipping use case for this in the Blazor gateway, and it maps almost exactly onto the two modalities described here.

The Blazor gateway (Microsoft.AspNetCore.Components.Gateway in dotnet/aspnetcore) is an ASP.NET Core executable that fronts standalone Blazor WebAssembly apps: it serves the static web assets, adds OTLP telemetry, and does service discovery / reverse proxying to backends. The gateway is multi-app by design (it can front several standalone WASM apps at once, as in an Aspire graph); the single-app case is just an optimization of that. We need to consume this one artifact both as a PackageReference and as a tool, without splitting it into separate package ids.

Scenario 1: standalone Blazor WebAssembly dev inner loop (PackageReference).
For development, we simply want a PackageReference to consume the gateway in the app. The package carries build logic (a build/…targets) that rewrites RunCommand/RunArguments, so dotnet run and F5 transparently launch the gateway to host the app locally (dev-time static assets, telemetry, backend routing). The developer adds one package and their run/launch stack changes to "run behind the gateway," with no project-file gymnastics and no second package to discover.

Scenario 2: Aspire orchestration (tool, driven internally by the integration).
In Aspire we want the AppHost to run the same gateway across the standalone WASM apps in the graph. Today we do this with a file-based C# app (Gateway.cs) that we pack into Aspire.Hosting.Blazor and copy into the AppHost output, then run. That is a workaround. What we actually want is to consume the real gateway through Aspire's existing dotnet-tool integration. The user does not write this: our Blazor integration calls AddDotnetTool internally and passes the gateway its arguments (static web assets manifests, endpoints manifests, path prefixes, logging filters, and so on):

// inside Aspire's Blazor integration, not user code
builder.AddDotnetTool(name, "Microsoft.AspNetCore.Components.Gateway")
       .WithArgs(/* per-app manifests, path prefixes, ... */);

Scenario 3: validating a published standalone app (tool).
We also want an easy way to run the published output of a standalone WASM app to validate it (not just the dev build). Because the same package is a tool, a user (or CI) can acquire the gateway on demand and point it at the publish output to serve and smoke-test the real published assets, using the same tool-acquisition path rather than a bespoke server. No dev build, no extra package.

Why the combined package matters to us.
These experiences must stay in lockstep: same gateway behavior, same version, same configuration surface. If they diverge, a standalone project, an Aspire-orchestrated graph, and a published-output check behave differently, which is a support nightmare. A combined tool + library package as proposed here lets us ship a single package id that is simultaneously tool-acquirable and PackageReference-able with build targets, retire the file-based Gateway.cs in Aspire, and converge on the real gateway binary. Leaning on the RID-specific tool manifest model also keeps the reference modality small, which matters because the gateway is an executable.

Desired end-state experience

  • Standalone WASM dev: add one PackageReference, dotnet run/F5 hosts the app behind the gateway.
  • Aspire: the Blazor integration internally acquires the same gateway as a tool and runs it over the WASM apps in the graph, with no copied source files and no separate package.
  • Published-output validation: acquire the same tool on demand and point it at the publish output to validate the real published app.

All three resolve the same package id at the same version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants