Bundling in .NET Core MVC Applications with Gulp

Bundling in .NET Core MVC Applications with Gulp Header Image

Today’s header image was created by Isabell Winter, the original source for the image is available here

Last week I discussed using BundlierMinifier.Core and how it’s become the default option for bundling and minifying JavaScript and CSS in .NET Core MVC applications, and the week before I discussed using webpack for the same purpose.

This week, we’re going to talk about the elephant giant soda cup in the room: Gulp.js.

I’ll also go into why BundlerMinifier.Core was chosen as the default option for bundling.

So What Is Gulp.js?

Gulp.js (or just Gulp) is a JavaScript task runner, its goal is to help with automating all of the repetitive tasks in a build pipeline for client side stuff (JavaScript, CSS, images, that kind of thing). Tasks like linting, building changelogs, bundling, minfying, image compression, and everything in between are what Gulp has in it’s sights.

It does this by reading a JavaScript config file, which consists of tasks using predefined methods and plugins. It can be configured to run repetitive tasks as and when certain files are changed or as part of a build action.

Is Gulp Supported In .NET Core?

Before .NET Core RC2, Gulp was the task runner of choice in default .NET Core MVC applications. So the short answer is: yes, it’s supported.

However before RC2 was released, a discussion on an issue was started on the .NET Tooling GitHub repo with the objective of deciding which task runner to use: Gulp or Grunt.

You can read though that discussion here

In the ASP.NET Community Stand up for June 21st 2016, Jon Galloway, Damian Edwards and Scott Hanselman discussed why decision was made to drop Gulp for the .NET tooling. You can watch the whole video here:

Fair warning, it’s around 73 minutes long

Oh, and they were joined by Mads Kristensen too

For those who don’t want to watch the whole video, here are the crib notes:

  • .NET Core is brand new (like re-written from the ground up new), so why force people to learn that AND node AND Gulp, just to create MVC Apps?
  • Without including node and Gulp, creating a new project is lightning quick. Once node and Gulp are included in the new project wizard, there are over 1,500 files in the node_modules folder alone (not to mention the .NET Core packages to pull down) which need to be pulled down and installed by node.
  • .NET Framework had a namespace called System.Web.Optimization which handled all of the bundling and minification, so why not include something as similar to that as possible?

That last point is interesting since the .NET Core team view bundling as a design time action:

Bundling and minifying before deployment provides the advantage of reduced server load. However, it’s important to recognize that design-time bundling and minification increases build complexity and only works with static files.

That’s why it isn’t the default option, so let’s get on to using it in a project.

How Do I Add Gulp To My Project?

The first thing you’ll need to do is add a gulpfile to the root of your source tree.

gulpfile Location
Right about here should do it

The gulpfile contains a list of steps for gulp to take whenever you run it, much like the bundlerconfig.json and the webpack.config files that we’ve written in previous posts.

Here’s a very basic gulpfile (and what we’ll be using throughout this post):

/// <binding Clean='clean' />
"use strict";
var gulp = require("gulp"),
rimraf = require("rimraf"),
concat = require("gulp-concat"),
cssmin = require("gulp-cssmin"),
uglify = require("gulp-uglify");
var paths = {
webroot: "./wwwroot/"
};
paths.js = paths.webroot + "js/**/*.js";
paths.minJs = paths.webroot + "js/**/*.min.js";
paths.css = paths.webroot + "css/**/*.css";
paths.minCss = paths.webroot + "css/**/*.min.css";
paths.concatJsDest = paths.webroot + "js/site.min.js";
paths.concatCssDest = paths.webroot + "css/site.min.css";
view raw gulpfile-setup.js hosted with ❤ by GitHub

There’s a lot going on here, so let’s take a look at the contents in chunks.

var gulp = require("gulp"),
rimraf = require("rimraf"),
concat = require("gulp-concat"),
cssmin = require("gulp-cssmin"),
uglify = require("gulp-uglify");
view raw gulpfile-setup.js hosted with ❤ by GitHub

Here we’re simply defining all of the node modules that we want to include in order to run all of the tasks outlined in this file.

  • gulp is the task runner itself
  • rimraf is used to delete files from disk (we’ll use it as part of our clean tasks)
  • gulp-concat is used to bundle several files into one large file
  • gulp-cssmin is used to minify CSS files (we’ll use this on our bundled CSS)
  • gulp-uglify minifies JS files (we’ll use with this on our bundled JS) and is based on UglifyJs
