CSProj to XProj: Supporting .NET Core using the Preview tools

With the RTM of the lovely .NET Core, I've now added support for .NET Standard 1.0 to ReadableExpressions and AgileMapper. Targeting .NET Standard 1.0 gives support for the following for free!

  • .NET Core
  • .NET Framework 4.5
  • Mono and Xamarin
  • Universal Windows Platform 10.0
  • Windows 8.0
  • Windows Phone 8.1
  • Windows Phone Silverlight 8.0

Both libraries previously supported .NET 4.0, so the NuGet packages also contain a .NET 4.0 assembly.

Targeting .NET Standard was... non-trivial. Y'see, while .NET Core is at RTM, the tools for Visual Studio 2015 are still in preview. I thought maybe Visual Studio 15 might work, so I got Preview 5 aaaaaaand...

Vs15SaysNo

...computer said no. Oh well –  using the Dapper and AutoMapper codebases as examples to help me along the way, here's how I added the support using VS2015 Update 3.

Replace csproj with xproj

xproj may not be here to stay (boooooooo), but if you want to cross-compile a library – build one assembly for, e.g. .NET Standard 1.0 and .NET 4.0 – this is currently how it's done.

So! I removed the old, out-dated Class Library project:

Remove

...and added a new, sparkly .NET Core one:

AddNew

...naming the new project something different to the old csproj library of course, because that still existed on disk. Next, I removed the new project:

RemoveCore

...and headed to windows explorer where I renamed it to match the old csproj, and moved it and its project.json into the original csproj folder. Then I re-added it to the solution, giving me this:

AddedXProj

Because of the wonder of xproj, the folder contents all show up without having to manually add them. Hurrah!

Sort out project.json

project.json is also apparently not here to stay (boooooooo x2), but again – this is how it's currently done.

The default file looks like this:

{
  "version": "1.0.0-*",

  "dependencies": {
    "NETStandard.Library": "1.6.0"
  },

  "frameworks": {
    "netstandard1.6": {
      "imports": "dnxcore50"
    }
  }
}

ReadableExpressions was on v1.6.5 at the time and I wanted to support .NET Standard 1.0 and .NET 4.0, so I updated it to this:

{
  "version": "1.6.5-*",

  "frameworks": {
    "netstandard1.0": {
      "imports": "dnxcore50",
      "dependencies": {
        "NETStandard.Library": "1.6.0"
      },
      "buildOptions": {
        "define": [ "NET_STANDARD" ]
      }
    },
    "net40": {}

  }
}

You may notice I moved the NETStandard.Library dependency into the dependencies section for the netstandard1.0 target – that's not necessary, I just did it as a kind of OCD thing, because that's the only target that requires that library.

I defined the conditional compilation symbol NET_STANDARD so the code could distinguish between its .NET Standard1.0 and .NET 4.0 versions – more on that later – and manually updated the version number. In a csproj I can link to a shared version number file, but as far as I can tell the way NuGet packages are produced for .NET Core requires the version number to be in the project.json. Shame.

Polyfills!

With these changes made to the project.json, I started getting errors about the DynamicExpression class not being supported:

DynamicExpression

There's a couple of things to note in that screenshot:

  • The highlighted drop-down at the top of the image includes the two targets I specified in the project.json – you can switch between the targets to see how the code looks for each
  • The error in the Error List is for project ReadableExpressions..NETStandard,Version=v1.0, specifying the target where it occured

I removed this code from the .NET Standard version by wrapping the whole class with #if !NET_STANDARD / #endif – where NET_STANDARD is the conditional compilation symbol I defined in the project.json – and compiled again*. Here's where I ran into problems with the changes to the reflection API:

PolyfillErrors

Some of them were resolved by adding a reference to System.Reflection.TypeExtensions:

{
  "version": "1.6.5-*",

  "frameworks": {
    "netstandard1.0": {
      "imports": "dnxcore50",
      "dependencies": {
        "NETStandard.Library": "1.6.0",
        "System.Reflection.TypeExtensions": "4.1.0"
      },
      "buildOptions": {
        "define": [ "NET_STANDARD" ]
      }
    },
    "net40": {}
  }
}

...and this was one example of the tools being in preview really biting – manually editing the dependencies section of the project.json caused VS to freeze for 10-15 seconds while it tried to resolve the package – even though the file hadn't been saved and I haven't finished typing. I ended up typing package references into Notepad and pasting them into project.json or using the NuGet package manager instead.

The rest of the 'you can't do that in .NETStandard1.0' errors I fixed with polyfills, as per a recent Jeremy D Miller blog. For example:

