blog post image
Andrew Lock avatar

Andrew Lock

~21 min read

Supporting platform-specific .NET tools on old .NET SDKs

Exploring the .NET 10 preview - Part 8

Share on:

In this post I look in more depth at the platform-specific (self-contained or Native AOT) NuGet package support added in .NET 10, and in particular look at how you can continue to support users working with older versions of the .NET SDK.

I start by discussing what the new platform-specific features mean to .NET tool authors, in terms of advantages, trade-offs, and implications. I then discuss some possible approaches that attempt to give the best of both worlds: improvements for .NET 10 SDK users, but continued support for .NET 9 SDK and earlier users.

The evolution of .NET tools in .NET 10

I have talked about .NET tools in my last couple of posts, but the reality is that nothing much changed about authoring or consuming .NET tools for a long time. .NET Core 3.0 introduced "local" tools back in 2019, and they pretty much stayed the same since then.

However in .NET 10, we suddenly have new features πŸŽ‰

In this post I discuss how these two features interact and their implications when thinking about the maintenance and support of a .NET tool, as a tool author.

What do these new features mean for tool authors?

The new features in .NET 10 are cool for consumers of packages, but what do they mean for tool authors?

One-shot tool running with dnx

We'll start by thinking about the dnx "one-off" tool running scenario. The primary benefit of this feature is to the consumers of packages, as they don't need to run two commands just to run a tool. It also means they don't "pollute" their path, among other minor things, for something that they only want to run once.

As a package author, I don't think there's much for you to think about here. Regardless of how you've written your package, the consumer-side feature works pretty much the same. Customers need to be using the .NET 10 SDK, but you can pack your tool using any SDK, so there's no limitations there. And the runtime requirements for your package are the same regardless of whether they are run using dnx or dotnet tool install.

I think the one aspect you could tweak to improve the experience for consumers of your package is to keep your packages as small as possible. The first time a consumer runs dnx, .NET must download your package, and the smaller the package is, the quicker this will be. This applies to the "normal" dotnet tool install path too, but the "one-off" nature of dnx adds additional weight to the size aspect I think.

Platform-specific tools

The other feature, platform-specific tools, has more nuance to it, as it requires changes to the .nupkg packages themselves, and isn't solely an SDK feature. It puts requirements on the author-side (you need to use the .NET 10 SDK to produce platform-specific tools), but it also puts requirements on the consumers of the package.

As far as I can tell, there are three main advantages to platform-specific tools:

  1. Simpler support matrix
  2. Reduced package size
  3. Faster startup for Native AOT tools

I'm thinking primarily about self-contained or nativeAOT packages for these advantages, but I'll explain each advantage in more detail below.

1. Simpler support matrix with self-contained tools

As I explained in a previous post, if you really want to support as many consumers of your package as possible, you need to compile your tool for all the .NET runtime versions you support. For example if you want to support .NET 6+ that means you need to build and ideally test your tool against .NET 6, .NET 7, .NET 8, .NET 9, and soon .NET 10. That's easy enough in practice, but it's also a bit of a pain.

As a reminder, this is necessary because you don't know what .NET runtime or SDK version consumers may have installed when they try to use your tool.

You can potentially partially work around this issue by only building for .NET 6 (given the example above), and relying on setting RollForward=Major to ensure consumers can still run the tool if they only have .NET 8 installed (for example). That reduces the "build" burden, but you still need to test your tool on newer runtimes, as there can potentially be breaking changes between major versions. If there are breaking changes that affect you, then you might not be able to rely on the rollforward setting at all.

With self-contained or NativeAOT tools, the whole support matrix issue goes away. You only need to support a single target framework, the one you pack into the package. This takes away a bunch of complexity, avoids potential issues related to rollforward and simplifies your life overall as a package author by removing this variable. And on the consumer side, you hopefully get a more reliable experience too.

2. Reduced package size

Given you're only packing your tool for a single platform and framework with self-contained/NativeAOT tools, you may be able to significantly reduce the size of the resulting package. This will inevitably be very tool-specific:

  • Some tools can't be NativeAOT compiled, and so may need to bundle more of the underlying runtime in the package, especially if your tool isn't trim-safe.
  • If a tool has native platform-specific dependencies, the platform-specific tool can significantly reduce the number of dependencies required, by only packing a single file, instead of one per platform.
  • If a tool supports many target frameworks (for maximum compatibility with consumer environments) you may be able to save a lot of space by switching to only including the single self-contained runtime.

