I’ve been using Grunt to build and deploy .NET apps for about a year now. Its a huge improvement over Rake and the crusty XML build tools but I’d been hearing a lot of good things about gulp so I thought it was time to check it out. I was not disappointed and would highly recommend using gulp over Grunt. Gulp’s code over configuration approach eliminates the friction I experienced with Grunt. It also reduces (Or eliminates entirely) the code required to wire up tasks. So lets see how we can use gulp to build .NET apps.

Project Layout

The project layout will be along the lines of this:

/MyApp
    /src
        MyApp.sln
        ...
    /...
    gulpfile.js
    package.json
    ...

At the root of your project will be a package.json that specifies your dependencies. Also a gulpfile.js which is your build script.

Initial Setup

  • Download and install Node.js.
  • Create a minimal package.json at the project root or use npm init to generate it for you:
{
    "name": "MyApp",
    "version": "0.0.0"
}
  • Create a bare bones gulpfile.js at the project root:
var gulp = require('gulp');

gulp.task('default', []);

gulp.task('ci', []);

Above we have a special default task alias that gets run when you type gulp with no arguments. You can decide what you want that to do; I have it setup to run Karma in watch mode. The second task alias, ci, will be what is run by the build server (And you can call this whatever you want so long as the build server knows about it, as we’ll see later).

  • Install gulp globally (-g): npm install gulp -g
  • Install gulp locally (No -g) and save the dependency to your package.json: npm install gulp --save
  • Run gulp to make sure all is working, should display something along these lines:
[14:48:30] Using gulpfile /.../gulpfile.js
[14:48:30] Starting 'default'...
[14:48:30] Finished 'default' after 32 μs

NOTE: Throughout this post I will be using the --save flag to save NPM dependencies to the package.json file. You may have also seen the --save-dev flag and might be confused as to when you should use one over the other. Check out this SO question for an explanation.

Task Sequence

Gulp will try to run every task in parallel. Obviously you will need to run certain tasks in a particular order in your build. The current version of gulp allows you to do this a few ways. First you will need to specify a dependency and then some way to indicate the dependency has completed. According to the gulp docs, you can indicate that a dependency has completed by either returning a stream, returning a promise or taking in a callback and calling it when done. The following demonstrates the stream and callback approaches:

// Return a stream so gulp can determine completion
gulp.task('clean', function() {
    return gulp
        .src('app/tmp/*.js', { read: false })
        .pipe(clean());
});

// OR

// Take in the gulp callback and call it when done
gulp.task('clean', function(callback) {
    gulp.src('app/tmp/*.js', { read: false })
        .pipe(clean());
    callback();
});

// Specify the dependencies in the second parameter
gulp.task('build', ['clean'], function() {
    // Build...
});

So if you run the build task in this example, the clean task will run and complete first, then the build task will run. I will favor returning the stream throughout this post unless the callback or promise method makes sense.

Seem awkward and/or confusing? You’re not alone. The upcoming gulp 4 release will revamp how this is handled. When that is released I will update this post to reflect those changes.

Passing in parameters

Undoubtedly you’ll want to pass parameters into your build scripts. For example passing in the version or a nuget api key. One way to do this is with environment variables:

gulp.task('default', function() {
    var version = process.env.BUILD_NUMBER;
    var nugetApiKey = process.env.NUGET_API_KEY;
    ...
});

Another way to do this is by passing them into gulp as parameters:

var args = require('yargs').argv;

gulp.task('default', function() {
    console.log(args.buildVersion);
    console.log(args.debug);
});
$ gulp --build-version 1.2.3.4 --debug
[14:48:30] Using gulpfile /.../gulpfile.js
[14:48:30] Starting 'default'...
[14:48:30] 1.2.3.4
[14:48:30] true
[14:48:30] Finished 'default' after 32 μs

Here we are using the yargs module (Which is a fork of optimist) to parse the gulp command line args (npm install yargs --save). You can pass in any arguments you like so long as they don’t conflict with gulp options (Which is why I use build-version instead of version which is already used by gulp). One nice feature of yargs is that arguments that do not have a value are considered flags and represented as booleans (As demonstrated above with --debug). Another nice feature is the automatic conversion of spinal-case-args to camelCase. I will use this approach throughout this post.

