Tales from the .NET Migration Trenches - Cataloging

Posts in this series:

When I talk with folks about modernization, inevitably the question comes up "OK but how much is it going to cost?" This is never an easy question but first we need to understand what exactly our target state is. That's also context-dependent, but for the actual project this series is about, our goals were:

  • Completely retire any .NET Framework applications and libraries
  • Migrated to latest LTS .NET release
  • Replace, remove, or port any unsupported or incompatible libraries/features/frameworks
  • Migrate any supported libraries/features/frameworks

It can be easy to be ambitious about "modernization" where you realize that if you're already mucking about the whole codebase, why not improve it? That introduces risks and muddies the water about our overall goal. We're not interested in a complete rewrite. Instead, each bit of code will be moved as-is, and only modified to target ASP.NET Core and/or .NET LTS.

Unfortunately, none of the tooling is perfect for figuring out how to migrate any bit of code. The legacy version of the CLI tool has an "analyze" tool that then lets you view the results in an HTML document:

However, I found it to be a bit lacking and challenging to sift through. Eventually our goal is a punch list or backlog of stories to tackle, and this won't get us there.

Instead, we took a more manual approach. In particular, we wanted to catalog everything that needed to be ported, upgraded, or removed:

  • MVC HTTP endpoints (MVC and API controllers/actions)
  • Non-MVC HTTP endpoints (ASPX, ASHX, etc)
  • ASP.NET MVC middleware
  • ASP.NET middleware (web.config etc)
  • Dependencies

Our general modernization strategy was not so much "lift-and-shift" because although it's possible to migrate many things as-is with shims, we wanted to use "modern" .NET and ASP.NET Core for as much as we could.

ASP.NET MVC Endpoints

Unfortunately, due to the dynamic nature of MVC routing, there's not a meaningful way to categorize at runtime what all the possible/valid MVC or API routes are. Instead, I typically resort to reflection tricks in a unit test. I create a "ScratchTests" project and loop through types and members to find valid controllers and actions:

[TestMethod]
public void ControllerStats()
{
    var markerType = typeof(Startup);

    var controllers =
        (from t in markerType.Assembly.GetTypes()
        where !t.IsAbstract && t.IsPublic && typeof(Controller).IsAssignableFrom(t)
        let m = t.GetMethods(BindingFlags.Instance 
                             | BindingFlags.DeclaredOnly 
                             | BindingFlags.Public)
            .Where(m => !m.GetCustomAttributes<NonActionAttribute>().Any())
        let c = t.GetConstructors().First()
        select new ControllerInfo
        {
            Type = t,
            Actions = m,
            Constructor = c,
        }).ToList();

    Console.WriteLine(controllers.Count.ToString());
    Console.WriteLine(controllers.SelectMany(c => c.Actions).Count().ToString());

    foreach (var controller in controllers)
    {
        Console.WriteLine($"{controller.Type.FullName}\t{controller.Actions.Count()}\t{controller.Constructor.GetParameters().Length}");
    }
}

A controller action is any public instance method that isn't decorated with a NonAction attribute. In my sample app (ContosoUniversity, of course), this gets me an output of:

6
58
ContosoUniversity.Features.Department.UiController	8	1
ContosoUniversity.Features.Student.UiController	8	3
ContosoUniversity.Features.Instructor.UiController	8	1
ContosoUniversity.Features.Home.UiController	4	1
ContosoUniversity.Features.Course.UiController	8	3
ContosoUniversity.Features.Account.UiController	22	0

If I have multiple web applications, I like to know the general relative sizing of controllers and actions. Then as we're looking at tackling any one controller, I want to know roughly how "complex" it is. That can be complicated or even impossible to quantify, but a couple rough measures are "number of actions" and "number of dependencies". There are probably more precise ways of calculating this, but our overall goal was really just to know what would be a simple controller to start with.

Doing the same with API controllers gives us an inventory (and list of stories) as we planned as much as possible to migrate one controller at a time.

Non-MVC HTTP Endpoints

Luckily our project didn't have many of these, but since ASP.NET MVC is still just ASP.NET, we had to catalog:

  • ASPX pages
  • ASMX web services
  • ASHX web handlers