The sample app in my previous post was the perfect example of how the platform-specific tools can significantly reduce the size of the package, due to a large number of supported frameworks, and a native dependency that supports many target platforms:

Comparing framework-dependent with Native AOT packages

All those files mean a difference of 92MB to 2.6MB!

Bear in mind, this is pretty much a worst/base case example, given the large number of supported frameworks and the large number of native dependencies. Your mileage may vary.

Of course, disk space is cheap, so should you really care? The answer, as always, is it depends. Depending on how your tool is used, the size difference may or may not make a big difference.

That said, a smaller package is clearly better for the dnx feature of .NET 10, as the assumption is that consumers often won't have the tool downloaded. The bigger the package, the slower it will be to download, and the longer the user has to wait for your tool to execute. Smaller packages are always going to be better in these cases.

3. Faster startup

Having a smaller package should make dnx executions faster the first time, but once the tool is downloaded, the size of the package doesn't really matter that much. However, if you have native AOT compiled your application, then you'll consistently benefit from the faster startup characteristics.

Fast startup times are one of the key benefits of native AOT compiling your .NET application. They also often have a smaller memory footprint when running, as there's no JIT compiler running (among other reasons).

Even if you're not native AOT compiling your application, then another theoretical benefit of a self-contained application is that you may be able to benefit from the performance improvements of a newer runtime. Every version of .NET gets a bit faster; so if consumers of your package use the included .NET 10 runtime instead of an earlier runtime, then your tool could see performance improvements.

However, as self-contained tools require the .NET 10 SDK, that means customers will definitely have the .NET 10 runtime available, so you likely won't see performance improvements today compared to a framework-dependent package. For that reason, I've not considered this as a benefit today, though in the future, as more .NET SDKs are released this calculation may change.

So we have three potential benefits of platform-specific tools: a simpler support matrix, reduced package size, and Native AOT startup time improvements. Unfortunately, it's not all roses. There are downsides to adopting platform-specific tools too.

Downsides to platform-specific tools.

In my previous post I discussed some of the implications of using the platform-specific tools feature, and I also mentioned it above. The crux of the problem is that by default, using platform-specific tools means consumers must be using the .NET 10 SDK. If they try to install a platform-specific tool when using an earlier SDK, they'll get an error like this:

> dotnet tool install sayhello

Tool 'sayhello' failed to update due to the following:
The settings file in the tool's NuGet package is invalid: Command 'sayhello' uses unsupported runner ''."
Tool 'sayhello' failed to install. Contact the tool author for assistance.

or like this:

> dotnet tool install sayhello.win-x64

Tool 'sayhello.win-x64' failed to update due to the following:
The settings file in the tool's NuGet package is invalid: Command 'sayhello' uses unsupported runner 'executable'."
Tool 'sayhello.win-x64' failed to install. Contact the tool author for assistance.

This all stems from the DotnetToolSettings.xml that's included in the .nupkg package and tells the .NET SDK how to run your app. For .NET 9 and below (and for framework-dependent, platform-agnostic tools in .NET 10) the file looks something like this:

<?xml version="1.0" encoding="utf-8"?>
<DotNetCliTool Version="1">
  <Commands>
    <Command Name="sayhello" EntryPoint="MultiRid.dll" Runner="dotnet" />
  </Commands>
</DotNetCliTool>

As you can see, this uses Version=1 for the schema, and has Runner=dotnet for the Command entry. In contrast, if we look at the DotnetToolSettings.xml files for your .NET 10 platform-specific tools, these use schema Version=2, and a different (or missing) Runner.

The DotnetToolSettings.xml file in the platform-specific package looks like this, for example:

<?xml version="1.0" encoding="utf-8"?>
<DotNetCliTool Version="2">
  <Commands>
    <Command Name="sayhello" EntryPoint="MultiRid" Runner="executable" />
  </Commands>
</DotNetCliTool>

Unfortunately, the Runner=executable is only understood by the .NET 10+ SDK, which means you can't install these packages on earlier SDKs.

The conundrum: should tool authors use platform-specific packages?

We've established that platform-specific packages may have many benefits for some .NET tools, but they can only be used by users with the .NET 10 SDK. The two questions you need to answer as a tool author are:

  1. Would my tool benefit from platform-specific packages?
  2. Is it a problem to require that my users use the .NET 10 SDK?

