CSBrick: Reuse Your Projects as Source Includes in C#

Updated on 2019-12-16

Easily reuse source from entire projects at the source level instead of the binary level

Introduction

.NET has a lot of cool features to help make your apps deploy exactly like you want them to. Unfortunately, static linking - the ability to embed dependent assembly code - isn't one of them.

CSBrick is something of a workaround for this. What it does, is gather all the source for a particular project it is pointed to, and then merge and minify it into a "source brick" - a single C# file you can easily include into your projects instead of referencing the equivalent binary class library. Extra whitespace and comments are stripped since this is never meant for human reading - the original files it came from are. Global usings and #defines are moved to the top of the file, and duplicate usings are removed. Doc comments are preserved.

This code is part of my Build Pack - a suite of build tools and code to make building build tools easier and better. Build tools can't lug around extra DLLs because it complicates using them as pre-build steps so it's important to keep all the code in the single executable. This tool is perfect for that.

Building this Mess

The build pack uses itself to build itself, but since I've stripped all the binaries from the zip I gave you, you'll have to jumpstart it. Open it in Visual Studio and build in Release a couple of times until the file locking errors go away. This is VS hicupping because I use a circular build step, which is okay, but it just causes these messages to come up. After the second build, you should be okay, but flip to your Output pane to be sure the build succeeded since the Error pane tends to get "stuck" on this. Finally, switch to Debug and you're ready to roll. Any time you need to rebuild, you'll have to rebuild in release for changes to the build tools themselves to be reflected in the next build. This is because these projects use the release binaries of other projects in the same solution as build steps to build themselves.

Using this Mess

Using it is pretty straightforward. Here's the using screen:

CsBrick merges and minifies C# project source files

csbrick.exe <inputfile> [/output <outputfile>]
[/ifstale] [/exclude [<exclude1> <exclude2> ... <excludeN>]]
   [/linewidth <linewidth>] [/definefiles]

   <inputfile>     The input project file to use.
   <outputfile>    The output file to use - default stdout.
   <ifstale>       Do not generate unless <outputfile> is older than <inputfile>
                   or its associated files.
   <exclude>       Exclude the specified file(s) from the output.
                   - defaults to "Properties\AssemblyInfo.cs"
   <linewidth>     After the width is hit, break the line at the next opportunity
                   - defaults to 150
   <definefiles>   Insert #define decls for every file included

must be a .csproj file. It works with Visual Studio 2017 projects but should work with others too. I just haven't tried it with other versions.

You can add it as a pre-build event for your project in Visual Studio. Simply go to your Project|Properties|Build Events, and add the command line for it. I recommend using the macros like $(SolutionDir) and $(ProjectDir). For example, the included Deslang project has this build event:

$(SolutionDir)CSBrick\bin\Release\csbrick.exe
$(SolutionDir)CodeDomGoKit\CodeDomGoKit.csproj /output $(ProjectDir)CodeDomGoKit.brick.cs

In this case, it takes all the code from the CodeDomGoKit.csproj and packs it into a single file, CodeDomGoKit.brick.cs in its own project directory, which it then includes. This is basically the equivalent of going to References and adding CodeDomGoKit, but without the extra assembly, and that's rather the point.

Don't edit the brick file. Change the original source the brick file was made from. It will be regenerated automatically any time that happens due to the build event.

Coding this Mess

I've coded a much earlier version of this before but this one is far more suitable to my needs. It uses my ParseContext class which I cover here. It's not actually parsing C#, but rather doing remedial tokenization of C# and throwing away extra tokens like whitespace and (non doc) comments. The only real gotcha is knowing when to throw away whitespace, and also to skip things like the inside of strings.

I cover here

It's all in Minifier.cs, mainly MergeMinifyBody(). Most of it is a large switch case doing things like this:

case '\"':
    isIdentOrNum = false;
    pc.TryReadCSharpString();
    writer.Write(pc.GetCapture());
    ocol += pc.CaptureBuffer.Length;
    break;
case '\'':
    isIdentOrNum = false;
    pc.TryReadCSharpChar();
    writer.Write(pc.GetCapture());
    ocol += pc.CaptureBuffer.Length;
    break;

Any time we set isIdentOrNum, the next token will have a space put in front of it, otherwise no whitespace will be emitted. ocol keeps track of our output column so we can break the line once we've exceeded the line width (recommended 150) - this keeps editors from choking on one single long line in the output.

Using Minifier.cs is easy, you just call this method:

void MergeMinify(TextWriter writer, int lineWidth = 0,
                 bool defineFiles=false,params string[] sourcePaths)

like so:

Minifier.MergeMinify(output, linewidth, definefiles,
    inputs.ToArray());

Where output is your output TextReader, linewidth is the desired line width (recommended 150), definefiles is true to insert #defines for each file included, like #define FOO_CS for "foo.cs", and inputs is an array of string filenames to process.

Finally, the big nasty mess you get back is just all the code from the project (accept AssemblyInfo.cs) packed into one file. Use that instead of referencing the project. It bloats your binary size, but keeps you from needing to drag around a DLL. This is why I say it's a workaround for lack of static linking.

History

  • 16th December, 2019 - Initial submission