var paths = {
webroot: "./wwwroot/"
};
paths.js = paths.webroot + "js/**/*.js";
paths.minJs = paths.webroot + "js/**/*.min.js";
paths.css = paths.webroot + "css/**/*.css";
paths.minCss = paths.webroot + "css/**/*.min.css";
paths.concatJsDest = paths.webroot + "js/site.min.js";
paths.concatCssDest = paths.webroot + "css/site.min.css";
view raw gulpfile-setup.js hosted with ❤ by GitHub

Here we’re defining the paths that we’ll use for our tasks and storing them in an array. We’ll use this array in our tasks, later.

Gulp Tasks

Underneath the setup (from above), we need to define some tasks. Here are some basic tasks that we’ll use in our gulpfile:

gulp.task("clean:js", function (cb) {
rimraf(paths.concatJsDest, cb);
});
gulp.task("clean:css", function (cb) {
rimraf(paths.concatCssDest, cb);
});
gulp.task("clean", ["clean:js", "clean:css"]);
gulp.task("min:js", function () {
return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest("."));
});
gulp.task("min:css", function () {
return gulp.src([paths.css, "!" + paths.minCss])
.pipe(concat(paths.concatCssDest))
.pipe(cssmin())
.pipe(gulp.dest("."));
});
gulp.task("min", ["min:js", "min:css"]);
gulp.task('default', ['min']);
gulp.task('watch', function() {
gulp.watch(paths.css, ['min:css']);
gulp.watch(paths.js, ['min:js']);
});

Before we take a look at the tasks in our gulpfile.js, it’s helpful to know the two forms that a gulp task can take:

// individual task
gulp.task("name:type", function (arguments) {
// steps that the task should take
});
// group of tasks
gulp.task("name", ["childTaskOneName:type", "childTaskTwoName:type"]);
view raw gulp-task-formats.js hosted with ❤ by GitHub

A task can either be self contained (lines 2-4 in the above example), or can depend on other tasks (line 7 in the example above).

The naming convention for self contained tasks is “nameOfTask:fileTypeThatIsActsOn”, so a task called “clean:css” tells use that it will clean the css files in a given path.

Now let’s take a look at some of the tasks in the example above, first the cleanup tasks:

gulp.task("clean:js", function (cb) {
rimraf(paths.concatJsDest, cb);
});
gulp.task("clean:css", function (cb) {
rimraf(paths.concatCssDest, cb);
});
gulp.task("clean", ["clean:js", "clean:css"]);

Here we’re defining a selection of cleanup tasks, two of which use the rimraf command alongside the relevant paths variable and one of which calls the other two.

gulp.task("min:js", function () {
return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest("."));
});
gulp.task("min:css", function () {
return gulp.src([paths.css, "!" + paths.minCss])
.pipe(concat(paths.concatCssDest))
.pipe(cssmin())
.pipe(gulp.dest("."));
});
gulp.task("min", ["min:js", "min:css"]);

Here is the real meat of our gulpfile, we’re defining the minification tasks here. Two of which will do the bundling for us, and the third will call the other two. Let’s take a look at one of them

Either will do, but let’s take a look at the JS minification task

gulp.task("min:js", function () {
return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest("."));
});

There’s a lot going on here, so let’s look at each line of the task in turn.

We’ll skip the first line, as it not really that interesting

return gulp.src([paths.js, "!" + paths.minJs], { base: "." })

Here we’re passing a node glob to the gulp.src command, which sets the source of this task. The node glob tells us which files to include:

  • paths.js

This tells the src method to use all files within the path.js path.

We defined that earlier, if you remember.

  • ! + paths.minJs

This tells the src method to exclude all files in the path.minJs path

Again, we defined this earlier

The object after the node glob tells the src method to strip a ‘.’ from all relevant source file names.

The return type of this method is a stream of Vinyl files. But we’re going to pipe the output of this to other functions before returning the stream of files.

If you’re familiar with Fluent Interfaces, this is sort of similar.

.pipe(concat(paths.concatJsDest))