Assembly Info

First thing you will want to do is set the version number in the project assembly info files (And any other info you’d like). I personally let the build server manage the version and then grab it from an environment variable, but you can do whatever works best for you. To do this we’ll use the gulp-dotnet-assembly-info plugin (npm install gulp-dotnet-assembly-info --save). Use the plugin as follows:

var args = require('yargs').argv,
    assemblyInfo = require('gulp-dotnet-assembly-info');

gulp.task('assemblyInfo', function() {
    return gulp
        .src('**/AssemblyInfo.cs')
        .pipe(assemblyInfo({
            version: args.buildVersion,
            fileVersion: args.buildVersion,
            company: 'Planet Express',
            copyright: function(value) { 
                return value + '-' + new Date().getFullYear(); 
            },
            ...
        }))
        .pipe(gulp.dest('.'));
});

So we pipe in all AssemblyInfo.cs files, modify them and then save them back out. You can specify a value or a function that returns the value. See here for more info.

Setting Configuration Values

You may need to set values in the app.config or web.config. To do this we’ll use the xmlpoke module (npm install xmlpoke --save). Use the module as follows:

var xmlpoke = require('xmlpoke');

gulp.task('configuration', ['assemblyInfo'], function(cb) {
    xmlpoke('**/{web,app}.config', function(xml) {
        xml.withBasePath('configuration')
           .set("appSettings/add[@key='connString']/@value", 
                'Server=server;Database=database;Trusted_Connection=True;')
           .set('system.net/mailSettings/smtp/network/@host', 'smtp.mycompany.com');
    });
    cb();
});

This module sports a lot more features than shown here. See here for more info.

Building

Now for building. To do that we will use the gulp-msbuild plugin (npm install gulp-msbuild --save). Use the plugin as follows:

var msbuild = require('gulp-msbuild');

gulp.task('build', ['configuration'], function() {
    return gulp
        .src('**/*.sln')
        .pipe(msbuild({
            toolsVersion: 12.0,
            targets: ['Clean', 'Build'],
            errorOnFail: true,
            stdout: true
        }));
});

The plugin looks for msbuild in the PATH. You can also specify the version you want to target with the toolsVersion option. This plugin supports more options than shown here, see here for more info.

On a side note, you may run into the following error when building web applications on your build server:

The imported project "C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\
v11.0\WebApplications\Microsoft.WebApplication.targets" was not found.

A solution can be found here.

Running Tests

Next you will want to run your tests. If you are using NUnit, you’re in luck as there is a gulp plugin for that. We will use the gulp-nunit-runner plugin (npm install gulp-nunit-runner --save). Use the plugin as follows:

var nunit = require('gulp-nunit-runner');

gulp.task('test', ['build'], function () {
    return gulp
        .src(['**/bin/**/*Tests.dll'], { read: false })
        .pipe(nunit({
            teamcity: true
        }));
});

The plugin looks for NUnit in the PATH and by default runs the anycpu version of NUnit (The x32 version can be specified with the platform option). You can also explicitly pass the nunit runner path if you like. You’ll notice we’re passing read: false into the source; this indicates that only filenames, and not content, are to be included in the stream. Also, the teamcity option integrates the test results with TeamCity. The plugin supports many more options than shown here, see here for more info.

Controlling Windows Services

Sometimes you will need to deploy Windows services. In order to do this you will need to stop these services before the delploy and start them after. To do this we can use the windows-service-controller node module (npm install windows-service-controller --save). Use the module as follows:

var sc = require('windows-service-controller');

gulp.task('stop-services', ['nunit'], function() {
    return sc.stop('MyServer', 'MyService');
});

...

gulp.task('start-services', ['deploy'], function() {
    return sc.start('MyServer', 'MyService');
});

Here we are passing in a single service although you can pass in an array of service names if there are multiple. The module supports many more options than shown here, see here for more info.

Deploying Files

There are a couple of ways to deploy files. Out of the box, gulp’s innate ability to work with files will get you a long way:

