I recently migrated an old .NET Framework 4.5.2 app to .NET Core 2.0. This isn’t a guide to that process and certainly isn’t an exhaustive list of all the things that can go wrong, but rather provides an account of what worked for me, where the pitfalls were, and whether it was worth it.
The app essentially imports and processes data from SurveyMonkey. A DataPersistence project uses Entity Framework 6.2 to handle all database access. The logic for communicating with SurveyMonkey and transforming the data, as well as various administration functions lives in the ImporterCore library. Importer is a very thin command line app which wraps some functionality from ImporterCore, allowing it to be run easily as a scheduled Windows task. The Explorer project is an ASP.NET MVC 5 website to explore the data. A Tests project (not included in the diagram) is built on nUnit 3, and brings the total projects in the solution to 5.
Overview of the process
It ended up taking just over 2 days and looked like this:
>80% of the effort was spent reading blogs, trial & error, making mistakes, and drinking more coffee than I’m proud of. Doing this again would take at most a quarter of the time – if anyone on your team has done this already I strongly recommend you buddy up! The general approach I ended up taking was:
Break everything, Google solutions for every problem in turn until stuff worked again.
- For all but the web project, upgrade the csproj files to the new (much simpler) VS15 format, but still building for .NET 4.5.2. I chose to do this manually rather than creating new projects from scratch.
- Unload all projects in the solution apart from the DataPersistence library, which was at the bottom of the pyramid. Make that build for .NET Core instead.
- Update all the packages in the DataPersistence library to the latest versions which supported .NET Core, or for some cases like Entity Framework, replace the packages entirely with their .NET Core equivalents (Entity Framework Core in this case).
- Go through all build failures and patch up api changes until the project compiles.
- Repeat steps 2-4 but adding additional projects back into the solution one-at-a-time.
- Run the tests. Weep at the multitude of ways that “building just the same as before” does not mean “doing what it did before”.
- Fix up the issues from step 7, ranging from one-liners through to pretty invasive changes which touch dozens of files.
- Test manually.
Plenty of things went wrong. Most of these focus on issues I encountered during steps 7 and 8.
ImporterCore depended on a library I originally wrote a couple of years ago, which didn’t yet support .NET Core. It uses WebClient, which didn’t exist in .NET Core 1.0 / 1.1, so getting this working would previously have been painful. Fortunately, WebClient was added into .NET Core 2.0, making the upgrade simple – just some changes to the csproj, AssemblyInfo, and nuspec files. But if you depend on any incompatible libraries which you don’t control, you could be blocked.
This is what took most of the effort. Entity Framework 6.2 isn’t available for .NET Core, and its replacement EF Core has significant changes, which makes it more like a port than an upgrade. (Here is a feature list comparison) First though, a happy point:
Overall I’ve grown to like EF Core more than EF 6.2. The main reason is an emphasis on configuration by convention. Here’s a mapping file for an original Survey object. It tells Entity Framework the names of columns for each property, the table name, and which item is the key (plus more in most cases).
public class SurveyMap : EntityTypeConfiguration<Survey>
// Primary Key
this.HasKey(t => t.Id);
// Table & Column Mappings
this.Property(t => t.Id).HasColumnName("Id");
this.Property(t => t.Title).HasColumnName("Title");
this.Property(t => t.Nickname).HasColumnName("Nickname");
this.Property(t => t.Category).HasColumnName("Category");
this.Property(t => t.Language).HasColumnName("Language");
this.Property(t => t.IsOwner).HasColumnName("IsOwner");
//And a dozen or so more properties
In EF Core, by convention a property is assumed to map to a column of that name unless you tell it otherwise, just as the overall object is assumed to map to a table of the same name. It’s also assumed that if there’s a property called Id or SurveyId, then that is the Primary Key unless you tell it otherwise. So I got to delete nearly a thousand lines of unnecessary boilerplate, which is pretty cool.
Most of the remaining mapping can be done through annotations (for example to tell Entity Framework about values which are generated by SQL Server), though a few things like composite keys can only be configured using the fluent api, which is done in the context’s OnModelCreating(ModelBuilder modelBuilder) method.
There are a handful of api changes – for example foreign keys used to be configured with something like:
.HasRequired(parent => parent.ResponsePage)
.WithMany(child => child.ResponseQuestions)
.HasForeignKey(prop => new
In EF Core, HasRequired() was changed to HasOne(). The tests also used context.Database.Create() and context.Database.Delete(), which in EF Core were changed to context.Database.EnsureCreated() and context.Database.EnsureDeleted(), but these were pretty minor.
Slightly more effort was getting some custom handling of DateTime values working nicely. The application always stores DateTimes as Utc in the database, but when reading from the database Entity Framework doesn’t know this, so treats them as DateTimeKind.Unspecified, which would lead to some incorrect behaviour elsewhere in the app. In the original version of the application, I worked around that with a variation of this technique – essentially using the Interception capabilities in Entity Framework to patch up the DateTime object’s DateTimeKind property. Unfortunately, this kind of Interception isn’t yet possible in EF Core, so instead I used an EntityMaterializerSource, as described here, with some adaptations to let it handle nullable columns. Conveniently the old version of the application had tests in place to cover this conversion in a variety of scenarios (in particular because projection queries behave differently to entity queries) so it was easy to be sure it was working as expected.
As an aside, it drive me crazy that neither version of Entity Framework handles this well out of the box – I can’t remember the last time I stored date information in a database without wanting to consider it as UTC.
This was the biggest time suck: EF Core doesn’t support lazy loading. It’s coming in EF Core 2.1, due Q1 / Q2 2018, but that’s no help today. I’ve written a fair bit about Entity Framework performance, and the original choice to use lazy loading was for good reasons. There’s no indication anything’s going wrong when building the app – it’s just that when accessing an entity’s child property which you expect to be lazily loaded, you get an empty list instead. Fortunately there were quite a few end to end tests using a live database which failed all over the place, but I could imagine this not being noticed in a situation where an app only very occasionally uses lazy loading (possibly without the developer even knowing), where there weren’t tests in place.
The solution of using eager loading instead wasn’t the end of the world, but it did result in a lot of extra testing, generally messier code with deeply nested Include() and ThenInclude() statements all over the place, and is slightly less performant overall. I’ll probably undo these changes in a few months once upgrading to v2.1 is possible.
While .NET Framework configuration is typically stored as xml in app.config / web.config files, .NET Core uses appsettings.json to store configuration data. I actually like this change a lot, but it did require some changes.
Hosting in IIS
The original Explorer website was hosted under IIS, and I didn’t realise that IIS doesn’t know how to serve ASP.NET Core websites out of the box. ASP.NET Core uses the Kestrel server, which runs as a separate process to IIS. You need to install the .NET Core Windows Server Hosting Bundle which lets IIS handle things like security and some management tasks while handing off to Kestrel to actually run the code. The application pool needs to be configured to run No Managed Code too, since that’s handled by Kestrel.
Unfortunately I discovered this the hard way by deploying it to production and wondering why it didn’t work. Nor did I have access to install components on the server, so there was a little downtime while I waited for a friendly sysadmin to help out. Oops.
I didn’t encounter any roadblocks; just a sequence of minor issues which each caused a small amount of hassle. The Portability Analyzer lets you know if this will be the case before doing the work – you should definitely run it before you start.
This isn’t a huge app – 5 projects with a few tens of thousands of lines of code, much of which is boilerplate / framework stuff. Were I to do this again it would be a lot quicker than the couple of days it took me this time around, so if you’re considering migrating a larger application I’d recommend practicing on a smaller one like this first, or teaming up with someone who’s done it before if you can.
Ultimately I had little choice but to do this migration because of the need to interact with some other .NET Core systems. If it weren’t for that I probably wouldn’t have bothered – while it’s nice to use the shiny new thing and nothing’s worse than before (at least not that I’ve noticed yet!), I haven’t noticed it make anything easier either.