Using Grunt to Build and Deploy .NET Apps December, 2013
UPDATE: I’ve since moved to gulp as my build tool of choice and highly recommend using it over Grunt. I’ve rewritten this post for gulp here.
I’ve been using Rake and Albacore to build and deploy .NET apps for a while now. Its a huge improvement over the XML based build tools but the rise of Node.js got me interested in moving to Grunt. I was also shelling out to Grunt from Rake for all client side tasks (like testing, linting, ect) so it only made sense to consolidate these into the same build tool. Turns out its pretty easy to do.
If you’re not familiar with NPM or Grunt take a look here and here for an introduction.
Project Layout
The project layout will be along the lines of this:
/
/src
MyApp.sln
...
/...
gruntfile.js
package.json
...
At the root of your project will be a package.json
that specifies your NPM dependencies. Also a gruntfile.js
which is your build script.
Initial Setup
- Download and install Node.js.
- Create a minimal
package.json
at the project root (You can also usenpm init
):
{
"name": "MyApp",
"version": "0.0.0"
}
- Create a bare bones
gruntfile.js
at the project root:
module.exports = function(grunt) {
grunt.registerTask('default', []);
grunt.registerTask('ci', []);
grunt.initConfig({
});
}
Above we have a special default
task alias that gets run when you type grunt
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).
- Install
grunt-cli
globally (-g
):npm install grunt-cli -g
- Install
grunt
locally (No-g
) and save the dependency to yourpackage.json
(--save
):npm install grunt --save
- Run
grunt
to make sure all is working, should displayDone, without errors.
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 TeamCity 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 grunt-dotnet-assembly-info task (npm install grunt-dotnet-assembly-info --save
). In your gruntfile.js
, load the task and configure it as follows:
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-dotnet-assembly-info');
...
grunt.registerTask('ci', ['assemblyinfo']);
grunt.initConfig({
assemblyinfo: {
options: {
files: ['src/MyApp.sln'],
info: {
version: process.env.BUILD_NUMBER,
fileVersion: process.env.BUILD_NUMBER,
company: 'Planet Express',
copyright: 'Copyright 3002 (c) Planet Express',
...
}
}
}
});
}
The task supports all the standard assembly attributes, see here for more info.
Building
Now the most important step, building. To do that we will use the grunt-msbuild task by @stevewillcock (npm install grunt-msbuild --save
). In your gruntfile.js
, load the task and configure it as follows:
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-dotnet-assembly-info');
grunt.loadNpmTasks('grunt-msbuild');
...
grunt.registerTask('ci', ['assemblyinfo', 'msbuild']);
grunt.initConfig({
...
msbuild: {
src: ['src/MyApp.sln'],
options: {
projectConfiguration: 'Release',
targets: ['Clean', 'Rebuild'],
stdout: true
}
}
});
}
The task supports more options than shown here, see here for more info.
On a side note, you may run into the following 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 Grunt task for that. We will use the grunt-nunit-runner task (npm install grunt-nunit-runner --save
). In your gruntfile.js
, load the task and configure it as follows:
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-dotnet-assembly-info');
grunt.loadNpmTasks('grunt-msbuild');
grunt.loadNpmTasks('grunt-nunit-runner');
...
grunt.registerTask('ci', ['assemblyinfo', 'msbuild', 'nunit']);
grunt.initConfig({
...
nunit: {
options: {
files: ['src/MyApp.sln'],
teamcity: true
}
}
});
}
The teamcity
option integrates the test results with TeamCity. The task supports many more options than shown here, see here for more info.
Deploying
I’m a big fan of using Robocopy (née xcopy) to deploy web apps. We can use the grunt-robocopy task to run it (npm install grunt-robocopy --save
). In your gruntfile.js
, load the task and configure it as follows:
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-dotnet-assembly-info');
grunt.loadNpmTasks('grunt-msbuild');
grunt.loadNpmTasks('grunt-nunit-runner');
grunt.loadNpmTasks('grunt-robocopy');
...
grunt.registerTask('ci', ['assemblyinfo', 'msbuild', 'nunit', 'robocopy']);
grunt.initConfig({
...
robocopy: {
options: {
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', 'fubu-content', 'Properties'],
},
retry: {
count: 2,
wait: 3
},
}
}
});
}
The 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 in particular have been useful when deploying websites. The task fully supports all the robocopy options, see here for more info.
Nuget
If you are publishing a library instead of deploying an app there is a package for that too. We can use the grunt-nuget task by @somaticIT (npm install grunt-nuget --save
). In your gruntfile.js
, load the task and configure it as follows:
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-dotnet-assembly-info');
grunt.loadNpmTasks('grunt-msbuild');
grunt.loadNpmTasks('grunt-nunit-runner');
grunt.loadNpmTasks('grunt-nuget');
...
grunt.registerTask('deploy', ['assemblyinfo', 'msbuild', 'nunit', 'nugetpack', 'nugetpush']);
grunt.initConfig({
...,
nugetpack: {
myApp: {
src: 'MyLib.nuspec',
dest: './'
},
options: {
version: process.env.BUILD_NUMBER
}
},
nugetpush: {
myApp: {
src: '*.nupkg'
},
options: {
apiKey: process.env.NUGET_API_KEY
}
}
});
}
The grunt-nuget
package ships with nuget so there is no need to install it on your build server. As demonstrated above you can dynamically set the version number and pass in your nuget API key. 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 nuget pack
will fail. The options you specify are passed directly to nuget so all nuget CLI parameters are supported.
Build Server
The last step is to setup your build server to run Grunt. I’m going to assume you’re using TeamCity for your builds. Follow the steps below on your build server:
- Download and install Node.js.
- Install
grunt-cli
:npm install grunt-cli -g -prefix="C:\Program Files\nodejs"
. You will need to set the prefix to be a path TeamCity can access. 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 asProgram Files
can be locked down. Also the 32 bit version will be installed toProgram 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, setRun
toCustom script
and enter the following as theCustom script
:
call npm install
call grunt ci
NOTE: If your TeamCity agent directory is not on the system drive you will hit a bug in NPM (As of the date of this post anyways). To get around it just add call npm config set cache d:\temp\npm-cache
before call npm install
above. You want the cache path to be on the same drive as the TeamCity agent directory, in this example its on D:
.
Conclusion
Hopefully this demonstrates how easy it is to setup a .NET build/deploy with Grunt. If you are 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.