Key Takeaways
- The dotnet cli makes automating and scripting builds on .NET projects simple, especially compared to the state of the art in .NET a decade or so ago.
- The dotnet cli extensibility model makes it readily possible to incorporate external .NET authored command line applications distributed via Nuget into your automated builds.
- The dotnet cli allows for running tests in your build scripts for your solutions.
- The testing output for the dotnet cli helps with better use of continuous integration (CI).
- Using Container technologies like Docker is much easier using the dotnet cli.
With the release of .NET Core 2.0, Microsoft has the next major version of the general purpose, modular, cross-platform and open source platform that was initially released in 2016. .NET Core has been created to have many of the APIs that are available in the current release of .NET Framework. It was initially created to allow for the next generation of ASP.NET solutions but now drives and is the basis for many other scenarios including IoT, cloud and next generation mobile solutions. In this second series covering .NET Core, we will explore some more the benefits of .NET Core and how it can benefit not only traditional .NET developers but all technologists that need to bring robust, performant and economical solutions to market.
This InfoQ article is part of the series ".NET Core". You can subscribe to receive notifications via RSS.
Recently I’ve been asked what the advantages are in moving to .NET Core from folks who have either been hesitant or unable to switch off the older, full .NET framework. As a reply, I’ll mention the better performance, the improved csproj file format, the improved testability of ASP.NET Core, and that is cross platform.
As the author of several OSS tools (Marten, StructureMap, and Alba is referenced in this project as examples), the biggest advantage to me personally has possibly been the advent of the dotnet cli. Used in conjunction with the new .NET SDK csproj file format, the dotnet cli tooling has made it far easier for me personally to create and maintain build scripts for my projects. It’s easier to run tests in build scripts, easier to both consume and publish Nuget packages, and the cli extensibility mechanism is fantastic for incorporating custom executables distributed through Nuget packages into automated builds.
To get started with the dotnet cli, first install the .NET SDK on your development machine. Once that’s done, there’s a couple of helpful things to remember:
- The “dotnet” tools are globally installed in your PATH and available anywhere in your command line prompts
- The dotnet cli uses the Linux style of command syntax using “--word [value]” for optional flags in longhand or “-w [value]” as a shorthand. If you’re used to the Git or Node.js command line tools, you’ll feel right at home with the dotnet cli
- “dotnet --help” will list the installed commands and some basic syntax usage
- “dotnet --info” will tell you what version of the dotnet cli you are using. It’s probably a good idea to call this command in your continuous integration build for later troubleshooting when something works locally and fails on the build server or vice versa
- Even though I’m talking about .NET Core in this article, do note that you can use the new SDK project format and the dotnet cli with previous versions of the full .NET Framework
Hello World from the Command Line
To take a little bit of a guided tour through some of the highlights of the dotnet cli, let’s say we want to build a simple “Hello, World” ASP.NET Core application. Just for fun though, let’s add a couple twists:
- There’ll be an automated test for our web service in a separate project
- We’ll deploy our service via a Docker container because that’s what all the cool kids do (and it shows off more of the dotnet cli)
- And of course, we’ll try to utilize the dotnet cli as much as possible
If you want to see the final product of this code, check out this GitHub repository.
First off, let’s start with an empty directory named “DotNetCliArticle” and open your favorite command line tool to that directory. We’re going to start by using the “dotnet new” command to generate a solution file and new projects. The .NET SDK comes with several common templates to create common project types or files, with other templates available as add-ons (more on this in a later section). To see what templates are available on your machine, start by using this command dotnet new –help, which should give you some output like this:
As you’ll notice above, one of the available templates is “sln” for an empty solution file. We’ll use that template to get started by typing the command dotnet new sln
which will generate this output:
The template "Solution File" was created successfully.
By default, this command will name the solution file after the containing directory. Because I called my root directory “DotNetCliArticle,” the generated solution file is “DotNetCliArticle.sln.”
Going farther, let’s add the actual project for our “Hello, World” with this command:
dotnet new webapi --output HeyWorld
The command above executes the “webapi” template to the directory “HeyWorld” that we specified through the optional “output” flag. This template will generate a slimmed down MVC Core project structure suitable for headless APIs. Again, the default behavior is to name the project file after the containing directory, so we get a file named “HeyWorld.csproj” in that directory, along with all the basic files for a minimal ASP.NET MVC Core API project. The template also sets up all the necessary Nuget references to ASP.NET Core that we need in our new project to get started.
Since I just happened to be building this in a small Git repository, after adding any new files with git add
., I use git status
to see what has been newly created:
new file: HeyWorld/Controllers/ValuesController.cs
new file: HeyWorld/HeyWorld.csproj
new file: HeyWorld/Program.cs
new file: HeyWorld/Startup.cs
new file: HeyWorld/appsettings.Development.json
new file: HeyWorld/appsettings.json
Now, to add the new project to our empty solution file, you can use the “dotnet sln” command like this:
dotnet sln DotNetCliArticle.sln add HeyWorld/HeyWorld.csproj
Alright, now we’ve got the shell of a new ASP.NET Core API service without ever having to open Visual Studio.NET (or JetBrains Rider in my case). To go a little farther and start our testing project before we write any actual code, I issue these commands:
dotnet new xunit --output HeyWorld.Tests
dotnet sln DotNetCliArticle.sln add HeyWorld.Tests/HeyWorld.Tests.csproj
The commands above create a new project using xUnit.NET as the test library and adds that new project to our solution file. The test project needs a project reference to the “HeyWorld” project, and fortunately we can add a project reference with the nifty “dotnet add” tool like so:
dotnet add HeyWorld.Tests/HeyWorld.Tests.csproj reference HeyWorld/HeyWorld.csproj
Before opening up the solution, I know upfront there’s a couple other Nuget references that I’d like to use in the testing project. Shouldly is my assertion tool of choice, so I’ll add a reference to the latest version of Shouldly by issuing another call to the command line:
dotnet add HeyWorld.Tests/HeyWorld.Tests.csproj package Shouldly
Which will give me some command line output like this:
info : Adding PackageReference for package 'Shouldly' into project 'HeyWorld.Tests/HeyWorld.Tests.csproj'.
log : Restoring packages for /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/HeyWorld.Tests.csproj...
info : GET https://api.nuget.org/v3-flatcontainer/shouldly/index.json
info : OK https://api.nuget.org/v3-flatcontainer/shouldly/index.json 109ms
info : Package 'Shouldly' is compatible with all the specified frameworks in project 'HeyWorld.Tests/HeyWorld.Tests.csproj'.
info : PackageReference for package 'Shouldly' version '3.0.0' added to file '/Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/HeyWorld.Tests.csproj'.
Next, I want to add at least one more Nuget reference to the testing project called Alba.AspNetCore2 that I’ll use to author HTTP contract tests against the new web application:
dotnet add HeyWorld.Tests/HeyWorld.Tests.csproj package Alba.AspNetCore2
Now, just to check things out a little bit before working with the code, I’ll make sure everything compiles just fine by issuing this command to build all the projects in our solution at the command line:
dotnet build DotNetCliArticle.sln
And ugh, that didn’t compile because of a diamond dependency version conflict between Alba.AspNetCore2 and the versions of the ASP.NET Core Nuget references in the HeyWorld project. No worries though, because that’s easily addressed by fixing the version dependency of the Microsoft.AspNetCore.All
Nuget in the testing project like this:
dotnet add HeyWorld.Tests/HeyWorld.Tests.csproj package Microsoft.AspNetCore.All --version 2.1.2
In the example above, using the “--version” flag with the value “2.1.2” will fix the reference to exactly that version instead of just using the latest version found from your Nuget feeds.
To double check that our Nuget dependency problems have all gone away, we can use the commands shown below to do a quicker check than recompiling everything:
dotnet clean && dotnet restore DotNetCliArticle.sln
As an experienced .NET developer, I’m paranoid about lingering files in the temporary /obj and /bin folders. Because of that, I use the “Clean Solution” command in Visual Studio.NET any time I try to change references just in case something is left behind. The “dotnet clean” command does the exact same thing from a command line.
Likewise, the “dotnet restore” command will try to resolve all known Nuget dependencies of the solution file I specified. In this case, using “dotnet restore” will let us spot any potential conflicts or missing Nuget references quickly without having to do a complete compilation – and that’s the main way I use this command in my own work. In the latest versions of the dotnet cli, Nuget resolution is done for you automatically (that behavior can be overridden with a flag though) in calls to "dotnet build/test/pack/etc” that would require Nugets first.
Our call to “dotnet restore DotNetCliArticle.sln” ran cleanly with no errors, so we’re finally ready to write some code. Let’s open up the C# editor of your choice and add a code file to the HeyWorld.Tests project that contains a very simple HTTP contract test that will specify the behavior we want from the “GET: /” route in our new HeyWorld application:
using System.Threading.Tasks;
using Alba;
using Xunit;
namespace HeyWorld.Tests
{
public class verify_the_endpoint
{
[Fact]
public async Task check_it_out()
{
using (var system = SystemUnderTest.ForStartup<Startup>())
{
await system.Scenario(s =>
{
s.Get.Url("/");
s.ContentShouldBe("Hey, world.");
s.ContentTypeShouldBe("text/plain; charset=utf-8");
});
}
}
}
}
The resulting file should be saved in the HeyWorld.Tests
directory with an appropriate name such as verify_the_endpoints.cs.
Without getting too much into the Alba mechanics, this just specifies that the home route of our new HeyWorld application should write out text saying “Hey, world.” We haven’t actually coded anything real in our HeyWorld application, but let’s still run this test to see if it’s wired up correctly and fails for the “right reason.”
Back in the command line, I can run all of the tests in the testing project with this command:
dotnet test HeyWorld.Tests/HeyWorld.Tests.csproj
Which with our one test that will fail because nothing has actually been implemented yet, gives us this output:
Build started, please wait...
Build completed.
Test run for /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/bin/Debug/netcoreapp2.1/HeyWorld.Tests.dll(.NETCoreApp,Version=v2.1)
Microsoft (R) Test Execution Command Line Tool Version 15.7.0
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
[xUnit.net 00:00:01.8266290] HeyWorld.Tests.verify_the_endpoint.check_it_out [FAIL]
Failed HeyWorld.Tests.verify_the_endpoint.check_it_out
Error Message:
Alba.ScenarioAssertionException : Expected status code 200, but was 404
Expected the content to be 'Hey, world.'
Expected a single header value of 'content-type'='text/plain', but no values were found on the response
Stack Trace:
at Alba.Scenario.RunAssertions()
at Alba.SystemUnderTestExtensions.Scenario(ISystemUnderTest system, Action`1 configure)
at Alba.SystemUnderTestExtensions.Scenario(ISystemUnderTest system, Action`1 configure)
at HeyWorld.Tests.verify_the_endpoint.check_it_out() in /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/verify_the_endpoint.cs:line 14
--- End of stack trace from previous location where exception was thrown ---
Total tests: 1. Passed: 0. Failed: 1. Skipped: 0.
Test Run Failed.
Test execution time: 2.5446 Seconds
To sum up that output, one test was executed and it failed. We also see the standard xUnit output that gives us some information about why the test failed. It’s important to note here that the “dotnet test” command will return an exit code of zero denoting success if all the tests pass and a non-zero exit code denoting failure if any of the tests fail. This is important for continuous integration (CI) scripting where most CI tools use the exit code of any commands to know when the build has failed.
I’m going to argue that the test above failed for the “right reason,” meaning that the test harness seems to be able to bootstrap the real application and I expected a 404 response because nothing has been coded yet. Moving on, let’s implement an MVC Core endpoint for the expected behavior:
public class HomeController : Controller
{
[HttpGet("/")]
public string SayHey()
{
return "Hey, world!";
}
}
(Note that the previous code should be appended as an additional class in your HeyWorld\startup.cs file.)
Returning to the command line again, let’s run the previous “dotnet test HeyWorld.Tests/HeyWorld.Tests.csproj
” command again and hopefully see results like this:
Build started, please wait...
Build completed.
Test run for /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld.Tests/bin/Debug/netcoreapp2.1/HeyWorld.Tests.dll(.NETCoreApp,Version=v2.1)
Microsoft (R) Test Execution Command Line Tool Version 15.7.0
Copyright (c) Microsoft Corporation. All rights reserved.
Starting test execution, please wait...
Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 2.4565 Seconds
Alright then, now that the test passes let’s run the actual application. Since the “dotnet new webapi” template uses the in-process Kestrel web server to handle HTTP requests, literally the only thing we need to do to run our new HeyWorld application is to launch it from the command line with this command:
dotnet run --project HeyWorld/HeyWorld.csproj
Running the command above should give you some output like this:
Using launch settings from HeyWorld/Properties/launchSettings.json...
: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]
User profile is available. Using '/Users/jeremydmiller/.aspnet/DataProtection-Keys' as key repository; keys will not be encrypted at rest.
Hosting environment: Development
Content root path: /Users/jeremydmiller/code/DotNetCliArticle/HeyWorld
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
To test out our new application now that it’s running, just navigate in your browser like so:
Dealing with HTTPS set up is outside the scope of this article
Do note again that I’m assuming all commands are being executed with the current directory set to the solution root folder. If your current directory is a project directory and there is only one *.csproj file in that directory, you can just type “dotnet run.”
Now that we have a tested web api application, let’s take the next step and put HeyWorld into a Docker image. Using the standard template for dockerizing a .NET Core application, we’ll add a Dockerfile to our HeyWorld project with this content:
FROM microsoft/dotnet:sdk AS build-env
WORKDIR /app
# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore
# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out
# Build runtime image
FROM microsoft/dotnet:aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "HeyWorld.dll"]
(Note that the previous text should be saved to a text file called Dockerfile in the project directory—in this case HeyWorld\Dockerfile.)
As this article is just about the dotnet cli, I just want to focus on the two usages of that within the Dockerfile:
- “dotnet restore” -- as we learned above, this command will resolve any Nuget dependencies of the application
- “dotnet publish –c Release –o out” -- the “dotnet publish” command will build the designated project and copy all the files that make up the application to a given location. In our case, “dotnet publish” will copy the compiled assembly for HeyWorld itself, all the assemblies referenced from Nuget dependencies, configuration files, and any files that are referenced in the csproj file
Please note in the usage above that we had to explicitly tell “dotnet publish” to compile with the “Release” configuration through the usage of the “-c Release” flag. Any dotnet cli command that compiles code (“build”, “publish”, “pack” for example) will be default build assemblies with the “Debug” configuration. Watch this behavior and remember to specify the “-c Release” or “--configuration Release” if you are publishing a Nuget or an application that is meant for production usage. You’ve been warned.
Just to complete the circle, we can now build and deploy our little HeyWorld application through Docker with these commands:
docker build -t heyworld .
docker run -d -p 8080:80 --name myapp heyworld
The first command builds and locally publishes a Docker image for our application named “heyworld.” The second command actually runs our application as a Docker container named “myapp.” You can verify this by opening your browser to “http://localhost:8080.”
Summary
The dotnet cli makes automating and scripting builds on .NET projects simple, especially compared to the state of the art in .NET a decade or so ago. In many cases you may even eschew any kind of task-based build script tool (Cake, Fake, Rake, Psake, etc.) in favor of simple shell scripts that just delegate to the dotnet cli. Moreover, the dotnet cli extensibility model makes it readily possible to incorporate external .NET authored command line applications distributed via Nuget into your automated builds.
About the Author
When Jeremy Miller was growing up in a farm community in Missouri, there was a “special” breed of folks around called Shade Tree Mechanics. Usually they were not the most reputable people in the world, but they had a knack for fixing mechanical problems and a reckless fearlessness to tinker with anything. A shade tree mechanic can be spotted by finding the pair of legs sticking out from under a beater car on blocks, surrounded by other skeletal vehicles crowding the rest of his scrubby, junk-laden yard. The beaters you see abandoned all around him aren’t useless, they’re fodder. He takes bits and pieces and tweaks and tunes, and comes up with a creative solution to your needs. Reputation notwithstanding, a shade tree mechanic knows how to get things running. While Miller doesn’t have any particular mechanical ability (despite a degree in mechanical engineering), he likes to think that he is the developer equivalent of a shade tree mechanic. His hard drive is certainly littered with the detritus of abandoned open source projects.
With the release of .NET Core 2.0, Microsoft has the next major version of the general purpose, modular, cross-platform and open source platform that was initially released in 2016. .NET Core has been created to have many of the APIs that are available in the current release of .NET Framework. It was initially created to allow for the next generation of ASP.NET solutions but now drives and is the basis for many other scenarios including IoT, cloud and next generation mobile solutions. In this second series covering .NET Core, we will explore some more the benefits of .NET Core and how it can benefit not only traditional .NET developers but all technologists that need to bring robust, performant and economical solutions to market.
This InfoQ article is part of the series ".NET Core". You can subscribe to receive notifications via RSS.