gulp.task('deploy', ['nunit'], function() {
    return gulp
        .src('./src/MyApp.Web/**/*.{config,html,htm,js,dll,pdb,png,jpg,jpeg,gif,css}')
        .pipe(gulp.dest('D:/Websites/www.myapp.com/wwwroot'));
});

If for some reason you need more advanced file copy capabilities you can use Robocopy (née xcopy). We can use the robocopy node module to run it (npm install robocopy --save). Use the plugin as follows:

var robocopy = require('robocopy');

gulp.task('deploy', ['nunit'], function() {
    return robocopy({
        source: 'src/MyApp.Web',
        destination: 'D:/Websites/www.myapp.com/wwwroot',
        files: ['*.config', '*.html', '*.htm', '*.js', '*.dll', '*.pdb',
                '*.png', '*.jpg', '*.jpeg', '*.gif', '*.css'],
        copy: {
            mirror: true
        },
        file: {
            excludeFiles: ['packages.config'],
            excludeDirs: ['obj', 'Properties'],
        },
        retry: {
            count: 2,
            wait: 3
        }
    });
});

The robocopy function returns a promise so we can just return this to allow gulp to know when it has completed. You’ll also notice that robocopy is not a gulp plugin and this is ok. Unlike Grunt where everything is a plugin, gulp plugins are really only useful if they operate on a stream of files. Many times you will just invoke a module in a gulp task like we do above, no plugin involved at all. @ozcinc has a nice writeup on this here.

The Robocopy options above are pretty self explanatory. The mirror option allows you to synchronize your destination with your source folder, removing any deleted files. The retry options allow you to retry the copy after so many seconds if it failed. Both these options can be useful when deploying websites. The task fully supports all the robocopy options, see here for more info.

Database Migration

More than likely your app is data driven and you will need to deploy schema changes. The simplest approach is to apply a delta script. To do this we can use the sqlcmd-runner module (npm install sqlcmd-runner --save).

var sqlcmd = require('sqlcmd-runner');

gulp.task('database', ['robocopy'], function() {
    return sqlcmd({
        server: 'sql.mycompany.int',
        database: 'myapp',
        inputFiles: [ 'delta.sql' ],
        outputFile: 'delta.log',
        failOnSqlErrors: true,
        errorRedirection: true
    });
});

This module is a wrapper around the sqlcmd utility and supports all options; see here for more info.

If you’re using DB Ghost to create your delta script, you can use the dbghost module (npm install dbghost --save).

var sqlcmd = require('sqlcmd-runner');

gulp.task('database', ['robocopy'], function() {
    return dbghost.buildCompareAndCreateDelta({
        configSavePath: 'CreateDelta.dbgcm',
        changeManager: {
            reportFilename: 'CreateDelta.log',
            buildDatabaseName: 'Source',
            deltaScriptsFilename: 'delta.sql',
            templateDatabase: {
                name: targetSchema,
                server: 'sql.mycompany.int'
            },
            targetDatabase: {
                name: targetSchema,
                server: 'sql.mycompany.int'
            },
            schemaScripts: {
                rootDirectory: 'schema',
            }
        }
    })
    .then(function() {
        return sqlcmd({
            server: 'sql.mycompany.int',
            database: 'myapp',
            inputFiles: [ 'delta.sql' ],
            outputFile: 'delta.log',
            failOnSqlErrors: true,
            errorRedirection: true
        });
    });
});

This module is a wrapper around ChangeManagerCmd.exe and can either generate a config from scratch or from a template, overriding the template with the config passed in. See here for more info.

Nuget

If you are publishing a Nuget package instead of deploying an app there is a module for that too. We can use the nuget-runner module (npm install nuget-runner --save). You will need to create a nuspec file as described here. Use the module as follows:

var args = require('yargs').argv,
    Nuget = require('nuget-runner');

gulp.task('deploy', ['nunit'], function() {

    // Copy all package files into a staging folder
    gulp.src('src/MyLibrary/bin/Release/MyLibrary.*')
        .pipe(gulp.dest('package/lib'));

    var nuget = Nuget({ apiKey: args.nugetApiKey });

    return nuget
        .pack({
            spec: 'MyLibrary.nuspec',
            basePath: 'package', // Specify the staging folder as the base path
            version: args.buildVersion
        })
        .then(function() { return nuget.push('*.nupkg'); });
});

