Umbraco

Package Migration to V9 Using Multi-Targeting

A step-by-step on our test migration of the UI examples package to V9

Written by: The Package Team

As we approach the release of Umbraco 9 on .NET 5, many package developers are turning their attention to migrating their own packages to the new version.  To see how this works in practice, we've given it a try using a multi-targeting approach.  Here's how it went!.

For all you regular readers of the Umbraco blog, it won’t have escaped your notice that Umbraco version 9 running on .NET 5 is nearing its release.  

It’s not only the HQ developers who have been kept busy with the releases of various alphas, betas and release candidates - as the upgrade will not only be for the core product, but will be needed for the ecosystem of packages that support and build on it.

Many package developers have already taken the plunge in starting development and releasing versions of their V8 packages to NuGet, that can be installed into and run on the current release candidate.

This week was the turn of the package team collective to get involved in migrating one of the packages they support - UI Examples - to a version supporting Umbraco V9.  We did this over a live, hour-long Zoom call where we took the existing package code and updated it as necessary to work on version 9.

Now, I say “we” and “collective”, but as you’ll see, we really have to thank Kevin Jump for doing most of the work.  The rest of our contributions were a few hopefully helpful comments to speed the process, and quite a number of questions to slow him down!  

In this blog post we’ll describe the steps we took and the techniques used, but if you want to see the process yourself we’ve recorded and shared the video - watch here on the blog post or click to watch on the Umbraco Community YouTube channel.

 

UI Examples

Let’s start by explaining what the UI Examples package is: when installed, it provides a new section in the back-office where the various UI components can be viewed along with details of how to use them. 

This is relevant for package or solution developers looking to modify the back-office with new property editors and dashboards, and wanting to re-use core components to ensure a user interface consistent with Umbraco itself.

As far as packages go, it’s a relatively simple one to migrate. There’s quite a bit of angularjs/front-end code, none of which needs to change to work on version 9. Plus, a small amount of C#/back-end code, primarily used for a migration to create the section and provide administrator permissions to it, that does need some modification.

 

Screenshot of UI Examples package on the Umbraco backoffice

Maintenance Considerations

When migrating a package to V9, an important decision to make is how to maintain the code moving forward when it comes to supporting multiple Umbraco versions - in addition to the actual code changes necessary.  Even when V9 is fully released, there will be many Umbraco installations running on V8 for quite some time, so any package already supporting this version will need to be maintained with bug fixes, and, for many, new features too.

There are a few ways to tackle this, broadly trading off some complexity in the initial and ongoing development, versus ease of maintaining parity between versions and releasing.  A non-exhaustive set of options being:

  1. Start a separate V9 project, in a new code repository. This might be attractive if you consider the V8 package broadly “done” and/or want to make significant changes or refactorings to the new version for V9.
  2. Maintain a single repository but have different branches for V8 and V9 versions, using merging to ensure features and bug fixes made in one are applied to the other.  This is the approach HQ have adopted with their commercial packages.
  3. Implement a single branch with multi-targeted code, using compiler directives and project file conditions to ensure a single code-base can be compiled to both .NET Framework (Umbraco 8) and .NET 5 (Umbraco 9).

For UI examples, we decided to go with the latter approach. That’s partly so we could see this in action, and help others see if it’s an appropriate option for them. Additionally, the project does seem to have some characteristics that make it a good candidate for this method: the amount of C# code compared to client-side code is small, and it’s only the former that we need to be concerned about for multi-targeting.  

Even for larger packages though, we’d still consider this a viable option. There is additional complexity at the outset and to an extent in ongoing development, but in practice, it’s mostly the C# code that has dependency on Umbraco APIs that requires the multi-targeting directives in the code. Although these APIs haven’t been altered significantly, there are now different namespaces and constructors to consider, and some names, signatures and access modifiers have changed.

If you have other, non-Umbraco dependent code for business logic or other infrastructure, it’s quite possible these will compile to both .NET versions with little difference, or could even be implemented as .NET Standard class libraries working across both .NET Framework and .NET 5 without need for multi-targeting.

 

Umbraco Package icon

Multi-targeting Approach

As you’ll see in the video, the approach taken to implement the multi-targeted package was to start with a fresh .NET 5 class library which uses the newer SDK style project format. Once created, we opened the .csproj file and changed the default, single target framework…

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

… to target both .NET Framework and .NET 5 (note the pluralisation of the element name):

  <PropertyGroup>
    <TargetFramework>net472;net5.0</TargetFramework>
  </PropertyGroup>

We then need to add the dependencies to Umbraco, making use of a conditional statement in the project file to include the Umbraco 8 NuGet package for .NET Framework, and those necessary for Umbraco 9 when compiling to .NET 5:

  <ItemGroup Condition="'$(TargetFramework)' == 'net472'">
    <PackageReference Include="UmbracoCms.Web" Version="8.10.1" />
  </ItemGroup>
  <ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
    <PackageReference Include="Umbraco.Cms.Web.Website"
                      Version="9.0.0-rc001" />
    <PackageReference Include="Umbraco.Cms.Web.BackOffice"
                      Version="9.0.0-rc001" />
 </ItemGroup>

Expanding the Dependencies list (found under the project in the solution explorer of Visual Studio) will show the assemblies, frameworks and packages referenced for each target:

