netstandard

Create and Pack Reference Assemblies

July 3, 2018 Coding 3 comments , , , ,

Update July 9: Read the follow-up post for an easier way to implement.

Create and Pack Reference Assemblies

Reference Assemblies, what are they, why do I need that? Reference Assemblies are a special kind of assembly that’s passed to the compiler as a reference. They do not contain any implementation and are not valid for normal assembly loading (you’ll get an exception if you try outside of a reflection-only load context).

Why do you need a reference assembly?

There’s two main reasons you’d use a reference assembly:

  1. Bait and switch assemblies. If your assembly can only have platform-specific implementations (think of a GPS implementation library), and you want portable code to reference it, you can define your common surface area in a reference assembly and provide implementations for each platform you support.

  2. Selectively altering the public surface area due to moving types between assemblies. I recently hit this with System.Interactive (Ix). Ix provides extension methods under the System.Linq namespace. Two of those methods, TakeLast, and SkipLast were added to .NET Core 2.0’s Enumerable type. This meant that if you referenced Ix in a .NET Core 2.0 project, you could not use either of those as an extension method. If you tried, you’d get an error:

    error CS0121: The call is ambiguous between the following methods or properties: 'System.Linq.EnumerableEx.SkipLast<TSource>(System.Collections.Generic.IEnumerable<TSource>, int)' and 'System.Linq.Enumerable.SkipLast<TSource>(Sy stem.Collections.Generic.IEnumerable<TSource>, int)'.
    

    The only way out of this is to explicitly call the method like EnumerableEx.SkipLast(...). Not a great experience. However, we cannot simply remove those overloads from the .NET Core version since:

    • It’s not in .NET Standard or .NET Framework
    • If you use TakeLast from a .NET Standard library, then are running on .NET Core, you’d get a MissingMethodException.

The method needs to be in the runtime version, but we need to hide it from the compiler. Fortunately, we can do this with a reference assembly. We can exclude the duplicate methods from the reference on platforms where it’s built-in, so those get resolved to the built-in Enumerable type, and for other platforms, they get the implementation from EnumerableEx.

Creating reference assemblies

I’m going to explore how I solved this for Ix, but the same concepts apply for the first scenario. I’m assuming you have a multi-targeted project containing your code. For Ix, it’s here.

It’s easiest to think of a reference assembly as a different project, with the same name, as your main project. I put mine in a refs directory, which enables some conventions that I’ll come back to shortly.

The key to these projects is that the directory/project name match, so it creates the same assembly identity. If you’re doing any custom versioning, be sure it applies to these as well.

There’s a couple things to note:

  • In the project file itself, we’ll include all of the original files
    <ItemGroup>
      <Compile Include="..\..\System.Interactive\**\*.cs" Exclude="..\..\System.Interactive\obj\**" />
    </ItemGroup>
    
  • The TargetFrameworks should be for what you want as reference assemblies. These do not have to match that you have an implementation for. For scenario #1 above, you’ll likely only have a single netstandard2.0 target. For scenario #2, Ix, given that the surface area has to be reduced on specific platforms, it has more.
  • There is a Directory.Build.props file that provides common properties and an extra set of targets these reference assembly projects need. (Ignore the bit with NETStandardMaximumVersion, that’s me cheating a bit for the future 😉)

    In that props, it defines REF_ASSM as an extra symbol, and sets ProduceReferenceAssembly to true so the compiler generates a reference assembly.

    The other key thing in there is a target we’ll need to gather the reference assemblies from the main project during packing.

    <Target Name="_GetReferenceAssemblies" DependsOnTargets="Build" Returns="@(ReferenceAssembliesOutput)">
      <ItemGroup>
        <ReferenceAssembliesOutput Include="@(IntermediateRefAssembly->'%(FullPath)')" TargetFramework="$(TargetFramework)" />
        <ReferenceAssembliesOutput Include="@(DocumentationProjectOutputGroupOutput->'%(FullPath)')" TargetFramework="$(TargetFramework)" />
      </ItemGroup>
    </Target>
    

    With these, you can use something like #ifdef !(REF_ASSM && NETCOREAPP2.0) in your code to exclude certain methods from the reference assembly on specific platforms. Or, for the “bait and switch” scenario, you may choose to throw an NotImplementedException in some methods (don’t worry, the reference assembly strips out all implementation, but it still has to compile).

You should be able to build these reference assemblies, and in the output directory, you’ll see a ref subdirectory (in \bin\$(Configuration)\$(TargetFramework)\ref). If you open the assembly in a decompiler, you should see an assembly level: attribute [assembly: ReferenceAssembly]. If you inspect the methods, you’ll notice they’re all empty.

Packing the reference assembly

In order to use the reference assembly, and NuGet/MBuild do its magic, it must be packaged correctly. This means the reference assembly has to go into the ref/TFM directory. The library continues to go into lib/TFM, as usual. The goal is to create a package with a structure similar to this:

NuGet folder structure