As I've already described, the answer to the first question will depend on your tool. Platform-specific packages give 3 main advantages in general, but these advantages are biggest if you have a large range of supported target frameworks, if your tool uses platform-specific dependencies, and if you can NativeAOT your package. If those don't apply to you, then the trade off in installation support may not be worth it.

However, if we assume your tool would benefit from being platform-specific packages, you have to consider whether the requirement that users use the .NET 10 SDK is a problem.

Depending on the tool that you're creating, this might be fine. If you're just creating an OSS tool, or a tool for your own consumption, for example, then requiring that users have the .NET 10 SDK installed might not be a problem for you. After all, in general, people should always be using the newest .NET SDK as it can still build for earlier .NET runtimes.

However, if you're building a tool where adoption and ease-of-use is your primary concern, then this may not be acceptable. This might be the case if your tool is produced by a company, if it's a part of your product, or if it's intended to run in places where .NET 10 won't.

.NET 10 doesn't support as many Linux distributions as some previous versions, dropping support for older Alpine and Ubuntu versions, for example. If you need your tool to run in those scenarios, and you want to distribute it via NuGet, then you must support earlier versions of the SDK.

This conflict made me wonder if there was some sort of middle-ground possible, a kind of "compromise" package that gives you the best of all worlds.

The best of both worlds? Framework-dependent and platform-specific tools in the same package

The compromise solution I had in mind looks something like this:

A workaround for the .NET 10 SDK compatibility problem

I'll show how to implement this sort of package shortly, but before we get into the technical details, I think it's worth discussing the pros and cons of this approach, what scenarios it supports, and the implications for both tool authors and consumers.

With this setup, you pack the framework-dependent, platform-agnostic version of your tool inside the lowest support target-framework tools folder, i.e. tools/netcoreapp3.1 in this case. In addition, in the net10.0 folder, we pack the Version=2 DotnetToolSettings.xml file that points to all our other platform-specific packages.

As a reminder, when you produce platform-specific tools, you typically produce N+1 packages, where N is the number of platforms. e.g. you produce the root package, sayhello.<version>.nupkg, sayhello.win-x64.<version>.nupkg, sayhello.linux-x64.<version>.nupkg, etc.

If you install or run this tool with the .NET 10 SDK, the SDK will download the package, read the xml file in the net10.0 folder, and then immediately fetch and run the appropriate platform-specific package, e.g. sayhello.win-x64. So if you have the .NET 10 SDK installed you get the optimised Native AOT version of the package.

For users that are using earlier versions of the SDK, anything from netcoreapp3.1 to net9.0, the SDK looks for the highest compatible tools folder (tools/netcoreapp3.1) and reads the xml file it contains, which is a standard Version=1 file:

<?xml version="1.0" encoding="utf-8"?>
<DotNetCliTool Version="1">
  <Commands>
    <Command Name="sayhello" EntryPoint="MultiRid.dll" Runner="dotnet" />
  </Commands>
</DotNetCliTool>

The older SDKs just see a normal, "traditional", framework-dependent platform-agnostic .NET tool and can run it without issue.

On the face of it, this looks like you have the best of both worlds:

  • .NET 10 SDK users can use a NativeAOT version of the tool distributed in platform-specific packages.
  • Earlier SDK users can still use the .NET tool, just as they would before.

However, it's not quite as simple as that. Let's consider the three advantages of platform-specific tools I highlighted earlier:

  1. Simpler support matrix. ❌ With this model you're not simplifying your matrix. Instead of replacing your support matrix with a single self-contained tool, your existing support matrix stays the same, assuming you were also always going to add support for .NET 10 (likely self-contained or Native AOT compiled).
  2. Reduced package size. ❌ This also likely doesn't apply. Your "root" package contains the same target framework files as in the previous (non-platform specific) version of the package. So users on old SDKs have the exact same package size, and users on the .NET 10 SDK have the combination of the "root" package and the platform-specific package to download!
  3. Faster startup for Native AOT tools. βœ… This one still applies. For .NET 10 SDK users, once you've downloaded the platform-specific Native AOT package, they'll benefit from the Native AOT advantages.

So on the face of it, this approach currently only seems to make sense if you are producing Native AOT platform-specific packages, which will see the associated startup benefits.