Then, for each file, we can make use of compiler directives to conditionally include or exclude code from being referenced when the package is compiled for each target.

Here’s an example for a migration plan, where the only difference between Umbraco 8 and 9 is the namespace for the MigrationPlan class, and so we conditionally include the correct one:

 

#if NETCOREAPP
using Umbraco.Cms.Infrastructure.Migrations;
#else
using Umbraco.Core.Migrations;
#endif
namespace Our.Umbraco.UiExamples.Migrations
{
    public class UiExamplesMigrationPlan : MigrationPlan
    {
        public UiExamplesMigrationPlan() : base("UiExamples")
        {
            From(string.Empty)
              .To<AddSectionToAdminsMigration>
                  ("AddedSectionForAdmins-Ran");
        }
    }
}

Visual Studio has a handy toggle in the top left of the code editor, allowing you to switch between your targets, and fix up the compilation errors as necessary:

With this package being quite small, we didn’t end up with too many compiler directives to get the code building across the two target frameworks, but that may not be the case for others.  

There are a few strategies you can adopt though to reduce the number and keep the code a bit cleaner: 

  1. Include or exclude whole files, either via a compiler directive spanning the whole file, or via project file excludes. This may be easier to work with than a single file with a lot of if/else compiler directives. You’ll see that we adopted this for the more significant V8 and V9 differences around start-up operations.

  2. If you are finding you need to do the same conditional include across a number of files, consider if you can make use of inheritance, composition or helper methods - standard refactoring methods to DRY up code and avoid repetitions.

  3. And lastly you can consider introducing your own abstractions that you depend on, that have an implementation that handles the changes between Umbraco 8 and 9 in one place. Matt Brailsford has blogged an approach for doing this for logging.

When the project builds, you’ll find output that assemblies for both target frameworks are created in the bin folder.

Umbraco 9 packages are installable only via NuGet, so the output of our work here needs to be not just the assemblies, but a NuGet package containing the necessary dlls and client-side code, setup correctly for adding to an Umbraco project.

Whilst nuspec files can be created for the metadata for a NuGet package, the simpler approach which sufficed for our needs here is to use the .csproj file to define the package, and so the various information around author, copyright, package name etc. can be added here:

  <PropertyGroup>
    <PackageId>Our.Umbraco.UIExamples</PackageId>
    <Version>2.0-beta001</Version>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageProjectUrl>
      https://our.umbraco.com/packages/developer-tools/ui-examples/ 
    </PackageProjectUrl>
    <PackageTags>Umbraco</PackageTags>
    <RepositoryUrl>
        https://github.com/umbraco/UI-Examples
    </RepositoryUrl>
    <Description>A collection of backoffice elements...</Description>
    ... 
  </PropertyGroup>

We also need to add to the project file two further elements - necessary to include and install the client-side files that will go into the App_Plugins folder.

  <ItemGroup>
    <Content Include="App_Plugins\**\*.*">
      <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
      <CopyToPublishDirectory>Always</CopyToPublishDirectory>
    </Content>
    <None Include="build\**\*.*">
      <Pack>True</Pack>
      <PackagePath>buildTransitive</PackagePath>
    </None>
  </ItemGroup>

And lastly, in a “build” folder that we’ve referenced above, a targets file needs to be added with a name that matches the NuGet PackageId - so in our case \build\Our.Umbraco.UiExamples.targets - with the following contents:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <UIExamplesContentFilesPath>
        $(MSBuildThisFileDirectory)..\content\App_Plugins\uiexamples\**\*.*
    </UIExamplesContentFilesPath>
  </PropertyGroup>
  <Target Name="CopyUIExamplesAssets" BeforeTargets="Build">
    <ItemGroup>
      <UIExamplesContentFiles Include="$(UIExamplesContentFilesPath)" />
    </ItemGroup>
    <Message
        Text="Copying UIExamples files: $(UIExamplesContentFilesPath) - #@(UIExamplesContentFiles->Count()) files"
        Importance="high" />
    <Copy SourceFiles="@(UIExamplesContentFiles)"
            DestinationFiles="@(UIExamplesContentFiles->'$(MSBuildProjectDirectory)\App_Plugins\UIExamples\%(RecursiveDir)%(Filename)%(Extension)')"
            SkipUnchangedFiles="true" />

  </Target>

  <Target Name="ClearUIExamplesAssets" BeforeTargets="Clean">
    <ItemGroup>
      <UIExamplesDir
          Include="$(MSBuildProjectDirectory)\App_Plugins\uiexamples\" />
    </ItemGroup>
    <Message Text="Clear old UIExamples data"  Importance="high" />
    <RemoveDir Directories="@(UIExamplesDir)"  />
  </Target>

</Project>


This file, along with the reference in the project file, will ensure that when the NuGet package is installed, the client-side files will be added to the correct folder location.

Watch Along

Whilst this blog post hopefully covers the main points from the video, we haven’t included everything - so you may still be interested to watch along to see how Kevin went about the process, and hear some of the discussion we had along the way.

 

 

He did a great job live coding with an audience, but unfortunately if you get to the end you’ll see we didn’t get everything working by the end of the hour. Almost, but not quite, so we had this little addendum on Slack after he returned with fresh eyes after lunch…

 

 

Don't forget to sign up to the Package Newsletter, to get important info, updates and tips straight to your inbox!