.Net Core Migration: it’s all about the dependenciesPosted on by Stephen Hailey
Skyscanner is powered by a constantly evolving collection of systems which have been implemented at different times, by different teams and using different technologies. As part of this evolution I have been exploring options for porting some of our .Net services from Windows-based servers to Linux-based Docker containers by using .Net Core. Along the way I have contributed back to the open source Compare .Net Objects project to add .Net Core support and discuss the details of what was involved below.
Using Docker fits well with the ‘services ≠ servers’ mantra. It allows services to be stacked up, increase overall server utilisation and avoid waste in a cloud computing model where you pay for resources that you provision but then don’t use. The new, cross-platform .Net Core runtime offers us an option to migrate some of our existing services to run on Linux using Docker without re-implementing core domain logic. Due to a significant overhaul of the .Net runtime and changes to underlying framework APIs this is not a simple lift-and-shift exercise. However, we have made good progress so far and I would like to share our experiences to help others who may be thinking in the same direction.
The Hard Bit
The biggest challenge in migrating existing code is in obtaining .Net Core-compatible versions of your third-party dependencies. We make use of a number of open-source libraries and found a spectrum of support offered for .Net Core. For example, NUnit already had a .Net Core-compatible version released. We used an open log4net pull request to build our own .Net Core-compatible version of that library to use while waiting for an official version. And at the other end of the spectrum RhinoMocks has had no movement for a long time and a clear alternative exists so, we decided to pivot towards Moq by using a Microsoft fork of that project. Compare .Net Objects did not have support yet but is still active and would welcome a pull request so, we viewed this as a good opportunity to use what we’d learned from exploring the other libraries and to contribute something back to the open source community. The remainder of this post covers the details of what was involved.
Compare .Net Objects is an open-source library which we use to unit test our .Net services. It can perform a deep compare of any two .Net objects using reflection and list the differences. There were four major steps involved in bringing .Net Core support which I will now describe in turn.
Task 1. We need a ‘Modern’ Portable Class Library
Existing libraries built for the .Net Framework generally need to be recompiled to run on .Net Core because it only includes a subset of the .Net Framework. Portable Class Libraries (PCLs) have been around for a while and Compare .Net Objects already offers one to support Windows Phone, Silverlight and some other platforms. These PCLs explicitly list the platforms that they support and require re-release to declare support when a new platform comes along – even if they don’t require any code changes to support them. We could have produced a library targeting .Net Core specifically but ‘Modern’ PCLs aim to remove this friction by allowing a .Net Platform Standard to be targeted. This is not actually a platform but rather a standard that platforms are implemented to. By targeting .Net Platform 5.4 in a new PCL, we support .Net Framework 4.6.x, .Net Core 1.0 and anything else which comes along and implements the same standard.
To produce the Modern PCL we followed the pattern established by the existing, old-style Compare .Net Objects PCL. The Compare .Net Objects solution has a main project targeting .Net Framework 3.5 and a Portable project which, instead of having its own duplicated copies, references the code files which live under the main project. If a class cannot be supported in the Portable version of the library (e.g. FontComparer.cs) then it is not linked. If part of a class cannot be supported by the Portable version then compiler directives are used to exclude particular methods (e.g. the IsFont method in TypeHelper.cs).
Example compiler directive to exclude method if building a Portable library
Our Modern PCL is a ‘Class Library (Package)’ project type which uses the new xproj file format. Visual Studio does not currently support the ‘Add as Link’ option for this project type but you can achieve the same functionality by listing the files under the “compile” section of your project.json file.
Our Class Library (Package) targets .Net Framework 4.6.x and .Net Platform Standard
Task 2. We need to exclude some API features when building the Modern PCL
For older Visual Studio projects, the project properties page offers an option to define conditional compilation symbols for use in our compiler directives. However, there is no such option in the property pages for a newer xproj project. An alpha of NodaTime v2.0 had recently added .Net Core support and taking a peek at that project revealed that the ‘compilationOptions’ section of project.json is where we can define them. With that in place we can then include/exclude particular features in the code just as the older PCL does.
Example of adding a “PCL” conditional compilation symbol from NodaTime v2.0 project.json
Task 3. We need to use new Type reflection model without breaking the older packages
PCLs targeting .Net 4.5 above use new reflection APIs which move some features from the Type class to a new TypeInfo class. Our Modern PCL can require an additional call to GetTypeInfo() where the older projects can act directly on a Type, but, as we share the source files between projects, we’d like to have the same code work for all projects. Since GetTypeInfo() is not available to the older projects we have added it as a conditionally compiled extension method on Type. This way all projects can use what looks like the new reflection API when in actual fact the older projects are still using the old APIs.
Task 4. We need to run tests using the CoreCLR
Once the Modern PCL was building successfully it was important to be able to verify the changes with some .Net Core compatible unit tests. This can be achieved using NUnit v3 tests wrapped in a .Net Core-compatible console app and NUnitLite (see this blog post for a detailed NUnit v3 guide).
In addition to running these tests on Windows we can easily run them on Linux by using the official ASP.NET Core preview Docker image.
Example Dockerfile to run NUnit tests
The test results can be seen by building an image using this Dockerfile – there is no need to actually run the image.
Once I was happy with the changes I submitted a pull request. Next steps involve this being merged back into the master branch, but, due to differences in build, test and packaging steps for .Net Core, this is not as straightforward as for typical changes. In the meantime my branch can be used just like the log4net one that I mentioned earlier to unblock exploration of other libraries.
The amount of work involved in porting this library was not huge but numerous sources had to be scoured to determine exactly which pieces were needed to form a solution. The techniques shared here can be applied by us again as we focus on other dependencies and they serve as a reference for others in the .Net Community when porting their libraries to support .Net Core.
I’d say that understanding the state of your dependencies is the most crucial aspect when considering porting an existing system to .Net Core. Consider whether you are likely to be blocked by a particular dependency, how much appetite you have for switching certain libraries and whether you are willing to use beta versions to prove other components. We are still a fair way off having a production .Net service being served from Linux and Docker using .Net Core but we have made tangible progress and steps in the right direction.