There are probably other ways to have stand-alone HTTP handlers but in our project this is really all we needed to look for. Finding and cataloging them was just a matter of using Powershell to find and list files with those extensions.

Now the strategy of how to deal with this, I'll save for the next post. Initially we just wanted to understand the scope of what we were dealing with.

ASP.NET MVC Middleware

Middleware in MVC is a bit different than ASP.NET in general. Typically, we can look in Global.asax.cs to see our MVC middleware and configuration:

public class MvcApplication : HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        DbInterception.Add(new SchoolInterceptorTransientErrors());
        DbInterception.Add(new SchoolInterceptorLogging());
        AutoMapperInitializer.Initialize();

        ViewEngines.Engines.Clear();
        ViewEngines.Engines.Add(new FeatureViewLocationRazorViewEngine());
    }
}

I'm mainly looking here at what features of MVC we're using, and how we're extending it. Above, we can see that there's other "non-MVC" stuff going on which will also need to be ported. We'll also dig down into each of these startup methods to see if there's anything special going on but for the most part I'm just trying to catalog.

Finally, we also have OWIN to worry about so I look for an OwinStartup class:

[assembly: OwinStartup(typeof(Startup))]
namespace ContosoUniversity
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();

            GlobalConfiguration.Configuration
                .UseSqlServerStorage("SchoolContext"
                .UseStructureMapActivator(IoC.Container)
                ;


            app.UseHangfireDashboard();
            app.UseHangfireServer();

            ConfigureAuth(app);
        }
    }
}

This is a bit more revealing, as we have SignalR, Hangfire, and authentication to worry about.

ASP.NET Middleware

In addition to the MVC-specific middleware (your Global.asax is probably WAY longer than this), I also need to worry about what ASP.NET kinds of things are going on. So much can happen in web.config, we need to make sure we account for those.

The sections I'm paying attention to are system.web and system.webServer which will declare all sorts of configuration, handlers, and modules:

<system.web>
  <compilation debug="true" targetFramework="4.8.1"/>
  <httpRuntime targetFramework="4.8.1"/>
  <customErrors mode="RemoteOnly" redirectMode="ResponseRewrite">
    <error statusCode="404" redirect="/404Error.aspx"/>
  </customErrors>
  <!-- Glimpse: This can be commented in to add additional data to the Trace tab when using WebForms
      <trace writeToDiagnosticsTrace="true" enabled="true" pageOutput="false"/> -->
  <httpModules>
    <add name="Glimpse" type="Glimpse.AspNet.HttpModule, Glimpse.AspNet"/>
  </httpModules>
  <httpHandlers>
    <add path="glimpse.axd" verb="GET" type="Glimpse.AspNet.HttpHandler, Glimpse.AspNet"/>
  </httpHandlers>
</system.web>

When it comes to figuring out what to do with each of these sets of values, we'll also need to examine the purpose for each and how critical each are to the overall operation of the ASP.NET Core application. Some things (like authorization) might be critical while others (Glimpse 😢) are not.

Dependencies

Library and component dependencies can come in a couple main flavors:

  • Package references
  • Library references

Component libraries can get rather exotic as well. Getting a list can be pretty straight forward - just listing what's in the various config files like packages.config. Many of the packages can be ignored as they won't come over in any form. Things like Microsoft.AspNet.* will have replacements we likely won't even need to reference explicitly. Other packages will have equivalents in the new runtimes. Some packages will be deprecated, with wholesale replacements. Others will have no replacement and could be years old without any new release.

This list can be quite long but initially our big question we want to answer is "are there any dependencies that do NOT have any upgrade path?". These dependencies present the biggest risk to migration as we might have to do quite a bit of work to replace or remove them.

The end result of all this cataloging feels a bit like organizing a junk drawer. You might not know how much cruft has piled up over the years. At the end, we felt like we had a decent grasp of everything that existed in the current system and needed decisions on what to do with it.

In the next post, I'll walk through our overall migration strategy and plan based on our cataloging of "current state" of the system.