DotNET Core Cake Task Runner (CSharp Makefiles for coverlet code coverage and more)

Cake is a free open-source task runner for writing custom cross-platform builds stages for .NET applications.

cake image

MailSlurp uses Cake to build CSharp SDKs for its free email API. In this tutorial we will show you how to create Cake files and a dotnet tool manifest in order to create custom builds and tasks in your .NET project.

What is Cake?

Cake is the Make of C#. It’s a cross-platform build automation system with a C# DSL that makes it easy to compile code, copy files, run tests, build packages, collect code coverage and more on Linux, Mac, and Windows - using the same tools for each.

Cake is not intended to replace dotnet test commands or your CI pipelines in Circle or TeamCity. Cake is designed for developers so they can run common tasks on any machine within an organization and get the same results. If you have ever developed a NodeJS application it is a bit like package.json scripts.

An example of a task runner in another environment, NodeJS:

{
    "scripts": { 
        "test": "jest"
    }
}

Why Cake? (Why not Make?)

Cake might seem unusual if you are used to Visual Studio or Resharper for .NET development but Cake adds the possibility of a central file for common tasks that can result in deterministic builds across different machine types. In plain english, Cake let’s you write common tasks such as running tests, collecting code coverage, zipping files etc. in a automated way when you may have performed them manually using an IDE in the past. It also helps to add a common build system to cross-platform apps that may run on Windows, Linux and Mac.

Why not Make? Make is the original inspiration for Cake and is used extensively in the Linux world. Make however isn’t bundled with Windows and uses a syntax that can be unfamiliar for many developers. Using Cake let’s you write tasks in C# and include regular Nuget packages like you would in a DotNET project.

Writing a Cakefile

You can install Cake as a dotnet tool locally using a tool manifest. This means versions and dependencies for your tools are stored in code.

Create a tool manifest

First create a tool manifest in your DotNET project:

dotnet new tool-manifest

This will create a new .config directory and inside that will be a dotnet-tools.json file.

{
  "version": 1,
  "isRoot": true,
  "tools": {
  }
}

Add Cake to your manifest

Next we want to install Cake as a tool:

dotnet tool install Cake.Tool --version 1.1.0

For demonstration purposes let’s also add dotnet-format to let use format code.

dotnet tool install dotnet-format`

Restore dependencies

Now that we have added tools our .config/dotnet-tools.json file should look like this:

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "cake.tool": {
      "version": "1.1.0",
      "commands": [
        "dotnet-cake"
      ]
    },
    "dotnet-format": {
      "version": "5.1.225507",
      "commands": [
        "dotnet-format"
      ]
    }
  }
}

To install tools on a new machine run:

dotnet tool restore

This will read the tool manifest and install any missing tools on your machine. It is good practice in an organization to add these setup steps to a README.md file like so:

# My Project

## Install
`dotnet tool restore`

Simple example

Create a new file called build.cake in the root directory of your project. The main concepts in Cake are targets and tasks. One defined tasks in a build file and targets them in the command line. For instance here is a simple Cake file that builds a solution:

var target = Argument("target", "Report");
var configuration = Argument("configuration", "Release");

Task("Build")
    .Does(() =>
{
    DotNetCoreBuild("./MySolution.sln", new DotNetCoreBuildSettings
    {
        Configuration = configuration,
    });
});

The build task can be invoked in a command line by running:

dotnet cake

Or you can specify the target explicitly using:

dotnet cake --target=Build

Creating custom Cake tasks

We can use Cake to turn tasks that might typically involve clicking through submenus in an IDE into jobs that can be run on any platform using a command line. This includes continous integration pipelines like Jenkins and Azure. For an example let’s create a Cake file that can collect code coverage using coverlet and generate a report to display the percentage of lines covered by our tests in the terminal.

Install coverlet and report generator

In your test solution add the coverlet package - this will orchestrate tests to collect code coverage. Code coverage defines how many lines of code your tests test for. Code coverage is a useful way to measure how well your application is tested.

dotnet add package coverlet.collector

Add tasks to your Cake file

Now we can define a Cake file to run our build, tests, code coverage and output the results in the command line with one simple command:

dotnet cake

The Cake file looks like this (see each comment):

// add package directives
#tool nuget:?package=ReportGenerator&version=4.8.8
#addin nuget:?package=Cake.FileHelpers&version=4.0.1
#r "Spectre.Console"
// import spectre for colored console output
using Spectre.Console

// define a default target
var target = Argument("target", "Report");
var configuration = Argument("configuration", "Release");

// define a build task
Task("Build")
    .Does(() =>
{
    DotNetCoreBuild("./project.sln", new DotNetCoreBuildSettings
    {
        Configuration = configuration,
    });
});

// define a test task that includes coverlet arguments
Task("Test")
    .IsDependentOn("Build")
    .Does(() =>
{
    var testSettings = new DotNetCoreTestSettings
    {
        Configuration = configuration,
        NoBuild = true,
        ArgumentCustomization = args => args
            .Append("--collect").AppendQuoted("XPlat Code Coverage")
            .Append("--logger").Append("trx")
    };
    var files = GetFiles("./*.{sln,csproj}").Select(p => p.FullPath);
    foreach(var file in files) 
    {
        DotNetCoreTest(file, testSettings);
    }
});

// generate a text summary report from the cobertura coverage collected during test
Task("Report")
    .IsDependentOn("Test")
    .Does(() =>
{
    // generate coverage report
    var reportSettings = new ReportGeneratorSettings
    {
        ArgumentCustomization = args => args.Append("-reportTypes:TextSummary")
    };
    var coverageDirectory = Directory("./reports");
    var files = GetFiles("./**/TestResults/*/coverage.cobertura.xml");
    ReportGenerator(files, coverageDirectory, reportSettings);
    
    // print summaries to console
    var summaries = GetFiles($"{coverageDirectory}/Summary.txt");
    foreach(var file in summaries) 
    {
        var summary = FileReadText(file);
        AnsiConsole.Markup($"[teal]{summary}[/]");
    }
});

// add a format task
Task("Format")
    .Does(() =>
{
    var projects = GetFiles("./*.csproj}").Select(p => p.FullPath);
    foreach(var project in projects)
    {
        DotNetCoreTool($"dotnet-format {project}");
    }
});


RunTarget(target);

Task output

Running the above Cake file will print code coverage to the terminal. This is great for cross platform tooling or for CI builds. The output looks like so:

2021-06-08T19:55:17: Report generation took 1.4 seconds
Summary
  Generated on: 8/06/2021 - 7:55:17 p.m.
  Parser: MultiReportParser (21x CoberturaParser)
  Assemblies: 1
  Classes: 174
  Files: 160
  Line coverage: 88.8%
  Covered lines: 2064
  Uncovered lines: 259
  Coverable lines: 2323
  Total lines: 5850

Extending Cake and next steps

extend cake

Cake is a highly underated tool that lets DotNET teams create custom build tasks that run cross-platform and IDE-independent. There are many Cake extensions available for AWS, GitHub, NuGet, Azure and more. You can even create your own!

We use Cake at MailSlurp to build and deploy our Email APIs. You can use MailSlurp to unlimited free test email accounts. Send and receive emails from tests or application using intuitive CSharp SDKs. Check it out!