Creating a cross platform package – Part 2

Featured

Introduction

In the previous post, I covered the steps required to migrate your project to the new format. In this post, I am going to move to the next stage and cover how you adapt the solution to target multiple frameworks.

Project Changes

The first step is to modify the project and specify which frameworks you want to target. This is a simple change, just modify the <TargetFramework> element to be <TargetFrameworks> and then specify the frameworks you wish to target.

<TargetFrameworks>net471;net5.0;net6.0</TargetFrameworks>

After the project as been modified you will also need to to update the package references ensuring they target the correct framework. This is straightforward, simply add a condition to the parent <ItemGroup>.

<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
    <PackageReference Include="EPiServer.CMS.UI.Core" Version="[12.0.3,13)" />
    <PackageReference Include="EPiServer.Framework.AspNetCore" Version="[12.0.3,13)" /> 
    <PackageReference Include="EPiServer.Framework" Version="[12.0.3,13)" />
	
    <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0" />
</ItemGroup>

There may also be orther sections of your project file that will also require you to use these condition clauses.

Code Changes

After changing the project to target multiple frameworks you will get compilation errors, you will need to fix these by creating different implementations of you code and wrapping each implementation with a preprocessor statement to indicate which framework the code is targeting.

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives

#if NET461
 // Code specific for .net framework 4.6.1
#elif NET5_0
 // Code specific for .net 5.0
#elif NET5_0_OR_GREATER
 // Code specific for .net 5.0 (or greater)
#else
 // Code for anything else
#endif

You may just need to alter a couple of lines within a class, or in some cases you will need to deliver a completely different approach. A good example would be Middleware replacing a .Net Framework HTTPModule.

Wrapping it all up

Everyones journey whist converting their module will differ. The type of module, whether it has a UI etc will detemine the complexity.

Whilst you are modifying the code base I would strongly recommend :

  1. Keep the the ‘DRY’ principle and refactor you code when necessary so that you are not repeating sections of code.
  2. If you have an interface that uses WebForms then it is probably better to replace this with an interface that works for all the different frameworks rather than trying to maintain two different interfaces.

I hope this post helps you migrate your project.

Creating a cross platform package – Part 1

With Optimizely’s transition to .NET5 last year, developers of add-on packages will need to follow suit.

The complexity of delivering a package that supports both frameworks will vary depending on the type of package you are trying to migrate. For example, if you have delivered an admin module based on webforms you will need to re-write this so that it is accessed via the main navigation. In this case, it is probably best to use this in the .Net Framework version as well. In short, you will need to really consider how you refactor your module to support both environments.

This multi-part blog post will take you through the process, with part 1 focusing on converting your existing project to use the new project format and part 2 focusing on how to modify the code and project to support multiple targets.

Migrate to the new project format

The easiest way to do this is probably to create a new project and then bring your code across.

Set the correct framework version

<TargetFramework>net5.0</TargetFramework>

When the project is created it will target either net5.0 or net6.0. This needs to be changed to match the framework version of the original project, i.e. net471.

<TargetFramework>net471</TargetFramework>

Move nuget package references

The nuget package references are no longer managed in the ‘packages.config’ file, they are now part of the project file.

It is straightforward to migrate the references across; ‘packages‘ becomes ‘ItemGroup‘ and ‘package‘ becomes ‘PackageReference

<ItemGroup>
    <PackageReference Include="EPiServer.Framework" Version="[11.1.0,12)" />
    <PackageReference Include="EPiServer.Framework.AspNet" Version="[11.1.0,12)" />
    <PackageReference Include="EPiServer.CMS.UI.Core" Version="[11.1.0,12)" />
</ItemGroup>

Remove nuspec file

Again this information is included in the project file. For the most part, transitioning to the new format is relatively straightforward with similar approaches, but some areas (such as adding files created during the build) have to be done differently.

Metadata

This is the information about the nuget package.

<?xml version="1.0" encoding="utf-8"?> 
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">    				
   <metadata> 
        <!-- Required elements--> 
	<id></id> 
	<version></version> 
	<description></description> 
	<authors></authors> 
	<!-- Optional elements --> 
	<!-- ... --> 
    </metadata> 
    <!-- Optional 'files' node --> 
</package>

changes to

<Project Sdk="Microsoft.NET.Sdk.Razor">
  <PropertyGroup>
    <!-- Required elements--> 
    <PackageId></PackageId>
    <Version></Version>
    <Authors></Authors>
    <Description></Description>
    <!-- Optional elements -->
    <RepositoryUrl></RepositoryUrl>
    <Title></Title>
    <Tags></Tags>
    <ReleaseNotes></ReleaseNotes>
  </PropertyGroup>
</Project>

Content Files

You may need to include additional, or remove files from the nuget package. This was handled in the nuspec file with <files> and <contentFiles> nodes.

<files>
    <file src="bin\Debug\*.dll" target="lib" exclude="*.txt" />
</files>

<contentFiles>
     <!-- Include everything in the scripts folder except exe files -->
     <files include="cs/net45/scripts/*" exclude="**/*.exe"  
            buildAction="None" copyToOutput="true" />
</contentFiles>

changes to

<ItemGroup>
  <Content Remove="src\**" />
  <Content Remove="node_modules\**" />
  <Content Remove="*.json" /> 

  <Content Include="deploy\**" Exclude="src\**\*">
    <Pack>true</Pack>
    <PackagePath>content</PackagePath>
    <PackageCopyToOutput>true</PackageCopyToOutput>
  </Content>
</ItemGroup>

NOTE: If you want to include files that are created during the build process then you need to take a different approach. i.e. a separate front-end build process whose output needs to be included.

You will need to create a target file, and reference this in your project. The targets file should be named the same as the built project.

<Content Include="build\net461\<project-name>.targets" PackagePath="build\net461\<project-name>.targets" />

<project-name>.target

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
    <ItemGroup>
        <SourceScripts Include="$(MSBuildThisFileDirectory)..\..\contentFiles\any\any\modules\_protected\**\*"/>
    </ItemGroup>

    <Target Name="CopyFiles" BeforeTargets="Build">
        <Copy
            SourceFiles="@(SourceScripts)"
            DestinationFolder="$(MSBuildProjectDirectory)\modules\_protected\%(RecursiveDir)"
        />
    </Target>
</Project>

Addtional project settings

<GeneratePackageOnBuild>true</GeneratePackageOnBuild>

When set to true will automatically create the nuget file when the project is built.

<AddRazorSupportForMvc>true</AddRazorSupportForMvc>

Is required when the project includes Razer files.

<RestoreSources>
  https://api.nuget.org/v3/index.json;
  https://nuget.optimizely.com/feed/packages.svc;
</RestoreSources>

Can be used to set the location of the package sources; these can be either external or from the local file system.

Build and Test

Build the project and resolve any issues you encounter, these should be minor.

Once the package is generated you should test to ensure that it contains the correct content.

Wrapping things up

At this point, you should have a solution that builds your project and creates a nuget package but still targets a single framework.

In the next part, I will cover how to convert to target multiple frameworks.