Migrating to the new NET SDK MSBuild Project Files

mountain road The mountain road of migration that isn’t all that scary

Farmhash.Sharp is a pet project of mine where I port Google’s Farmhash algorithm to the .NET platform, and I recently migrated it to use the new MSBuild architecture. Previously the project was structured based on the ProjectScaffold, but was found to be deficient. While the migration was not without some difficulties, in the end, I’ve found it to be worthwhile, and figured I’d document the process for my future self and others.

There was not a single keystone issue that caused the migration, but rather a series of small, annoying issues.

F#

The testing project for Farmhash.Sharp was a F# project using Nunit. Under .NET Core, a MarshalDirectiveException would be thrown:

System.Runtime.InteropServices.MarshalDirectiveException: Cannot marshal ‘parameter #2’: Unknown error.

The only internet help I found was an xunit issue with no resolution. I am guessing the root cause is that F# support for .NET Core is bad even though the .NET Core train has been on the horizon for at least a couple years. I have a love-hate relationship with F#. F# could be good but one loses hope after years of trying to run a simple test project and receiving cryptic errors.

The fix was to convert the F# Nunit project to C# Xunit. With a little editor skills and InlineData the tests were quickly migrated now could be executed on a multitude of platforms.

Github Pages Site

ProjectScaffold sets up a basic documentation site. It does have some nice features where it’ll include release notes, license, and a API reference page where you can see the code comments converted to Markdown. Also if F# code is used in the code samples, users can hover over lines for IDE-like tooltips.

The downside is that I find the style outdated. The bootstrap version is 2.2.1 which was released five years ago. I could go through all the work to update the site to a newer bootstrap, jquery, and jquery UI, but I thought it prudent to just use Github pages supported jekyll theme, which would be a lot quicker to update, as I only have to focus on the content. Losing the code tooltips is only a minor loss as they were only for F# and C# won the language war.

The only thing I really miss is since I have the release notes inlined in github pages, whenever I make a release I have to copy and paste from the main repo into the gh-pages branch, which is annoying but I’ve only done three Farmhash.Sharp releases so the annoyance is definitely tolerable.

Since moving to a Github Pages theme I find the site to be much more mobile friendly and stylish!

Paket and Fake vs .NET Cli and Nuget

The ProjectScaffold sets up a project to use the Paket dependency manager and the FAKE build tool. The problem with these tool is that they are quickly being outclassed. Nuget is now tightly integrated with MSBuild and solved many of the problems that Paket originally touted. Now instead of having way too many paket.* files, all dependencies are specified in the project file.

FAKE would build projects, build docs, create nuget packages, etc. While nice, a lot of that functionality is now built into the .NET cli, and since we’re using Jekyll and Github Pages, we no longer need our documentation built. The only feature I miss is having release notes and version propagated when changed, and FAKE creating Github releases. These losses are tolerable as Farmhash.Sharp releases are few and far between and updating release notes is a tasks that mainly involves copying + pasting.

The bottom line is, why should I continue using these tools if 95% of the functionality I want is built in to the new SDK which will be much more approachable to new users.

The Migration

Hopefully the decision to move to C# everywhere with the new .NET SDK is reasonable. I’m tired of using non-first class tools. Whether it’s Microsoft’s or someone else’s fault, I don’t care. I just want my project to be built, tested, and ran on all relevant platforms. Trying to be clever with build tools is just as bad as trying to be clever with code.

While the .NET SDK does provide dotnet-migrate I elected to start fresh because the Farmhash.Sharp code is relatively small and gives me an excellent chance to learn the new build system.

How did the resulting project file for the main hashing algorithm turn out? You tell me, here’s code with the assembly information omitted, which by the way is awesome as it used to always be a source of conflicts or contention with git.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard1.0</TargetFramework>
    <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
    <PlatformTarget>AnyCPU</PlatformTarget>
  </PropertyGroup>

</Project>

Let’s step it up a notch. Let’s take the benchmarking code which runs benchmarks across .NET Core, Mono, and the full .NET framework. Some hash functions that will be benchmarked can only be ran on the full .NET framework, so they need to be conditionally referenced. Below is the entirety of the benchmarking project file, which I believe is so self-explanatory that it can be left uncommented.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>netcoreapp1.1;net46</TargetFrameworks>
    <PlatformTarget>AnyCPU</PlatformTarget>
    <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
  </PropertyGroup>

  <PropertyGroup Condition=" '$(TargetFramework)' == 'net46' ">
    <DefineConstants>$(DefineConstants);CLASSIC</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.1' ">
    <DefineConstants>$(DefineConstants);CORE</DefineConstants>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.10.3" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Farmhash.Sharp\Farmhash.Sharp.csproj" />
  </ItemGroup>

  <ItemGroup Condition="'$(TargetFramework)' == 'net46'">
    <PackageReference Include="CityHash.Net" Version="1.0.1"/>
    <PackageReference Include="SpookilySharp" Version="1.1.5128"/>
    <PackageReference Include="System.Data.HashFunction.CityHash" Version="1.8.2.2"/>
    <PackageReference Include="System.Data.HashFunction.SpookyHash" Version="1.8.2.2"/>
    <PackageReference Include="xxHashSharp" Version="1.0.0"/>
  </ItemGroup>

</Project>

After the migration was said and done, there were 50% fewer lines in the repo. For a project where all the functionality is packed into 580 lines of code, that’s incredible.

Ensuring Farmhash.Sharp worked on Classic .NET and Mono

As part of the build process I wanted to ensure that the built Farmhash.Sharp DLL output could be used in a classic .NET MSBuild project as well as a Mono project. This was the hard part, but in retrospect isn’t that hard as long as the following steps are taken.

  1. Create a regular .NET 4.5 Project used for testing with Xunit, Nunit, or etc. I am calling mine Classic
  2. Exclude this project as part of the regular build process in the solution file.
  3. Add in a reference to the testing code and nuget testing packages.
  4. In travis and appveyor files one will need to explicitly reference nuget to restore these packages (remember, nuget wasn’t as integrated as before!)
  5. Then invoke xbuild or msbuild (depending on platform) manually to build the test dll
  6. Invoke the xunit console app with the output dll
  7. Once the tests pass you have confirmed that the library works as expected on Mono or the full .NET framework.

If anything is confusing, please see .travis.yml or appveyor.yml in the repo.

Conclusion

The new .NET SDK is a breeze to work with. Once MSBuild becomes cross platform for Mono projects there will be very little reason to ever want to use the old project files.

Comments: