.NET Core 2.1 Preview 1 is available

Several preview releases related to .NET Core 2.1 have been recently announced, including ASP.NET Core 2.1 Preview 1, EF Core 2.1 Preview 1, and an updated .NET Core SDK as well, which has significant build performance improvements. All of them bring interesting new features, like cookie consent and GDPR support in web apps, HTTPS binding by default, lazy loading and GroupBy translation in Entity Framework Core - each deserves a separate post, and there is plenty of them written already. What I wanted to cover this time is the global tools support added in .NET SDK 2.1 Preview 1.

The concept of tools was already there since the beginning of .NET Core: command-line utilities that can be installed as NuGet packages per project. This is, for instance, how Entity Framework Core tooling works. To enable the dotnet ef experience, you add a special type of package reference to the project file:

<ItemGroup> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" /> </ItemGroup>

This allows to use various EF Core commands, like dotnet ef database update or dotnet ef migrations add . The dotnet.exe in this case is a driver for running specific commands, which makes some fun extensibility scenarios possible (read how I used this to create a silly “dotnet-rocks” tool).

Having the tools scoped to a project like this is convenient and looks very similar to how it is done with npm packages. However, npm also allows to install a package globally, so that it is available everywhere on the machine, with npm install -g [package-name] . This can be especially useful for development-time tools like linters, test runners, or development web servers.

So, inspired by this, .NET Core team is adding support for installing tools globally too. This is still in preview, so the official documentation doesn’t describe it yet, but there is a sample on GitHub, which demonstrates how this will work.

A use case for a global tool

To try this out for myself, I needed a simple and relatively helpful use case. When prototyping the frontend part of a web application and trying out some HTML/CSS/JavaScript code, I often want to quickly expose the current directory via a web server. Let’s say, I have a web API in one app, already running, and then I’m building a quick Vue/Preact/Hyperapp UI for it in another folder. If it was scaffolded from a typical SPA template for these frameworks, chances are there is already a Node.js web server included, so it can be started with a simple npm start . However, sometimes I prefer to start from scratch and configure everything myself. Also, the applications I am currently working on are never hosted using Node, since it is all .NET-centric and all production apps are served by IIS. Basically, I just don’t want to depend on Node and npm for quick content serving.

For several years I have been using caddy for that: putting it in the PATH and simply running caddy in a folder will serve the contents locally via HTTP. Can I create something similar in .NET Core? It is ridiculously easy to configure static file serving with Kestrel, and it is just fun to start an HTTP server with something idiomatic, like dotnet serve , available everywhere on the system.

And that is exactly what I have built as an exercise.

Creating a global tool

Let’s do it all step by step. First, we’ll need to install .NET Core SDK 2.1 Preview 1, which you can grab from this page. After installing, verify the version by running dotnet --version :

$ dotnet --version 2.1.300-preview1-008174

Now let’s create a folder for our little web server project and scaffold a new console .NET Core app by running the following:

mkdir dotnet-serve && cd dotnet-serve && dotnet new console

Since this is just an exercise, I’ll add the convenient ASP.NET Core meta-package, which contains all we need (note that it has been renamed from Microsoft.AspNetCore.All to Microsoft.AspNetCore.App in 2.1):

dotnet add package Microsoft.AspNetCore.App -v 2.1.0-preview1-final

And then run dotnet restore to download the package. Now, as I said, configuring a minimalistic Kestrel-based using ASP.NET Core is ridiculously easy, so this Program.cs file is all we need:

using System; using System.IO; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.FileProviders; namespace dotnet_serve { class Program { public static void Main(string[] args) { new WebHostBuilder() .UseKestrel() .UseUrls("http://localhost:5001") .UseStartup<Program>() .Build() .Run(); } public void Configure(IApplicationBuilder app) { app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory()) }); app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory()) }); } } }

I didn’t even use a Startup.cs class to keep it a one-file project (surely it can be improved and simplified even further).

And that’s it! This is our web server, ready to become a global tool. Adding support for a project to be packaged in this new way requires just one extra line in the project file:

<PackAsTool>true</PackAsTool>

And now we can package it:

dotnet pack -c release -o nupkg

This produces a dotnet-serve.1.0.0.nupkg file in nupkg subfolder. If we look inside, it differs slightly from a typical library NuGet package, having tools folder inside instead of lib :

Installing and using the global tool

Now we want to test how global tool installation works. It is a preview and not everything is polished yet, so this is what the documentation in the global tool code sample says:

For Preview 1, defining the source during installation doesn’t work correctly, so you need a nuget.config file to test your new tool without deploying it to a NuGet feed. You can do this by placing this nuget.config in your project directory that looks similar to the following example:

So, as suggested, we are going to create the following nuget.config file in the project folder:

<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <clear/> <add key="local-packages" value="./nupkg" /> </packageSources> </configuration>

This explicitly configures the only NuGet package source to be in the ./nupkg subfolder of the current directory. Once again, this is a workaround for local installation only and will likely be fixed in the final 2.1 release. If your global tool is already on nuget.org (or any other configured package source, for that matter), then it can also be installed, just like the Microsoft’s dotnetsay sample app).

Finally, we can install the tool globally:

dotnet install tool -g dotnet-serve

This will do two things:

extract the tool from the package into %userprofile%\.dotnet\tools folder if this is the first time any tool is being installed globally, add this folder to the current user’s PATH environment variable.

After doing this for the first time, you might need to open/close you terminal or run refreshenv to pick up the updated PATH . If everything went fine, you can now test the tool. Create a new folder somewhere else and put a simple index.html file, optionally with some JavaScript and CSS included to make sure that we can serve all content types properly. Then navigate to this folder in the terminal and type dotnet-serve or even dotnet serve , without the dash (because this is how convention-based searching works in dotnet.exe driver). This will start serving the content at http://localhost:5001 :

Conclusion

I think global tools can enable lots of interesting scenarios, especially in the team environment, when certain team-specific tasks can be encapsulated and shared this way. For instance, in my current team we are using a tiny test data generation utility (uses SqlBulkCopy to efficiently insert millions of test records into a database for application performance testing) and this way of packaging/sharing it seems a perfect fit.

The source code used in this article can be found on GitHub at https://github.com/atsvetkov/dotnet-serve.

I didn’t upload the tool to nuget.org for one simple reason: there is already a package with exactly the same idea and name, created by a member of ASP.NET Core team, so it surely is a better implementation than my trivial example.

Thank you for reading, and have fun with global tools!