It's worth pointing out that this cost-benefit calculation applies today, but that it will change over time. In the future, when .NET 11, .NET 12, or .NET 13 SDKs are released, your package doesn't need to change. Those newer users can still install your .NET 10 tool, and your package size and support matrix doesn't need to change, because you can support those users with the existing NativeAOT tool.

In addition, as time goes on, you may well want to reduce your support for earlier versions of the .NET SDK. That will likely involve removing support for earlier runtimes from your root package, or relying on RollForward=Major for support instead. All of which means this approach may become more useful with time than it is today.

So unfortunately there's still not a "correct" answer here. It really depends on your requirements for supporting older .NET SDKs and how much you're willing to "penalise" users who are on the .NET 10 SDK with larger package versions.

Case-study 1: the Datadog dd-trace .NET tool

There's so many pros and cons at play here, it's hard to grasp onto something concrete, so I thought it would make sense to consider some concrete examples, starting with the Datadog dd-trace .NET Tool.

The Datadog dd-trace tool provides an easy way to add automatic instrumentation to your application. For example, after installing the tool you can instrument your app something like this:

dd-trace run -- MyApp.exe

The exact details of how to use the tool or how it works aren't important for this discussion, the more important point is that we support basically all of the frameworks that we can instrument:

The contents of the dd-trace tool showing support for .NET Core 2.1

In addition, we also ship self-contained versions of the tool for some platforms which we make available as direct download links rather than using the .NET SDK.

The question is whether platform-specific packages make sense for the dd-trace .NET tool πŸ€”

