The story of cross platform development for Microsoft developers has been one marked by iteration after iteration of .NET framework fragmentation. With .NET 5 the promise is that we’ll return to having a single framework that will be used everywhere…. Sorry, what?? when did we ever have a single framework? For as long as I can recall, developers working on mobile and/or IoT have always had a different framework to target (.NET CF, .NET MF, WinPhone (whatever that .NET version was called), UWP etc). However, one thing that hasn’t changed is the fact that in order to target different platforms, you inevitably have to write code that’s specific to that platform. Even during the announcement of MAUI, the new single project structure has specific files/code etc for iOS, Android, Windows etc. This problem isn’t going away, so let’s discuss how we deal with this issue today.
The scenario I’m going to cover in the post is relatively simple. I have three “head” or “target platform” projects (Windows, Android, iOS) that are going to reference a common library (Core). Whilst most of the Core library is common code that is compatible with .NET Standard 2.0, there’s some code that is specific to each platform. Rather than create a separate platform specific library, we can include this specific logic by setting up the Core library to be multi-targeted – meaning that a different DLL is generated for each supported platform. Luckily Visual Studio knows how to handle this, so you’re not left working out how to reference each DLL from the head projects.
Project Setup
Let’s get started. First up, we’ll create a blank solution. If you start with one of the head projects your solution will end up being called something list XXX.Windows.
Next up, let’s create the three head projects: Windows (UWP), Android and iOS
At this point, if you’re working on a Mac, you could also add a MacOS project template. For some reason this template isn’t available in Visual Studio for Windows (because no developer would ever want to create the MacOS project when working on windows …. I guess the same (lack of) logic is used on VS for Mac where you can’t create a UWP project).
Next up, we’ll create the common library that each of the head projects will reference.
Common Code
So that we can see how this library will work, we’ll simply expose a string constant that can be referenced and displayed in each head project. We’ll rename the Class1.cs file to AppProperties.cs and allow Visual Studio to rename the corresponding class. We’ll then add a string constant as follows:
namespace MultiTarget.Core
{
public class AppProperties
{
public const string AppName = "MultiTarget Sample";
}
}
In each of the head projects we’ll add an text/label element to the UI and set the value to be AppProperties.AppName. Rather than having to use the Add Reference workflow (i.e. right click on the head project and click Add Reference), we’ll simply type the AppProperties class name and use the prompt in the code editor to Add reference to the MultiTarget.Core project.
Now we can run each of the head projects and see the same output on the screen which should say “MultiTarget Sample” (I don’t have an iOS device/simulator handy, so I don’t have a screenshot of it running on iOS).
Platform Specific Code
The next step is to add platform specific code. In this case we’re going to augment the existing AppProperties class by adding a platform specific partial class. In Solution Explorer we can copy the AppProperties.cs and paste it into the same project. The copied file can then be renamed based on the platform. For example (see image below) the Windows specific code would be in a file called AppProperties.windows.cs.
Note: The actual name of the platform specific files doesn’t have any intrinsic meaning. As we’ll see in a minute, we will use the platform specific suffix to define which files will be included for each platform.
Inside the platform specific file we’ll add another constant called PlatformName. For example, the AppProperties.windows.cs includes a PlatformName constant that returns “Windows.”
using System;
namespace MultiTarget.Core
{
public partial class AppProperties
{
public const string PlatformName = "Windows";
}
}
We’re also going to update the AppName constant in the AppProperties.cs file to include the PlatformName. We also needed to add the partial keyword to the class definition.
namespace MultiTarget.Core
{
public partial class AppProperties
{
public const string AppName = "MultiTarget Sample - " + PlatformName;
}
}
After adding files for each platform, if you attempt to build the Core project it will fail with an error indicating that there are multiple definitions of the PlatformName constant. What we need to do is to only include one platform specific file at a time.
MultiTarget Project
Currently the Core project is configured with a single TargetFramework, netstandard2.0. This means that the only API that can be accessed are those defined by netstandard2.0. If we want to include platform specific code, or want to access platform specific APIs, we need to change the project to target multiple frameworks. Here’s the current project XML for the Core library
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>
In order to target multiple frameworks, we can just change the TargetFramework element to TargetFrameworks, and add multiple frameworks, semicolon delimited (eg <TargetFrameworks>netstandard2.0;net451</TargetFrameworks> ). However, out of the box, this doesn’t support the various target framework monikers that are used across different platforms. To overcome this limitation, we need to change the Sdk attribute from “Microsoft.NET.Sdk” to “MSBuild.Sdk.Extras”. We also need to create a file called global.json in the solution folder with the following version information for the MSBuild.Sdk.Extra nuget package:
{
"msbuild-sdks": {
"MSBuild.Sdk.Extras": "2.0.54"
}
}
Note: Alternatively you can specify the version number in the Sdk attribute (eg Sdk=”MSBuild.Sdk.Extras/2.0.54″ ).
With these changes made, we can update the project file to include multiple target frameworks:
<Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;xamarinios10;monoandroid10.0;</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">uap10.0.16299;$(TargetFrameworks)</TargetFrameworks>
</PropertyGroup>
</Project>
Note: we’ve included a conditional TargetFrameworks element so that the Windows (i.e. UWP) target platform is only include if the host OS is Windows. This is important because the UWP platform can’t be built using Visual Studio for Mac, so making this target framework conditional on the OS ensures the project can be loaded and built using Visual Studio for Mac.
At this point it’s worth noting that by including additional target frameworks, you’re effectively increasing the number of steps in the build. For example, when we build the Core library, we see that four DLLs are built – effectively increasing the built time by four times!!!
The new project format includes all files nested in the project folder by default. This means that we need to change the default behaviour for the platform specific files so that they’re excluded by default. Then we can specifically include files for each platform that is built. The project file now looks like this:
<Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;xamarinios10;monoandroid10.0;</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT' ">uap10.0.16299;$(TargetFrameworks)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Remove="**\*.netstandard.cs" />
<Compile Remove="**\*.droid.cs" />
<Compile Remove="**\*.ios.cs" />
<Compile Remove="**\*.windows.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) ">
<Compile Include="**\*.netstandard.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('xamarinios')) ">
<Compile Include="**\*.ios.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('monoandroid')) ">
<Compile Include="**\*.droid.cs" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('uap')) ">
<Compile Include="**\*.windows.cs" />
</ItemGroup>
</Project>
And that’s it – building the Core library will result in four DLLs, each one having a different value for PlatformName, and thus AppName. We didn’t need to make any changes to the head projects.
This technique for including platform specific code in a common library is used by popular frameworks such as ReactiveUI and MvvmCross, as well as Microsoft libraries such as Xamarin Essentials.
Hi Nick,
Thanks for this very nice post!
I have just a problem when I use the lib via a nuget package.
I have generated the package using the checkbox in Visual Studio (Package -> Generate the NuGet package…)
But when i use this package, the PlatformName property has always the “.NET Standard” value.
Thanks for help
Make sure you add the library to the head project (ie the iOS, Android, Windows projects). If you just reference the library in a net standard library you may only get the netstandard version of the library (I actually thought this was fixed but may still be an issue).