I recently went about the task of migrating most of my OSS libraries to the "new" .NET Core platform.
I would like to share with others some of the choices I made and the motivations I had. Besides, I also want to reflect about the pains and gains of doing so.
There are many worthy articles of people that have done it before me, at much earlier stages of the platform. But I'll write one myself because:
- Some of those valuable resources are from brave people that worked when JSON project format was a thing whereas I have done it in the bleaker days of XML project format
- I have not-so-unique scenarios that are useful to me and I did not find anywhere else
- It's my blog and there is no such thing as a "find duplicated topics and delete" bot in the Internet (yet)
Obligatory mention to tooling
I have to admit this is my second round to porting those libraries to support .NET Core.
The first round was with Beta versions of the tooling (but RTM versions of the runtime) and it was anything but not pleasant. I naively supposed that with RTM tooling it was going to be much easier, but it was not.
I gave up on the otherwise wonderful Visual Studio Code to make the newer Visual Studio 2017 my weapon of choice.
It is waaay better than the experience I had, but still not the same as "classic" .NET. Half the blame has to lie on Resharper acting super weird in a mixed environment of full .NET Framework and .NET Core.
Other than Visual Studio 2017, or more accurately, inside of, a tool to run before embarking the migration is the .NET Portability Analyzer. This Visual Studio extension (there is a console version for those that do not dig Visual Studio anymore) would produce a report about how portable your code is to several platforms in terms of API usage and would suggest you possible workarounds.
It is basically a prettier output than changing the target of your project, hit compile and fall into despair.
The other must-have "tool" is, ironically, one that you cannot have, but the most useful website you can imagine for the migration task: a reverse package search.
Instead finding packages, one types an API (a class, interface, method,...) and it will display a list of packages that contain that member.
Imagine you get a compilation error in the library that is being ported, stating that
IConvertible is not found. That means that member is not part of the core library, but may come from a package. Type the name of the member in the site and you'll know that from netstandard1.3 and upwards, if you reference the System.Runtime package you are going to be able to use that API.
Other than those tools, is "just" old boring compile, fix and repeat kind of work. But before it comes to it, there are some hard choices to be made.
A choice is a future murderer
There is a very interesting document from Microsoft themselves that explain some of the various possibilities that you can take when you decide to support the full .NET Framework on top of the new .NET Standard/Core.
From the document, I ended up taking the project replacement route in one of my projects but I took another approach not mentioned in the document for two of the others.
First choice. .NET Core or .NET Standard?
Do you want your library to have further reach by supporting the most platforms?
Sounds like a good idea, but many platforms mean lower API surface since not all platforms are equally capable.
In my case, this was the easiest decision: .NET Standard will be. Let's give other platforms the ability to use my code.
Second choice. Ready to part with functionality?
In any case, .NET Core or .NET Standard offer a trimmed API surface. It may be impossible to retain all your features, because the API might simply not be there.
Keeping as much as possible is also feasible once you come to terms to the fact you are going to bring a lot of NuGet packages with your library.
Another case is depending on a project that does not have a portable version. And there are many.
If one is happy to give features up for the sake of portability, replacing the project and whichever code that is not compatible for either compatible code or deletion is a way to go.
It is easy to maintain a single project and the build process becomes way easier.
If giving up features makes you feel a little sick, then multi-targeting (that is different versions of your library for multiple platforms) is your way to go.
Once multi-targeting, once needs to decide what to do with the unsupported features:
- have a base package that targets as many platforms as possible and release the extra features as add-ons of some sort that target only the supported platforms. That is not always feasible, but when it is, it sounds like a good trade off to justify the extra project.
- have a single package with two versions of the library: the .NET Framework assembly gets all the goodies and the portable assembly gets a washed-down set of features
But multi-targeting can be achieved is several different ways, each with its unique trade-offs.
You are likely to migrate your project to the new format and then, perform some conditional compilation tricks to remove the unsupported parts from the un-supportive platforms, while keeping them in the supported ones.
Conditional tricks include:
- conditional compilation directives. Meaning sprinkling your code with
#elsein your source code files. Definitely feasible but noise and in the verge of becoming a maintainability liability.
- isolate the un-supported parts in their own files (partial classes are good tools when applicable) and use project conditional excludes to not compile those files in certain platforms.
The old project stays the same and there is an extra project in order to target the newer platforms.
The aforementioned document points out this option: having a new project in another folder. One can do file linking to bring source code to the new project but it is definitely weird. And we still need to do the conditional tricks to bring the right files.
What the document does not point out is that you can have the projects in the same folder. So less file inclusion magic needs to be done so other tooling than Visual Studio can be used.
But What did I do?
In the end, I ended up doing different things for every project.
I'll go through the details in another post as I feel this is already getting too long.
Keep an eye on it.