There are a lot of solutions to this, such as Grunt or gulp.js, or the new Bundler & Minifier Visual Studio extension by Mads Kristensen. These are all great options, though sometimes grunt and gulp can feel a bit like using a crane to crush a fly if all you want is minification (and require adding the notoriously complex graph of dependencies that is Node.js to your application).
So it seemed I was down to rolling my own solution. This led me to researching
MSBuild Task Writing.
The jist is this: you can create a task that runs at a specified point during
your project’s comppilation/build/publish operation. You just have to implement
a class that inherits from the
Microsoft.Build.Utilities.Task class and
implements a signle method:
Execute(). Here’s what I came up with:
This task uses NUglify (the same library the
Bundler & Minifier extension uses under the hood) to create minified versions of
all of the
.css files inside your project directory (the project
directory is inferred from the
BuildEngine.ProjectFileOfTaskNode property, which
contains the path to your project’s
Here, I’m creating the minified files at the same location of the original file,
but appending an MD5 hash and
.build.min suffix to the filename. This serves two
purposes: the MD5 hash ensures that the user’s browser will always request the new
file if the contents have changed (no more telling users to try hitting CTRL+F5 or
clearing their browser’s cache!), and also make it easy to visually distinguish which
files are minified or not based on the
.build.min portion of the filename.
The files are then passed through GZip compression. The trick here is that IIS’s static file hanlder will automatically serve up the GZipped file if it exits in the same directory as the non-GZipped version. So for instance, if you have the following files deployed to your web server:
When the browser makes a request for
myapp/js/myfile.min.js, IIS will notice that there’s a file
with the same name in the same folder except with an additional
.gz suffix, and
serve that file to the user. This prevents IIS from having to GZip the file at runtime,
which should reduce server load and response time.
In order for the task to be executed, we need to add a
<UsingTask /> statement
to the project’s
.csproj file. This requires opening the file in a text editor
(or something like Visual Studio Code). If you look near the bottom, you should see
some commented-out sample
UsingTask statements. Here’s the final configuration I wound
<UsingTask TaskName="MyWebApp.MinifyWebAssets" AssemblyFile="bin\MyWebApp.dll" /> <Target Name="AfterCompile"> <MinifyWebAssets ContinueOnError="WarnAndContinue" /> </Target>
I found that using the
AfterCompile target worked best for me,
as when using
AfterBuild the generated files were not included
with the application publish. You may wish to try playing with the
various available targets.
Deploying the Minified Files
So, now that I had the files being generated at build time, I needed
a way to get MSBuild to include the minified files along with a publish.
Remember, the files are not actually part of the project (and in fact I added
.gitignore rules to prevent any of the
.build.min.* files from being
committed to my source code repository).
For that, I found some documentation on the ASP.NET MVC website about deploying extra files during an msdeploy (although I don’t think it’s really specific to MVC at all).
To do that, I needed to add another bit of XML to the end of my application’s
.csproj file, again using Notepad or VS Code.
<Target Name="CustomCollectFiles"> <ItemGroup> <_CustomFiles Include="**\*.build.min.*" Exclude="bin\**\*.build.min.*;obj\**\*.build.min.*" /> <FilesForPackagingFromProject Include="%(_CustomFiles.Identity)"> <DestinationRelativePath>%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath> </FilesForPackagingFromProject> </ItemGroup> </Target> <PropertyGroup> <CopyAllFilesToSingleFolderForPackageDependsOn> CustomCollectFiles; $(CopyAllFilesToSingleFolderForPackageDependsOn); </CopyAllFilesToSingleFolderForPackageDependsOn> <CopyAllFilesToSingleFolderForMsdeployDependsOn> CustomCollectFiles; $(CopyAllFilesToSingleFolderForMsdeployDependsOn); </CopyAllFilesToSingleFolderForMsdeployDependsOn> </PropertyGroup>
Exclude attribute. I found through trial and error that without
the exclude attribute, MSDeploy would recursively keep generating files in
it’s temp build folder until a stack overflow occurred.
Now your pre-minified files should get deployed along with your web app despite not being included in the project and not needing to be checked in to source control!
There was actually one more bit of customization that I needed to do for the
application I was working on, which was re-writing the
<script /> tags at runtime
to point to the minified files when not running in debug mode, but that was
pretty specific to the particular way the app was configured, so I don’t know
how valuable that would be to reproduce here.