The code is here.
Thanks to Mike for reviewing this.
I have always been mildly irritated by how many .net projects I need to create in my standard workﬂow.
Usually I start with an idea for a library; I then want to test it with a simple executable; write some XUnit tests for it and ﬁnally benchmark some key scenarios. So I end up with at least four projects to manage.
Sure, I can ﬁnd ways to automatically generate those projects, but I have always been weary of codegen to solve complexity issues. It always ends up coming back to bite you. For those of you as old as I am, think MFC …
So what is my ideal world then? Well, let’s try this:
- One single project for the library and related artifacts (i.e. test, benchmarks, etc…).
- Distinguish the library code from the test code from the benchmark code by some convention (i.e. name scheme).
- Generate each artifact (i.e. library, tests, benchmarks, executable) by passing different options to
- Create a new project by using the standard
- Have intellisense working normally in each ﬁle for my chosen editor (VSCode).
- Work with
dotnet watchso that one can automatically run tests when anything changes.
What follows, despite working ﬁne, is not the standard way .net tools are used. It is not in the ‘golden path’. That is problematic for production usage as:
- It might not work in your particular conﬁguration.
- It might not work with other tools that rely on the presence of multiple projects (i.e. code coverage? …).
- It might work now in all scenarios, but get broken in the future as you update to a new framework, sdk, editor.
- It might expose bugs in the tools, now or later, which aren’t going to be ﬁxed, as you are not using the tools as intended.
- It might upset your coworkers that are used to a more standard setup.
I need to write a blog post about the concept of the ‘golden path’ and the perils, mostly hidden, of getting away from it. The summary, it is a bad idea.
Having said all of that, for the daring souls, here is one way to achieve most of the above. It also works out as a tutorial on how the different components of the .NET Core build system interacts.
How to use it
Here are the steps:
dotnet new -i Lucabol.SingleSourceProject.
- Create a directory for your project and cd to it.
dotnet new lsingleprojectand optionally
--standardVersion <netstandardXX> --appVersion <netcoreappXX>.
- Either modify the
Bench.csﬁles or create your own with this convention:
- Code for the executable goes in potentially multiple ﬁles named
- Code for the tests goes into ﬁles named
- Code for the benchmarks goes into ﬁles named
.csﬁle not following the above conventions is compiled into the dll.
- Code for the executable goes in potentially multiple ﬁles named
dotnet build -c releaseto build
releaseversion of your dll. This doesn’t include any of the main, test or bench code.
dotnet build -c mainor
dotnet build -c main_releaseand the corresponding
dotnet run -c ..build and run the exe.
dotnet build -c test,
dotnet build -c test_releaseand
dotnet test -c testbuild and run the tests.
dotnet build -c bench,
dotnet run -c benchbuild and run the benchmark.
How it all works
The various steps above are implemented as follows:
dotnet new -i ... install a custom template that I have created and pushed on NuGet.
The custom template is composed of the following ﬁles:
There is one ﬁle for each kind of artifact that the project can generate: library, program, tests and benchmark. The ﬁles follow the name terminating conventions, as described above.
The project ﬁle is identical to any other project ﬁle generated by
dotnet new except that there is one additional line appended at the end:
<Import Project="Base.targets" />
msbuild to include the
Base.targets ﬁle. That ﬁle has most of the magick. I have separated it out so that you can use it unchanged in your own projects.
We start by removing all the ﬁle from compilation except the ones that are used to build the library.
<Compile Remove="**/*Bench.cs;**/*Test.cs;**/*Main.cs" />
We then conditionally include the correct ones depending on which conﬁguration is chosen. Please notice the last line, which instruct
dotnet watch to watch all the
.cs ﬁles. By default it just watches the ones in the
<Compile Include="**/*Test.cs" Condition="'$(Configuration)'=='Test'"/>
<Compile Include="**/*Test.cs" Condition="'$(Configuration)'=='Test_Release'"/>
<Compile Include="**/*Bench.cs" Condition="'$(Configuration)'=='Bench'"/>
<Compile Include="**/*Main.cs" Condition="'$(Configuration)'=='Main'"/>
<Compile Include="**/*Main.cs" Condition="'$(Configuration)'=='Main_Release'"/>
<Watch Include="**\*.cs" />
Then we need to deﬁne the references. Depending on what you are building you need to include references to the correct NuGet packages (i.e. if you are building
test you need the
xunit packages). This is done below:
<ItemGroup Condition="'$(Configuration)'=='Bench' OR '$(Configuration)'=='Debug'">
<PackageReference Include="BenchmarkDotNet" Version="0.11.3" />
<ItemGroup Condition="'$(Configuration)'=='Test' OR '$(Configuration)'=='Test_Release' OR '$(Configuration)'=='Debug'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
One thing to notice is that most references are also included in the
debug conﬁguration. This is not a good thing, but it is the only way to get VSCode Intellisense to work for all the ﬁles in the solution. Apparently, IntelliSense uses whatever reference are deﬁned for the
debug build in
debug is special, if you wish …
But that’s not enough. When you create your own
MsBuild conﬁgurations, you also have to replicate the properties and constants that are set in the
release conﬁgurations. You would like a way to inherit them, but I don’t think it is possible.
It is particularly important to set the
TargetFramework property, as it needs to be set to
netcoreappXXX for the main, test and benchmark conﬁgurations. I give an example of the
Test_release conﬁgurations below. The rest is similar:
The .template.conﬁg/template.json ﬁle
This is necessary to create a
dotnet new custom template. The only thing to notice is the two parameters
appVersion that gives the user a way to indicate which version of the .NET Standard to use for the library and which version of the application framework to use for Main, Test and Bench.
"author": "Luca Bolognese",
"classifications": [ "Classlib", "Console", "XUnit" ],
"name": "One single Project",
"description": "One single Project for DLL, XUnit, Benchmark & Main, using configurations to decide what to compile",
Now that you know how it all works, you can make an informed decision if to use it or not. As for me …