public static bool IsClass(this Type type)
{
#if NET_STANDARD
return type.GetTypeInfo().IsClass; #else return type.IsClass; #endif }

After updating the code to use the polyfills, the project built again. Progress!

Restore the Unit Tests

My unit tests project referenced the old csproj, but that was now gone. They couldn't reference the new xproj, because csproj –> xproj project references don't work, so I added a new ReadableExpression.Net40 csproj project, and replaced the ItemGroup listing the included files with:

<ItemGroup>
  <Compile Include="..\CommonAssemblyInfo.cs">
    <Link>Properties\CommonAssemblyInfo.cs</Link>
  </Compile>
  <Compile Include="..\ReadableExpressions\**\*.cs" Exclude="..\ReadableExpressions\obj\**\*.cs;">
    <Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
  </Compile>
  <Compile Include="..\VersionInfo.cs">
    <Link>Properties\VersionInfo.cs</Link>
  </Compile>
</ItemGroup>

...which included links to the CommonAssemblyInfo and VersionInfo files and all the .cs files from the original project – I  could then reference that from the unit tests project, and my tests were wired back in. They all passed! Magic!

Tidying up + NuGet

NuGet packages are created from xproj / project.json projects using the dotnet pack command. This doesn't use the .nuspec file that NuGet.exe does, so I added the old package information into the project.json. dotnet pack also uses the name of the project's folder for the output assembly name, so I had to specify that in project.json's buildOptions. The final project.json can be seen on GitHub here.

Testing (some of) the Polyfills

So far so not-so-bad, but my .NET 4.0 unit tests only test the else branches in the #if NET_STANDARD forks – not the .NET Standard paths. Most of the polyfills were very simple like IsClass above, but a few were more involved, and I wasn't comfortable leaving them untested. I followed the instructions in this article to add a new .NET Core Class library with a project.json pulling in MSTest – added a test class, and the references came through fine! Great!

Next, I added the ReadableExpressions project reference, giving me a frameworks section which looked like this:

"frameworks": {
  "netcoreapp1.0": {
    "imports": [
      "dnxcore50",
      "portable-net45+win8"
    ],
    "dependencies": {
      "Microsoft.NETCore.App": {
        "version": "1.0.1",
        "type": "platform"
      },
      "dotnet-test-mstest": "1.1.1-preview",
      "MSTest.TestFramework": "1.0.4-preview",
      "System.Data.Common": "4.1.0",
      "System.Reflection.TypeExtensions": "4.1.0",
      "ReadableExpressions": {
"target": "project"
} }, "buildOptions": { "define": [ "NET_STANDARD" ] } } }

And that... didn't work. The package restore completed, but I had the following build errors:

NoPackage

The classes under test are internal, so I added an InternalsVisibleToAttribute to the project... and it built! But... looking like this:

BuildSucceeded

...still saying the classes under test weren't accessible, 'The dependency ReadableExpressions could not be resolved' and 'The given key was not present in the dictionary'. The tests didn't show up in Test Explorer. The project looked like this:

Project

A bit more research led me to global.json, where you can list the folders in which the projects in your solution exist, so other .NET Core projects can reference them. Like this:

{
  "projects": [ "ReadableExpressions" ]
}

That must have been it! But... the error list still looked the same. I unloaded the project and reloaded it, and finally!

ProjectOK

It still claimed (and continues to claim) an error in the references, but the error list now only stated 'The given key was not present in the dictionary' – it built, and nothing had a red underline.

So, final thing... actually running the tests. The Test Explorer didn't find them, ReSharper found them but couldn't run them... so I went to dotnet test. And that worked:

DotNetTest

At last! Not perfect, but... good enough. Maybe all this trial and error will help someone else get to the result more quickly Smile

 

* In the course of writing this blog I discovered I can get DynamicExpression back by referencing the System.Dynamic.Runtime package – I'll make that change at some point

Print | posted @ Friday, October 7, 2016 11:48 AM

Comments on this entry:

Gravatar # re: CSProj to XProj: Supporting .NET Core using the Preview tools
by Stef at 10/12/2016 12:26 PM

For some polyfill helper project, maybe this one https://github.com/StefH/ReflectionBridge can help you ?
Gravatar # re: CSProj to XProj: Supporting .NET Core using the Preview tools
by Steve at 10/12/2016 12:58 PM

Definitely looks like a useful project, but it doesn't target .NET Standard 1.0 (1.3 is the earliest version) and it has an Apache licence where I use MIT. I'm also kind of reluctant to build in a dependency on another package for the sake of the few extension methods I had to write. Will come in very handy for others though, I'm sure! :)
Gravatar # re: CSProj to XProj: Supporting .NET Core using the Preview tools
by Someone at 12/7/2016 11:52 PM

You my friend saved me a lot of head-banging against the wall. Thanks for the easy to follow walkthrough :-)
Gravatar # re: CSProj to XProj: Supporting .NET Core using the Preview tools
by Steve at 12/8/2016 8:46 AM

You're welcome! That's what I'm here for :)
Post A Comment
Title:
Name:
Email:
Comment:
Verification: