Tales from the .NET Migration Trenches - Shared Library

Posts in this series:

In the previous post, we established a beachhead with a completely empty proxy application to prepare for migrating controllers incrementally all the way to production.

There's still one more step we need to take care of before we migrate controllers, however, and it's all the common models and logic that both the .NET Framework and ASP.NET Core application will share. Right now we have two completely independent projects:

Very soon after we migrate a controller, we'll hit some shared logic/types/dependencies between these two. To enable each application to share logic, we can create a shared library:

In this shared library would be all the "non-ASP.NET" stuff. Technically, you can also move common logic that depends on ASP.NET pieces (like HttpContext), but I'll only focus on the logic that doesn't depend on any ASP.NET components.

A secondary concern will be business logic that has "external" dependencies that aren't ASP.NET. These are things like:

  • EF6 configuration
  • ViewModels, MediatR handlers
  • DI configuration

These pieces I'd rather migrate as I go instead of trying to get it all in one go. Some of those other dependencies require a bit more thought, but at least the "Domain/Data" model is fairly independent:

Even though my target frameworks are different, I can leverage .NET Standard in this common library to have one project be able to be consumed by multiple target target frameworks. If that doesn't work, I can always resort to multi-targeting (depending on my situation).

If I want any code to be incrementally migrated (and not ported) from ASP.NET to ASP.NET Core, I'll need to make sure that my dependencies can support both frameworks.

Dependency Upgrade

Looking at my dependencies that aren't ASP.NET specific in the analysis report, it tells me which packages which need to be upgraded:

Dependency Current Version Target Version
Antlr4 3.5.0.2 4.6.6
AutoMapper 5.1.1 10.1.1
AutoMapper.EF6 0.5.0 2.1.1
DelegateDecompiler 0.18.1 0.32.0
DelegateDecompiler.EntityFramework 0.18.1 0.32.0
EntityFramework 6.1.3 6.4.4
FluentNHibernate 2.1.1 3.2.1
FluentValidation 6.2.1.0 8.6.1
FluentValidation.Mvc5 6.2.1.0 8.6.1
MediatR 2.1.0 12.0.1
NHibernate 5.2.5 5.3.3

Funny enough this application is so old that it still has NHibernate references. It's also worth using some static analysis tools (like NDepend) to see if there are any unused package/library references we can remove so that we're not muddying the waters here.

Before we create any shared library, we'll upgrade our common dependencies to the latest version that support BOTH .NET Framework and .NET 6/7/8. Some things I'll leave alone for now, such as the current DI framework (StructureMap) as I don't necessarily want to bring those dependencies forward.

I won't go into too many details here on the individual library migrations other than to emphasize tackling these separately from moving logic over to the proxy application. We want to move in small, incremental, verifiable steps and each library upgrade could introduce regressions we weren't expecting.

After our dependencies are upgraded and deployed, we can move to refactoring our classes into the shared library.

Creating the Shared Library

In our shared library, we need to determine what target framework(s) we should support. Most of our dependencies support netstandard2.0 in some version but not all. AutoMapper for example dropped support for .NET Standard 2.0 starting with version 11.0, moving to .NET Standard 2.1. AutoMapper.EF6, DelegateDecompiler.EntityFramework, and EntityFramework dual target .NET Framework and .NET Standard 2.1.

The commonality here means that we can't simply target .NET Standard 2.0 since not all of our dependencies do. Instead, we'll create a class library that targets both net48 and netstandard2.1:

<PropertyGroup>
  <TargetFrameworks>net48;netstandard2.1</TargetFrameworks>
  <RootNamespace>ContosoUniversity</RootNamespace>
</PropertyGroup>

It's slightly annoying to multi-target but necessary given we don't have a common single target framework. With SDK-style projects, it's trivial to do this. Next, we pull in all of our dependencies at the correct versions:

<ItemGroup>
  <PackageReference Include="AutoMapper" Version="10.1.1" />
  <PackageReference Include="AutoMapper.EF6" Version="3.0.0" />
  <PackageReference Include="DelegateDecompiler" Version="0.32.0" />
  <PackageReference Include="DelegateDecompiler.EntityFramework" Version="0.32.0" />
  <PackageReference Include="EntityFramework" Version="6.4.4" />
  <PackageReference Include="FluentNHibernate" Version="3.2.1" />
  <PackageReference Include="MediatR" Version="12.0.1" />
  <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>

With all of my dependencies in place, moving types is really just a matter of using the refactoring tools (Rider or ReSharper or for the less fortunate, Visual Studio's tooling) to move types from one project to another:

I much prefer the refactoring tools because it results in far fewer errors/mistakes. Finally, we modify the ASP.NET Core application to reference the shared project to make sure we don't have any compile errors:

This is another deployment to production step to ensure we haven't introduced any regressions with our refactoring. Finally with common dependencies and code pulled out, we're ready for our first major hill to climb - migrating our first controller.