Removing test dependencies from .NET's release

There are a multitude of ways on how one can structure a .NET project. It's no wonder one can also have a multitude of downsides when chosing one of them. At my $CURRENT_JOB we introduced Fable to our project and guess what? We had problems with our testing infrastructure that required some tweaking to make it happen (I'll not get into the problem details here 😜).

After some trial and error I got the conclusion that our solution would get the most out of it if we just had a src and a tests directory inside each project. You can picture its structure like this:

./Solution.sln
./src/API/src/*.fs
./src/API/tests/*.fs
...

However, while this solved most of our previous issues, it also introduced two new problems:

  1. test code would ship with our release binary
  2. we have multiple EntryPoints on our solution, how will this work out?

Removing unwanted code from our releases

I assumed that .NET would remove code with "Test" attributes and unused code from the release artifact. Oh boy, was I wrong. This is a bummer because you don't want to ship more information than is needed as one might be able to strip debug symbols or even decompile a bunch of code you may not want others to see.

After some research, I found this Stack Overflow answer showing a way of disabling this code during release. They even introduced a new way of structuring tests, how nice! This is how my .fsproj file looked as after some testing:

<!-- Project-related group -->
<ItemGroup>
    <!-- Project files -->
    <Compile Include="src/Program.fs" />

    <!-- Project-related dependencies -->
    <PackageReference Include="FsToolkit.ErrorHandling" Version="4.4.0" />
</ItemGroup>

<!-- Test-related group -->
<ItemGroup Condition="'$(Configuration)' != 'Release'">
    <!-- Test files -->
    <Compile Include="tests/Shared.fs" />

    <!-- Test dependencies -->
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*"/>
</ItemGroup>

Now, the only important part here is the attribute Condition="'$(Configuration)' != 'Release'". Basically it's saying that this ItemGroup should only be evaluated if we are not releasing. You don't need to organize your project file like I did but, personally, I liked how it looked.

Dealing with EntryPoints

You can see that I left the test dependency Microsoft.NET.Test.Sdk. It was on purpose, this almost made me have a mental breakdown! I kept getting the following error:

error FS0433: A function labeled with the 'EntryPointAttribute' attribute must be the last declaration in the last file in the compilation sequence.

No matter how much I messed with the project file, OutputTypes and EntryPoints, I couldn't get this error1 to go away! At this point, removing as much code as possible from the error context helped a lot. Such as that I discovered the problem when I had this single dependency left!

Again, someone may have faced the same issue, right? Yes, someone have! The fix is really simple, just add this attribute on your project file:

<GenerateProgramFile>false</GenerateProgramFile>
  1. Being honest here, I'm glad I have setup my project to [error on build warnings]({{< relref "safer-fsharp-environments" >}}).