ASP.NET Core Blazor application as .NET Tool

In this post I quickly re-iterate the steps I used to create a dotnet tool. Creating a standard console dotnet tool is easy. As the documentation points out, add the following parameters needed to be added to the csproj:

<PackAsTool>true</PackAsTool>
<ToolCommandName>MyTool</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>

Publish the application as nuget package and upload the created package to NuGet for distribution.

For local testing (without the need for deploying to nuget) one can simply install his/her tool locally by providing a path to the package source:

dotnet tool install MyTool --add-source .\bin\Release\net6.0\publish --global

The command above installs the tool from the nuget package it finds under the relative path of .\bin\Release\net6.0\publish. Although the CLI installs the tool form a nuget package it does not depend on the nuget cache of the machine. This means a tool can be installed and uninstalled repeatedly with the same version number but different binaries. This comes handy if you want to iterate a testing of a few releases during development, without providing a new version number everytime.

You can create local (non-global) tool installations too, but I did not find much benefit in the process of creating a simple tool. However, I could imagine developers using local installations for testing purpose.

The Blazor App

Te key difference when packaging a Blazor Server app is that the above approach does not work out of the box. In this case we need to apply the steps done by the tooling manually. Fortunately, this is all manageable in a few key steps.

Why packaging a Blazor Server app did not work? There are two reasons:

  • packaging creates the content of the wwwroot folder under a different folder in the nuget package (this could be overcome by providing ContentRootPath)

  • scss files did not compile into css file required by _Layout.cshtml

At this point a simpler approach is to publish the Blazor application as a regular ASP.NET Core Blazor app, and package it into a dotnet tool.

The key steps to achieve this are detailed below.

ContentRootPath

Set content root path during application bootstrap in Program.cs:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions() { Args = args, ContentRootPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) });

This will use the executing assembly's path as the content root. A dotnet global tool it gets installed into folder: C:\Users\[user]\.dotnet\tools. Here a MyTool.exe file is placed, while the rest of the application files reside under .store\MyTool folder. Finding this path for the as the content root is less straightforward and implies a substantial amount of implementation details. Hence, I find easier to use the path of the executing assembly as relative pointer.

Nuspec file

Create a standard nuspec file with two additional customizations:

    <!--... -->
    <packageTypes>
      <packageType name="DotnetTool" />
    </packageTypes>
    <frameworkReferences>
      <group targetFramework="net6.0">
        <frameworkReference name="Microsoft.AspNetCore.App" />
      </group>
    </frameworkReferences>
  </metadata>
  <files>
    <file src="**\*" target="tools\net6.0\any" />
  </files>

Firstly, the package type node describes that this package is a dotnet tool. Secondly, in the files every published artifact is copied under the tools\net6.0.\any folder inside the nuget package. This way, from the Blazor application's point of view it looks and feels like a usual installation.

DotnetToolSettings

This file contains dotnet tool settings that cannot be described by the nuspec file. It is a standard XML file:

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

The command node describes the name of the command used to invoke the tool. This could be different from the nuget package name. It also details the entry point of the application and which runner to be used. I did not find much official documentation of about the schema of this file, so I had to 'reverse engineer' the content of it.

Unfortunately, having multiple commands is not supported. Trying to install such a package results an error: The settings file in the tool's NuGet package is invalid: More than one command is defined for the tool.

Build and Deploy

Build the tool with dotnet CLI:

  • dotnet clean MyTool.csproj --configuration Release --framework net6.0 to clean the publish folder

  • dotnet publish MyTool.csproj --configuration Release --no-self-contained --framework net6.0 publish the application as a non-self contained installation (we need to have the dotnet SDK installed to use dotnet tool command)

  • nuget pack ./bin/Release/net6.0/publish -OutputDirectory ./bin/Release/net6.0/publish the published project. Use the nuspec file created earlier.

To test the packaged tool:

  • dotnet tool uninstall MyTool --global uninstalls the previous dotnet tool installation

  • dotnet tool install MyTool --add-source .\bin\Release\net6.0\publish --global install the new binaries from the packaged path

  • MyTool to run the tool

Summary

This post has investigated the key steps of creating a dotnet tool from a Blazor Server application. It describes an approach for creating the tool with a custom nuspec file, DotnetToolSettings file and CLI commands. As a final thought, once the application is deployed and gets distributes, users of the tool shall install and trust ASP.NET Core development certificates (if not yet), or a custom certificate shall be used when the Blazor app is hosted with https.

To trust dev certificates, run the following command:

dotnet dev-certs https --trust