As demonstrated above you can set the version number and your nuget API key from an environment variable set by the build server. One thing to note is that even though you are passing in the version, the version element must exist in the nuspec file and have a value, otherwise pack will fail. The pack method returns a promise so we can just return this to allow gulp to know when it has completed.

You can also simplify the above code a bit more by specifying the the files in the .nuspec:

<package ...>
   <metadata>
      ...
   </metadata>
   <files>
      <file src="src\MyLibrary\bin\Release\MyLibrary.*" target="lib" />
   </files>
</package>

Then you can forgo creating the staging folder and specifying a basepath:

var Nuget = require('nuget-runner');

gulp.task('deploy', ['nunit'], function() {

    var nuget = Nuget({ apiKey: process.env.NUGET_API_KEY });

    return nuget
        .pack({
            spec: 'MyLibrary.nuspec',
            version: process.env.BUILD_NUMBER
        })
        .then(function() { return nuget.push('*.nupkg'); });
});

The module supports more commands and options than shown here, see here for more info.

Build Server

The last step is to setup your build server to run gulp. I’m going to demonstrate how to configure gulp with TeamCity but this should loosely apply to any build server.

  • Download and install Node.js.
  • Install gulp: npm install gulp -g -prefix="C:\Program Files\nodejs". You will need to set the prefix to a folder in the PATH. I simply put it in the node install directory along side NPM. By default this folder is added to the PATH by the Node.js installer. Note that depending on your UAC settings you may need to run that command in an elevated command prompt as Program Files can be locked down. Also the 32 bit version will be installed to Program Files (x86) by default.
  • Restart the TeamCity build agent so it picks up the Node.js path.
  • Create a Command Line build step in TeamCity, set Run to Custom script and enter the following as the Custom script:
call npm install
call gulp ci

Here we call gulp with the task we want to run.

TeamCity gulp task

As mentioned above you may want to pass parameters into your build script. There are a couple of ways to do this. First you can hard code them directly into the custom script above:

call npm install
call gulp ci --build-version 1.0.0.0 --nuget-api-key 78a53314-c2c0-45c6-9d92-795b2096ae6c

There are a couple of problems with this however. First, you don’t want to manually manage your version number when TeamCity already does that for you automatically. So you can take advantage of TeamCity predefined build parameters and dynamically pass the version number as follows:

call npm install
call gulp ci --build-version %build.number%

Now the Nuget API key could be hardcoded for some builds but what if you use the same API key for multiple builds? Again you can make use of build parameters as TeamCity allows you to create custom parameters at different levels. This allows you set the parameter in one place where it can be referenced by multilple builds. You would specify the custom parameter as you would the predefined one:

call npm install
call gulp ci --nuget-api-key %nuget.api.key%

Here you can see the custom build parameter set at the project level. All build configurations under it inherit this parameter.

TeamCity gulp task

Usually you will want to keep an eye on how long tasks take to execute. TeamCity allows you to customize notifications so you can include task timings in your notifications. To do this we’ll need to edit the build_successful.ftl template of your preferred notification type and add the following template. You can add it anywhere you want.

<b>Gulp Task Timings</b>
<br/>

<table border="0">
<#list build.buildLog.messages[1..] as message>
    <#assign tasks = message.toString()?matches(r".*\sFinished\s\'(.*)\'\safter\s(.*)")>
    <#if tasks && !tasks?groups[2]?contains("μ") ><tr>
        <td>${tasks?groups[1]}</td>
        <td>${tasks?groups[2]}</td>
    </tr></#if>
</#list>
</table>

Your notifications will then display task timings along the lines of this:

Gulp Task Timings

init           6.51 ms
assembly-info  1.9 s
config         119 ms
style-cop      71 ms
build          2.88 s
unit-tests     4.11 s
...

Tasks are displayed in the order they finish.

Final Thoughts

Hopefully this demonstrates how easy it is to setup a .NET build/deploy on Windows with gulp. If you are also doing client side development, this will be even more of a win as your tools (Like Karma, JSHint, etc) will be run by the same build tool.