The contents of the ref folder may not exactly match the lib, and that’s okay. NuGet evaluates each independently for the intended purpose. For finding the assembly to pass as a reference to the compiler, it looks for the “best” target in ref. For runtime, it only looks in lib. That means it’s possible you’ll get a restore error if you try to use the package in an application without a supporting lib.

Out-of-the-box, dotnet pack gives us the lib portion. Adding a Directory.Build.targets above your main libraries gives us a place to inject some code into the NuGet pack pipeline:

<Target Name="GetRefsForPackage" BeforeTargets="_GetPackageFiles" 
        Condition=" Exists('$(MSBuildThisFileDirectory)refs\$(MSBuildProjectName)\$(MSBuildProjectName).csproj') ">

  <MSBuild Projects="$(MSBuildThisFileDirectory)refs\$(MSBuildProjectName)\$(MSBuildProjectName).csproj" 
           Targets="_GetTargetFrameworksOutput">

    <Output TaskParameter="TargetOutputs" 
            ItemName="_RefTargetFrameworks" />
  </MSBuild>

  <MSBuild Projects="$(MSBuildThisFileDirectory)refs\$(MSBuildProjectName)\$(MSBuildProjectName).csproj" 
           Targets="_GetReferenceAssemblies" 
           Properties="TargetFramework=%(_RefTargetFrameworks.Identity)">

    <Output TaskParameter="TargetOutputs" 
            ItemName="_refAssms" />
  </MSBuild>

  <ItemGroup>
    <None Include="@(_refAssms)" PackagePath="ref/%(_refAssms.TargetFramework)" Pack="true" />
  </ItemGroup>
</Target>

This target gets called during the NuGet pack pipeline and calls into the reference assembly project using a convention: $(MSBuildThisFileDirectory)refs\$(MSBuildProjectName)\$(MSBuildProjectName).csproj. It looks for a matching project in a refs directory. If it finds it, it obtains the TargetFrameworks it has and then gets the reference assembly for each one. It calls the _GetReferenceAssemblies that we had in the Directory.Build.props in the refs directory (thus applying it to all reference assembly projects).

Building

This will all build and pack normally using dotnet pack, with one caveat. Because there’s no ProjectReference between the main project and the reference assembly projects, we need to build the reference assembly projects first. You can do that with dotnet build. Then, call dotnet pack on your regular project and it’ll put it all together.

Announcing Reactive Extensions for .NET 4.0 Preview 1

May 27, 2017 Coding 1 comment , , ,

Announcing Reactive Extensions for .NET 4.0 Preview 1!

I am happy to announce that the first preview of Rx.NET 4.0 is now available. This release addresses a number of issues and contains several enhancements.

The biggest enhancement is consolidating the existing packages into one main package, System.Reactive NuGet. This will prevent most of the pain around binding redirects that were caused by #205. If you are using Rx 4.0 and need to use libraries built against Rx 3.x, then you need to also install the compatibility package System.Reactive.Compatibility. That package contains facades with type forwarders to the new assembly so types are unified correctly. You only need this compatibility package if you are consuming a library built against 3.x. You do not need it otherwise.

If you’re interested in the background behind the version numbers, I suggest reading the thread as it contains the gory details. While the idea was technically sound, it did mean that binding redirects were required for all .NET Framework uses. We heard the feedback loud and clear that this was really painful and took steps to fix it in 4.0.

The fix was to consolidate the previous set of packages into a single System.Reactive package. With the single package, binding redirects are no longer required and the platforms will get the correct Rx package version.

Please try it out and let us know if you encounter any issues at our repo. The full release notes are there too.

Using Xamarin Forms with .NET Standard – VS 2017 Edition

April 23, 2017 Coding 39 comments , , ,

Using Xamarin Forms with .NET Standard – VS 2017 Edition

I have previously blogged about using .NET Standard with Xamarin Forms. Since then, the tooling has changed significantly with Visual Studio 2017 and Visual Studio for Mac. This post will show you what you need to use Xamarin.Forms with a .NET Standard class library.

Why use a .NET Standard class library instead of a PCL? There are many good reasons, but the two biggest ones are:

  • Much bigger surface area. PCL’s were the least common denominator intersection of supported platforms. The end result is that while the binary worked on many platforms, there were a much more limited set of APIs available. .NET Standard 1.4 is the version that supports UWP, Xamarin Android, Xamarin iOS, and Xamarin.Mac.
  • “SDK Style” project file goodness. Legacy PCL’s use the old csproj format which have tons of gunk in them. While it is possible to use the new project style to generate legacy PCLs (if you use my MSBuild.Sdk.Extras package), it’s time to move past those. If you target .NET Standard 1.0-1.2, some PCL profiles can install your library. See the full table for the list.

Prerequisites

Using .NET Standard requires you to use PackageReference to eliminate the pain of “lots of packages” as well as properly handle transitive dependencies. While you may be able to use .NET Standard without PackageReference, I wouldn’t recommend it.

You’ll need to use one of the following tools:

Getting Started

As of now, the project templates for creating a new Xamarin Forms project start with an older-style packages.config template, so whether you create a new project or have an existing project, the steps will be pretty much the same.