This is the first of our piped method calls: we’re calling concat on the stream of files from the previous step and outputting the concatenated files to the paths.concatJsDest directory.

.pipe(uglify())

Here we’re minifying all of the files from the previous step (which is the concatenated JS file).

.pipe(gulp.dest("."));

Here we’re writing the minified file (from the previous step) to a path calculated from the source path (which was defined in the previous pipe command). We’re using the ‘.’ marker, which means: ‘output into the directory we’re in via the previous step’.

Default Tasks

The keen eyed amongst you all will have noticed that I’ve added two final tasks to the above example:

gulp.task('default', ['min']);
gulp.task('watch', function() {
gulp.watch(paths.css, ['min:css']);
gulp.watch(paths.js, ['min:js']);
});

The default task is a required task for gulp, without this it will not work. At the moment, we’re wrapping the default task around our minification task, but in the future we might want to include more default tasks.

The watch task is an interesting one. As the name implies, it will watch files in any directories that you give it (via it’s first argument) and will run any tasks (via it’s second argument) when it notices that any of changes to those files have been written to disk.

Installing Gulp

Before we can install gulp, we need to add a package.json file to our src directory with the following contents:

{
"devDependencies": {
"gulp": "3.8.11",
"gulp-concat": "2.5.2",
"gulp-cssmin": "0.1.7",
"gulp-uglify": "1.2.0",
"rimraf": "2.2.8"
}
}
view raw package.json hosted with ❤ by GitHub

This file will tell node (when we run it in a moment) which packages to download and install in order to run our gulp tasks.

So, lets install the node packages:

npm install -D gulp
npm install -D gulp-cli
view raw shell.sh hosted with ❤ by GitHub

Before we move on to running the Gulp tasks, I just wanted to show you the full version of the gulpfile that we’ve built:

/// <binding Clean='clean' />
"use strict";
var gulp = require("gulp"),
rimraf = require("rimraf"),
concat = require("gulp-concat"),
cssmin = require("gulp-cssmin"),
uglify = require("gulp-uglify");
var paths = {
webroot: "./wwwroot/"
};
paths.js = paths.webroot + "js/**/*.js";
paths.minJs = paths.webroot + "js/**/*.min.js";
paths.css = paths.webroot + "css/**/*.css";
paths.minCss = paths.webroot + "css/**/*.min.css";
paths.concatJsDest = paths.webroot + "js/site.min.js";
paths.concatCssDest = paths.webroot + "css/site.min.css";
gulp.task("clean:js", function (cb) {
rimraf(paths.concatJsDest, cb);
});
gulp.task("clean:css", function (cb) {
rimraf(paths.concatCssDest, cb);
});
gulp.task("clean", ["clean:js", "clean:css"]);
gulp.task("min:js", function () {
return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest("."));
});
gulp.task("min:css", function () {
return gulp.src([paths.css, "!" + paths.minCss])
.pipe(concat(paths.concatCssDest))
.pipe(cssmin())
.pipe(gulp.dest("."));
});
gulp.task("min", ["min:js", "min:css"]);
gulp.task('default', ['min']);
gulp.task('watch', function() {
gulp.watch(paths.css, ['min:css']);
gulp.watch(paths.js, ['min:js']);
});
view raw gulpfile.js hosted with ❤ by GitHub

That way, if you’re copy-pasting, you can grab it all from here.

Running Gulp Tasks From The Terminal

To run the gulp tasks from the terminal, it’s simply a case of running:

gulp
view raw shell.sh hosted with ❤ by GitHub

Which will produce output similar to this:

[14:19:53] Using gulpfile path/to/gulpfile.js
[14:19:53] Starting 'min:js'...
[14:19:53] Starting 'min:css'...
[14:19:53] Finished 'min:js' after 70 ms
[14:19:53] Finished 'min:css' after 84 ms
[14:19:53] Starting 'min'...
[14:19:53] Finished 'min' after 79 μs
[14:19:53] Starting 'default'...
[14:19:53] Finished 'default' after 109 μs
view raw shell.sh hosted with ❤ by GitHub

This is because the gulp command takes a task name as an argument, but if you don’t supply a task name it will use the default task.

Which is why it’s a required task for all gulpfiles.

To run the default task explicitly, we can run the following:

gulp default
view raw shell.sh hosted with ❤ by GitHub

Which will produce output similar to this:

[14:19:53] Starting 'min:js'...
[14:19:53] Starting 'min:css'...
[14:19:53] Finished 'min:js' after 70 ms
[14:19:53] Finished 'min:css' after 84 ms
[14:19:53] Starting 'min'...
[14:19:53] Finished 'min' after 79 μs
[14:19:53] Starting 'default'...
[14:19:53] Finished 'default' after 109 μs
view raw shell.sh hosted with ❤ by GitHub

See? I told you that gulp will run the default task if you don’t give it a task name

If we wanted to run the watch task, we could run this command:

gulp watch
view raw shell.sh hosted with ❤ by GitHub

This will produce output similar to this:

[14:18:32] Starting 'watch'...
[14:18:32] Finished 'watch' after 15 ms
view raw shell.sh hosted with ❤ by GitHub

And each time that you save a change to one of the watched files, you’ll get output similar to this in the terminal:

[14:27:36] Starting 'min:js'...
[14:27:36] Finished 'min:js' after 8.46 ms
view raw shell.sh hosted with ❤ by GitHub

I saved a change to one of my JavaScript files here, obviously

Let’s say that you want to run the min:css task:

gulp min:css
view raw shell.sh hosted with ❤ by GitHub

That’ll do it, which will produce output similar to this:

[14:20:26] Using gulpfile path/to/gulpfile.js
[14:20:26] Starting 'min:css'...
view raw shell.sh hosted with ❤ by GitHub

Running Gulp Commands From Within Visual Studio Code

The first thing you want to do it pull up the command palette (with Cmd+Shift+P or Control+Shift+P depending on which OS you’re running) and issue “ctr” (which is short for “Configure Task Runner”), then select Gulp.

This will alter your tasks.json (in the .vscode directory), injecting the following into it:

{
"version": "0.1.0",
"command": "gulp",
"isShellCommand": true,
"args": ["--no-color"],
"showOutput": "always"
}
view raw tasks.json hosted with ❤ by GitHub

Once you’ve done this, you can bring up the command palette again and issue “rtask” (short for Run Task), and select “Run Task”. This will show all of the tasks that you have in your gulpfile. Selecting one will run it, and the output will show in the output window.

gulp task output - visual studio code
The output should look a little like this

You can do this with any of your tasks (just like when running from the terminal).

Running Gulp Commands From Within Visual Studio

Those are all well and good, but what if you’re running Visual Studio? Well, you have to use the Task Runner Explorer.

task runner explorer - gulpfile loaded
Visual Studio’s Task Runner Explorer with our gulpfile loaded

That’s the fella

You can get to the Task Runner Explorer in a few ways:

  • Right-click on the gulpfile and select “Task Runner Explorer”
  • Entering the Keyboard shortcut Ctrl+Alt+Backspace
  • Selecting it from the View menu
task runner explorer - view menu location
It’s hidden under a submenu, so I’ve included this screen shot to make it easier to find

Running one of the tasks is as easy as:

  • Find the task in the left-hand pane of the Task Runner Explorer
  • Right-click on the task
  • Select Run
task runner explorer - run option
We can run a task by right-clicking on it, then choosing Run

You can also bind any of the tasks here, too. This means that they will be run each time that a given event (like a build completes, or the project opens).

task runner explorer - bind options
Here are the bind options for our tasks. These are available for all tasks that are loaded into the Task Runner Explorer (not just Gulp ones)

Lets say that I wanted the clean:css task to run after every build. All I’d have to do is right click on it, follow the Bindings menu and click on After Build. Now each time that a successful build happens, my css would be wiped out afterwards.

Talk about a useful task to run each time 😛

Fair warning here: if your tasks become too big (say, you’re bundling about 20 JS files then minifying them), then your build times will increase dramatically.

But that should only be a problem if you’re bundling and minifying in dev, which isn’t an accepted practise.

And that’s how you can use gulp in your MVC applications. Pretty cool, huh?

A .NET developer specialising in ASP.NET MVC websites and services, with a background in WinForms and Games Development. When not programming using .NET, he is either learning about .NET Core (and usually building something cross platform with it), speaking Japanese to anyone who'll listen, learning about languages, writing for his non-dev blog, or writing for a blog about Retro Gaming (which he runs with his brother)