When developing with .NET Core, you have two different workflows to choose from: manage projects from Visual Studio or work from a command-line using dotnet
commands. New projects can be created this way, and after the recent updates the project templates system became extensible, allowing to install additional templates or even create your own.
“dotnet new” and friends
For example, this is how the output of dotnet new
looks like by default with the latest RC installed (.NET Core 1.0 SDK RC4, to be specific):
> dotnet new
Template Instantiation Commands for .NET Core CLI.
Usage: dotnet new [arguments] [options]
Arguments:
template The template to instantiate.
Options:
-l|--list List templates containing the specified name.
-lang|--language Specifies the language of the template to create
-n|--name The name for the output being created. If no name is specified, the name of the current directory is used.
-o|--output Location to place the generated output.
-h|--help Displays help for this command.
-all|--show-all Shows all templates
Templates Short Name Language Tags
--------------------------------------------------------------------------------------
Console Application console [C#], F# Common/Console
Class library classlib [C#], F# Common/Library
Unit Test Project mstest [C#], F# Test/MSTest
xUnit Test Project xunit [C#], F# Test/xUnit
Empty ASP.NET Core Web Application web [C#] Web/Empty
MVC ASP.NET Core Web Application mvc [C#], F# Web/MVC
Web API ASP.NET Core Web Application webapi [C#] Web/WebAPI
Solution File sln Solution
Examples:
dotnet new mvc --auth None --framework netcoreapp1.0
dotnet new classlib --framework netstandard1.4
dotnet new --help
We can additionally install JavaScriptServices templates like this:
dotnet new --install Microsoft.AspNetCore.SpaTemplates::*
As mentioned in the recent ASP.NET Community Standup, the asterisk at the end means “any version” and is required, because --install
argument needs a version to be specified.
Now, when we execute dotnet new
once again, the list will have more items and we can use these nicely preconfigured .NET Core + React/Angular/etc. projects:
Templates Short Name Language Tags
---------------------------------------------------------------------------------------
Console Application console [C#], F# Common/Console
Class library classlib [C#], F# Common/Library
Unit Test Project mstest [C#], F# Test/MSTest
xUnit Test Project xunit [C#], F# Test/xUnit
Empty ASP.NET Core Web Application web [C#] Web/Empty
MVC ASP.NET Core Web Application mvc [C#], F# Web/MVC
MVC ASP.NET Core with Angular angular [C#] Web/MVC/SPA
MVC ASP.NET Core with Aurelia aurelia [C#] Web/MVC/SPA
MVC ASP.NET Core with Knockout.js knockout [C#] Web/MVC/SPA
MVC ASP.NET Core with React.js react [C#] Web/MVC/SPA
MVC ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA
Web API ASP.NET Core Web Application webapi [C#] Web/WebAPI
Solution File sln Solution
These new SPA templates are built by Steve Sanderson (the author of knockout.js) together with the ASP.NET team and deserve a separate post, so I’m not going to discuss them now. What I want to talk about instead is the extensibility of dotnet
command itself.
How “dotnet” command works
Obviously, there is not too much magic: the .NET Core installer copies dotnet.exe
to C:\Program Files\dotnet and adds this directory to PATH. When in doubt, remember about where
command, which can help identify the location of an executable:
> where dotnet
C:\Program Files\dotnet\dotnet.exe
In most cases dotnet
command will be executed with an additonal argument, which is the actual action we want to perform, like new
, build
, or run
. The list of supported ones can be displayed by running dotnet help
:
Commands:
new Initialize .NET projects.
restore Restore dependencies specified in the .NET project.
build Builds a .NET project.
publish Publishes a .NET project for deployment (including the runtime).
run Compiles and immediately executes a .NET project.
test Runs unit tests using the test runner specified in the project.
pack Creates a NuGet package.
migrate Migrates a project.json based project to a msbuild based project.
clean Clean build output(s).
sln Modify solution (SLN) files.
All of them are the built-in commands, which can be seen in the source code on Github:
public class Program
{
private static Dictionary<string, Func<string[], int>> s_builtIns = new Dictionary<string, Func<string[], int>>
{
["add"] = AddCommand.Run,
["build"] = BuildCommand.Run,
["clean"] = CleanCommand.Run,
["help"] = HelpCommand.Run,
["list"] = ListCommand.Run,
["migrate"] = MigrateCommand.Run,
["msbuild"] = MSBuildCommand.Run,
["new"] = NewCommandShim.Run,
["nuget"] = NuGetCommand.Run,
["pack"] = PackCommand.Run,
["publish"] = PublishCommand.Run,
["remove"] = RemoveCommand.Run,
["restore"] = RestoreCommand.Run,
["restore-projectjson"] = RestoreProjectJsonCommand.Run,
["run"] = RunCommand.Run,
["sln"] = SlnCommand.Run,
["test"] = TestCommand.Run,
["vstest"] = VSTestCommand.Run,
};
...
However, if you tried using Entity Framework Core CLI, you know that it is managed using the dotnet ef
command installed with a separate NuGet package. And there is definitely no “ef” in the list of built-in commands above. How does this actually work? What happens when we execute dotnet COMMANDNAME
?
The answer is again in the source code, same Program.cs file:
int exitCode;
Func<string[], int> builtIn;
if (s_builtIns.TryGetValue(command, out builtIn))
{
exitCode = builtIn(appArgs.ToArray());
}
else
{
CommandResult result = Command.Create(
"dotnet-" + command,
appArgs,
FrameworkConstants.CommonFrameworks.NetStandardApp15)
.Execute();
exitCode = result.ExitCode;
}
return exitCode;
So dotnet.exe will look for COMMANDNAME
in the predefined list, and if it isn’t found, will try to run an executable with the filename dotnet-COMMANDNAME
, passing down the rest of the original arguments to it. This only works if a file dotnet-COMMANDNAME.exe can be found in at least one of the places from the PATH variable.
In fact, this is almost true: it actually doesn’t have to be an *.exe file, as you can see in the snippet above. Anything that is on PATH, can be executed and has the name dotnet-COMMANDNAME
will do. Which opens some creative possibilities…
Batch file
Let’s start from the simplest example. I created a batch file dotnet-hi.bat in C:\tools\go\bin (which happens to be in my computer’s PATH) with the following content:
@echo off
echo Hi there!
And now I can do dotnet hi
from anywhere:
> dotnet hi
Hi there!
Ok, it works, but not soo exciting. Let’s add some interaction and create the following dotnet-flickr.bat
file:
@echo off
set "url=https://www.flickr.com/photos/tags/%1"
start %url%
Now, when you are feeling sad, just run dotnet flickr cats
and be happy!
We can also do something at least remotely useful, for example, create a dotnet-code.bat
, which will start Visual Studio Code in the current directory:
@echo off
code .
dotnet rocks!
You can only do much with scripting, so there will be a point when a full-blown executable makes more sense. Plus, so far the commands have been still pretty simple and boring. And since I am a big fan of .NET Rocks! show, I decided to pay a tribute to Carl and Richard: the greatest podcast about .NET absolutely deserves its own dotnet
command.
So let me introduce you to dotnet rocks
!
> dotnet rocks
Pick one of 10 last episodes on .NET Rocks! to play:
-16 Feb 2017: Fusion Power Update Geek Out
-15 Feb 2017: Virtual, Augmented and Mixed Realities with Jessica Engstrom
-14 Feb 2017: Machine Learning Panel at NDC London
-09 Feb 2017: Ops and Operability with Dan North
-08 Feb 2017: Xamarin MVVM apps with Gill Cleeren
-07 Feb 2017: Chatbots with Galiya Warrier
-02 Feb 2017: IdentityServer4 with Brock Allen and Dominick Baier
-01 Feb 2017: Data and Docker with Stephanie Locke
-31 Jan 2017: Nodatime, Google Cloud and More with Jon Skeet
-26 Jan 2017: Punishment Driven Development with Louise Elliott
(Disclaimer: only freely available data is used here and I am not affiliated with .NET Rocks!, although I can’t recommend it enough! When asked for permission to use “dotnet rocks” as the tool name, Carl and Richard kindly allowed me to “go for it".)
The tool itself is very simple: it is built as a .NET Core console app, which will display a menu of N (10 by default) last episodes from .NET Rocks! and allow to pick one. Once selected, the URL of episode’s MP3 file will be opened by the default OS program (most likely - will open in your default browser and start playing). The source code is on https://github.com/atsvetkov/dotnet-rocks and the packaged version is available in a NuGet format at https://www.nuget.org/packages/dotnet-rocks. This last one is actually important: having an executable somewhere on your PATH is not exactly the most robust way of installing .NET CLI extensions, so there is a better option: adding it as a .NET CLI tool reference to a project file. So, since it is already on nuget.org, you can just create a new .NET Core app and add a tool reference to “dotnet-rocks” in a .csproj file:
<ItemGroup>
<DotNetCliToolReference Include="dotnet-rocks" Version="0.0.1" />
</ItemGroup>
Then, after performing dotnet restore
, you should be able to run dotnet rocks
in this project’s folder. This time there is no executable with the name dotnet-rocks anywhere on the PATH, but there is a package folder with dotnet-rocks.dll in the system-wide NuGet packages directory, which will also be probed by .NET CLI.
This way of installing custom extensions seems much cleaner and more under control than scripts with magic names residing in unpredictable corners of the filesystem. And now your dream of checking out the latest .NET Rocks episodes from the command-line has finally come true! OK, maybe it wasn’t anyone’s dream, but I just felt it had to be done.
Summary
Hopefully this post demonstrates what kind of funny and not completely useless things can be done using the .NET CLI extension mechanism. I see this as yet another way of building the convenience tools for making your dev team’s life easier. The probing logic in dotnet.exe is flexible enough to allow hooking up tools or scripts from different places, so go ahead and come up with your own dotnet something-cool
extension!
No comment