The obvious first point is that we can't require our users to use the .NET 10 SDK. We need to support users with whatever tools they're currently using, which means if we do anything with platform-specific packages, we would need to ship the "combination" package I described in the previous section (and which I'll show how to create in the next section).

But would that actually be useful? We already create self-contained (trimmed) builds of our tool, but we don't produce Native AOT builds, and I don't think we will be able to. Bearing that in mind, and considering the theoretical benefits:

  1. Simpler support matrix. ❌ We are shipping for all TFMs from .NET Core 2.1-.NET 10 whichever approach we take.
  2. Reduced package size. ❌ The size is actually much worse with platform-specific packages, as .NET 10 SDK users pay the cost of the existing tool (~135MB), and then the cost of a self-contained (trimmed) package (~50MB). In contrast, simply adding .NET 10 to the existing package would only add ~5MB to the package size
  3. Faster startup for Native AOT tools. ❌ We don't ship the tool as a NativeAOT tool, so there will likely not be any performance benefits.

So it seems like shipping a platform-specific package for dd-trace simply doesn't make sense. That said, it's possible we could redesign some of the tool internals so that it does make more sense, but without the potential benefits of Native AOT it's hard to justify.

Case study 2: the sleep-pc .NET tool

As a contrast to dd-trace is a small tool I wrote the other week to solve a simple problem called sleep-pc. sleep-pc simply sends a Windows PC to sleep after a specified duration.

A short post on this tool is coming shortly!

In general this is a tool for me, which I don't expect (or particularly want) to be widely used, so I could simply require the .NET 10 SDK. However as an exercise, I decided I would support .NET 8+. What's more, I could easily NativeAOT compile this tool.

In this case, the compromise-package approach works pretty well. If we look inside the sleep-pc package, we can see the framework-dependent platform-agnostic tool is in the net8.0 folder, so the tool supports the .NET 8 SDK+, and we have a link to the Native AOT compiled sleep-pc.win-x64 package in the net10.0 folder:

The sleep-pc tool root package contents

What's more, this "root" package is only 17KB, so you really aren't paying much size cost for having to download it first on .NET 10 (in addition to the 1.5MB Native AOT package). So in this case I think the compromise-package approach works well πŸŽ‰.

Implementing the compromise package

Hopefully we've established that the compromise-package approach, in which we embed one or more framework-dependent platform-agnostic versions of a .NET tool into a "root" package, may be useful. In this section I'll (finally) show how to implement it.

The good news is that it's actually relatively simple to do. The basic steps are:

  • Target all the runtimes you need. You must target at least .NET 10 plus one or more older runtimes.
  • Add conditions to your .csproj file so that you apply the self-contained/Native AOT properties to the net10.0 target only.
  • Conditionally set the <TargetFramework>/<TargetFrameworks> based on the value of RuntimeIdentifier (which is optionally specified when you're calling dotnet pack)
  • Run dotnet pack to produce the root package, and then dotnet pack -r <runtime> for each of your supported runtimes.

The .csproj below shows a concrete example based on the "sayhello" example from my previous post.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <!-- We specify these later, conditionally -->
    <!-- <TargetFrameworks>netcoreapp3.1;net10.0</TargetFrameworks> -->
    <LangVersion>latest</LangVersion>

    <!-- Standard tool settings -->
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>sayhello</ToolCommandName>
    <PackageId>sayhello</PackageId>
    <PackageVersion>1.0.0</PackageVersion>
    <Authors>Andrew Lock</Authors>
    <Description>A tool that says hello</Description>
    <PackageTags>ascii;art;figlet;console;tool</PackageTags>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <RollForward>Major</RollForward>
  </PropertyGroup>
  
  <!-- Package dependencies  -->
  <ItemGroup>
    <PackageReference Include="Spectre.Console" Version="0.50.0" />
    <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.8" />
  </ItemGroup>

  <!-- If we're running 'dotnet pack -r <runtime>' then             -->
  <!-- RuntimeIdentifier will have a value, and we explicitly        -->
  <!-- target .NET 10. This produces the platform-specific packages. -->
  <PropertyGroup Condition="$(RuntimeIdentifier) != ''">
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>

  <!-- By default, 'dotnet pack' does not have a RuntimeIdentifier  -->
  <!-- so the 'root' package will know about both of these runtimes -->
  <PropertyGroup Condition="$(RuntimeIdentifier) == ''">
    <TargetFrameworks>netcoreapp3.1;net10.0</TargetFrameworks>
  </PropertyGroup>
  
  <!-- For .NET 10, dotnet pack will treat the app as a Native AOT  -->
  <!-- package so it _won't_ pack it in the root package, and will  -->
  <!-- create a Version=2 DotnetToolSettings.xml file in the root.  -->
  <PropertyGroup Condition="$(TargetFramework) == 'net10.0'">
    <RuntimeIdentifiers>linux-x64;linux-arm64;win-x64;win-arm64;osx-arm64;</RuntimeIdentifiers>
    <PublishSelfContained>true</PublishSelfContained>
    <PublishAot>true</PublishAot>
    <PublishTrimmed>true</PublishTrimmed>
    <StripSymbols>true</StripSymbols>
  </PropertyGroup>
  
</Project>

The 'trick' to creating this compromise package is the conditional setting of frameworks when you're creating the 'root' package with dotnet pack versus the platform-specific packages with dotnet pack -r <runtime>.

When you run dotnet pack as-is, $(RuntimeIdentifier) is empty, so the pack tool sees both netcoreapp3.1 and net10.0 target frameworks. Additionally, it sees that the net10.0 target is going to be Native AOT compiled, so it shouldn't include it in the root package. For the netcoreapp3.1 target it just sees a normal framework-dependent, platform agnostic build, so it packs that using the Version=1 xml file.

In contrast, when you run dotnet pack -r <runtime>, $(RuntimeIdentifier) has a value, so the pack tool only sees the net10.0 target framework. This is important because .NET Core 3.1 doesn't support Native AOT so it would fail to build, but even if this was net8.0 or something which does support Native AOT, we don't want (or need) to include it in the platform-specific package; it would just be dead weight.

When you build this package you get a root package that supports .NET Core 3.1+, as I showed earlier:

A workaround for the .NET 10 SDK compatibility problem

And additionally, you'll have a Native AOT platform-specific package:

The platform-specific package contents

So if the compromise-package works for your use-case: win-win!

Alternative compromises

When I was initially discussing the possibility of a compromise package on the .NET SDK repo, Chet Husk described a slightly different approach that he had taken with the Aspire CLI. This approach is by necessity pretty hacky, as it has to dive into the internals of the pack command and tweak things. Overall, the result is quite different to my previous compromise package proposal.

This alternative takes the following approach:

  • The tool targets a single target framework, anything .NET 9 or below.
  • A framework-dependent platform agnostic version of the tool is embedded in the "root" package
  • The DotnetToolSettings.xml file embedded in the root package is tweaked to look a bit like a cross between a Version=1 file and a Version=2 file

That last point is a surprising one, in that it really doesn't feel like it should work πŸ˜… The resulting root package looks something like this:

The alternative compromise package

The xml file is marked as a Version=1 file, and has Runner=dotnet for the command so it can be run by older .NET SDKs without issue. The interesting point is that apparently the .NET 10 SDK still reads the RuntimeIdentifierPackage elements, even though the file isn't Version=2.

Which makes me wonder… why was a breaking Version=2 needed if a purely additive solution to Version=1 would have worked? 🀷

Overall, this alternative package has many of the same pros and cons as my compromise package. There are a few notable differences:

  • My compromise package requires that you multi-target, with at least one framework being .NET 10, the alternative package makes most sense with a single non-.NET 10 framework.
  • My compromise package generally requires that your platform-specific package is .NET 10, whereas the alternative package uses the same framework for everything.

I think that the differences between the approaches mean one isn't necessarily always better than the other, but you might find it suits some scenarios better than others.

For completeness, the following is a complete .csproj file for the tool generated above. Just be aware that it's somewhat messing with the internals of the Microsoft.NET.PackTool targets:

<Project Sdk="Microsoft.NET.Sdk">

  <!-- Standard tool settings -->
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <LangVersion>latest</LangVersion>
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>sayhello</ToolCommandName>
    <PackageId>sayhello</PackageId>
    <PackageVersion>1.0.0</PackageVersion>
    <Authors>Andrew Lock</Authors>
    <Description>A tool that says hello</Description>
    <PackageTags>ascii;art;figlet;console;tool</PackageTags>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <RollForward>Major</RollForward>
  </PropertyGroup>
  
  <!-- Package dependencies  -->
  <ItemGroup>
    <PackageReference Include="Spectre.Console" Version="0.50.0" />
    <PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.8" />
  </ItemGroup>

  <!-- target .NET 10 and configure AOT etc-->
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <RuntimeIdentifiers>linux-x64;linux-arm64;win-x64;win-arm64;osx-arm64;</RuntimeIdentifiers>
    <PublishSelfContained>true</PublishSelfContained>
    <PublishAot>true</PublishAot>
    <PublishTrimmed>true</PublishTrimmed>
    <StripSymbols>true</StripSymbols>
  </PropertyGroup>
  
  <!-- A bunch of hackery to embed the FDD build in the root package -->
  <!-- and to fix the xml files to look like V1 -->
  <PropertyGroup Condition="$(RuntimeIdentifier) == ''">
    <_ToolPackageShouldIncludeImplementation>true</_ToolPackageShouldIncludeImplementation>
    <GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);_MakeV2ConfigLookLikeV1</GenerateNuspecDependsOn>
  </PropertyGroup>

  <Target Name="_MakeV2ConfigLookLikeV1">
    <!-- Use the XmlPoke Task to make the v2 config also look like a v1 config -->
    <XmlPoke
            XmlInputPath="$(_ToolsSettingsFilePath)"
            Value="1"
            Query="/DotNetCliTool/@Version"
            Namespaces="$(Namespace)"/>
    <XmlPoke
            XmlInputPath="$(_ToolsSettingsFilePath)"
            Value="&lt;Command Name=&quot;$(ToolCommandName)&quot; EntryPoint=&quot;$(ToolEntryPoint)&quot; Runner=&quot;$(ToolCommandRunner)&quot; /&gt;"
            Query="/DotNetCliTool/Commands"
            Namespaces="$(Namespace)"/>
  </Target>
</Project>

This post is already plenty long enough, so I'm not going to go into any more detail about this approach, but it should at least give you an alternative for creating packages that can use the newest features of the .NET 10 SDK while remaining compatible with earlier .NET SDKs

Summary

In this post I discussed what the new platform-specific (self-contained or Native AOT compiled) NuGet package support means for package authors, and how to leverage them while still supporting users on pre-.NET 10 SDK versions. I started by describing the problem: users on older .NET SDKs can't use your tool at all by default if you use the new .NET 10 platform-specific tools feature.

For the remainder of the post I described a potential compromise in which .NET 10 SDK users can use platform-specific tools, while pre-.NET SDK users continue to use a framework-dependent platform-agnostic version of the tool. This has some trade offs, in that the "root" package needs to be large, but this may still be worth it in some cases. I described two different variants of the package, each with different requirements and trade-offs.

  • Buy Me A Coffee
  • Donate with PayPal
Andrew Lock | .Net Escapades
Want an email when
there's new posts?