Step 1: Convert your projects to use PackageReference. The NuGet blog has details on using PackageReference with all project types. Unfortunately there’s no current migration tool, so it’s probably easiest to uninstall your existing packages, make sure the packages.config file is gone and then install the package after setting the VS Options to PackageReference. You can also do it by hand (which is what I did for my projects).

Step 2: As part of this, you can remove dependencies from your “head” projects that are referenced by your other projects you reference. This should simplify things dramatically for most projects. In the future, when you want to update to the next Xamarin Forms version, you can update it in one place, not 3-4 places. It also means, you only need the main Xamarin.Forms package, not each of the packages it pulls in.

For now, you’ll need to add the <RestoreProjectStyle>PackageReference</RestoreProjectStyle> property near the top of your iOS and Android csproj files. That tells NuGet restore to use the PackageReference mode even if you don’t have any direct packages (this is important for transitive restore). If you have any PackageReference elements in your iOS or Android csproj, then you don’t need this. For UWP, you already should have a PackageReference to the UWP meta-package (Microsoft.NETCore.UniversalWindowsPlatform version 5.3.2).

If you hit any issues with binaries not showing up in your bin directories (for your Android and iOS “head” projects), make sure that you have set CopyNuGetImplementations to true in your csproj.

At this point, your project should be compiling and working, but not yet using netstandard1.x anywhere.

Step 3: Move your PCL library to .NET Standard. This is the hard part today as there’s no tooling to automatically do this correctly. Be warned and DO NOT use this option PCL Dialog in the PCL properties. It is broken and will create a project.json based library targeting dotnet. I hope this option is removed in a future VS Update! Instead, go to File -> New Project -> .NET Standard -> Class Library and create a new class library. If this is a new project, I’d simply delete the existing PCL and just use a new one. If it’s an existing project, you’ll want to migrate. The new format is far simpler and moving the PCL by hand is usually pretty easy. What I’ve usually done is this:

  1. Close the solution in VS
  2. Take the existing csproj and make a copy of it somewhere else. I’ll keep this other copy open in Notepad.
  3. Copy/paste the contents of the new project you created and replace the contents of your existing project. Most of what you had in the old project isn’t really needed anymore. What you’ll likely need are settings like any signing or assembly names that don’t match the folder name/conventions. If you have ResX files with design-time generated code, you’ll need to add the following. Likewise, for Xamarin Forms pages, you’ll need this.
  4. Decide which .NET Standard version to target, probably 1.4, based on the table. Here’s a cheat sheet:
    • If you only want to support iOS and Android, you can use .NET Standard 1.6. In practicality though, most features are currently available at .NET Standard 1.3 and up.
    • If you want to support iOS, Android and UWP, then NET Standard 1.4 is the highest you can use.
    • If you want to support Windows Phone App 8.1 and Windows 8.1, then NET Standard 1.2 is your target.
    • If you’re still supporting Windows 8, .NET Standard 1.1 is for you.
    • Finally, if you need to support Windows Phone 8 Silverlight, then .NET Standard 1.0 is your only option.

Once you determine the netstandard version you want, in your csproj, set the TargetFramework to it — netstandard1.4, etc.

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

  <PropertyGroup>
    <TargetFramework>netstandard1.4</TargetFramework>
    <PackageTargetFallback>portable-net45+win8+wpa81+wp8</PackageTargetFallback>
    <DebugType>full</DebugType>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Xamarin.Forms" Version="2.3.4.231" />
  </ItemGroup>

  <ItemGroup>
    <!-- https://bugzilla.xamarin.com/show_bug.cgi?id=55591 -->
    <None Remove="**\*.xaml" />

    <Compile Update="**\*.xaml.cs" DependentUpon="%(Filename)" />
    <EmbeddedResource Include="**\*.xaml" SubType="Designer" Generator="MSBuild:UpdateDesignTimeXaml" />
  </ItemGroup>

</Project>

Note the addition of the PackageTargetFallback property. This is required to tell NuGet that specified TFM is compatible here because the Xamarin.Forms package has not yet been updated to use netstandard directly. Also note that DebugType set to full is required for the Xamarin tool-chain currently as they don’t yet support the new portable PDBs that are created by default.

At this point, when you reload the project, it should restore the packages and build correctly. You may need to do a full clean/rebuild.

Seeing it in action

I created a sample solution showing this all working over on GitHub. It’s a good idea to clone, build and run it to ensure your environment and tooling is up-to-date. If you get stuck converting your own projects, I’d recommend referring back to that repo to find the difference.

Building on command line

You will need to use MSBuild.exe to build this, either on Windows with a VS 2017 command prompt or a Mac with Visual Studio for Mac. You cannot use dotnet build for these projects types. dotnet build only supports .NET Standard, .NET Core and .NET Framework project types. It is not able to build the Xamarin projects and the custom tasks in Xamarin Forms have not yet been updated to support .NET Core.

To build, you’ll need two steps:

  1. msbuild /t:restore MySolution.sln
  2. msbuild /t:build /p:Configuration=Release MySolution.sln

You can also restore/build the .csproj files individually if you’d prefer.

As always, feel free to tweet me @onovotny as well.