Tales from the .NET Migration Trenches - Our First Views

Posts in this series:

Back when we looked at our first controller, we tried out the "automatic" migration and the controllers migrated just fine but our views did not:

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    @*@Styles.Render("~/Content/css")*@
    @*@Scripts.Render("~/bundles/modernizr")*@
</head>
<body>
  <!-- junk -->
  @*@Scripts.Render("~/bundles/jquery")*@
  @*@Scripts.Render("~/bundles/bootstrap")*@
  @RenderSection("scripts", required: false)
</body>

Those commented out sections are there because ASP.NET Core does not support bundling and minification. We'll need to figure out an alternative solution for that. We also need to check any other components or libraries to see if there are compatible versions for ASP.NET Core. Front-end assets don't really need "migrating" so those should work just fine.

But if there are components that plug in to ASP.NET 4.8 itself, we'll need to address those as well. In our real-world application, we had two problematic components:

  • A "file upload" widget that included an .ASHX handler for uploads
  • The HtmlTag library that exposes custom HtmlHelper extensions

The file upload widget had no upgrade path - it was a legacy component that hadn't had a new release in many years, let alone any version targeting ASP.NET Core.

For the HtmlTag situation, that library is maintained and supports ASP.NET Core directly. We'll need to port our existing usage to the latest version of the library. This really just leaves the bundling and minification.

Modernizing Bundling and Minification

Because there is no out-of-the-box solution for bundling and minification in ASP.NET Core, we need to decide what our solution should be in our new ASP.NET Core world. Luckily, the ASP.NET Core docs on this topic give us some insight on our two basic options:

  • An OSS library that's very similar to the capabilities in ASP.NET MVC 5 (WebOptimizer)
  • 3rd-party libraries that aren't tied to ASP.NET Core (Webpack, etc.)

A 3rd-party library might be an option if we're also looking at incorporating more modern JavaScript libraries in the future. From a strict "minimal lift-and-shift" perspective, WebOptimizer looks better.

Integrating WebOptimizer is pretty simple, we first add the package:

<PackageReference Include="LigerShark.WebOptimizer.Core" Version="3.0.384" />

And configure it in our application startup:

builder.Services.AddWebOptimizer(pipeline 
    => BundleConfig.BundlingPipeline(pipeline, builder.Environment.IsDevelopment())
    );

The existing bundle configuration from the ASP.NET MVC 5 app looks like:

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                    "~/Scripts/jquery-{version}.js"));

        // other JS

        bundles.Add(new StyleBundle("~/Content/css").Include(
                  "~/Content/bootstrap.css",
                  "~/Content/site.css"));

        BundleTable.EnableOptimizations = true;
    }
}

We found that although the bundling capabilities of MVC 5 and ASP.NET Core were similar, they were not identical. Additionally, we needed to decide if we were going to migrate all of our front-end assets at the same time. Many pages included their own assets in separate bundles, and the URLs for the final bundles were slightly different.

We preferred to migrate incrementally as much as possible and not have any big bang migrations. If we tried to migrate all of our bundling all at once, we'd have to figure out how to deal with all of the existing pages that have bundles that don't work - since typically bundles are defined on shared layouts.

To make sure that we can support both applications each with their own bundling schemes, we decided that each app will do its own bundling, but the static assets will only live in one place in the repository. We migrated the configuration of the bundling to WebOptimizer (abridged version):

private static void ProcessScripts(IAssetPipeline pipeline)
{
    pipeline.AddJavaScriptBundle("/Scripts/bundles/jquery.js", 
        "/wwwroot/Scripts/jquery-2.1.4.js")
        .UseFileProvider(_fileProvider);

    pipeline.AddJavaScriptBundle("/Scripts/bundles/jqueryval.js",
        "/wwwroot/Scripts/jquery.validate.*")
        .UseFileProvider(_fileProvider);

    pipeline.AddJavaScriptBundle("/Scripts/bundles/modernizr.js",
        "/wwwroot/Scripts/modernizr-*")
        .UseFileProvider(_fileProvider);

    pipeline.AddJavaScriptBundle("/Scripts/bundles/bootstrap.js",
        "/wwwroot/Scripts/bootstrap.js",
        "/wwwroot/Scripts/respond.js")
        .UseFileProvider(_fileProvider);

    pipeline.AddJavaScriptBundle("/Scripts/bundles/lodash.js",
        "/wwwroot/Scripts/lodash.js")
        .UseFileProvider(_fileProvider);
}

Then our ASP.NET Core layout can use this new way of doing bundles:

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - Contoso University</title>
    <link rel="stylesheet" href="/Content/css.css" />
    <script src="/Scripts/bundles/modernizr.js"></script>
    <script src="/Scripts/bundles/lodash.js"></script>

It's not identical to what we saw before (there are many options we can configure with WebOptimizer omitted here), and we found it impossible to exactly emulate the bundling of ASP.NET MVC 5. We just needed to make sure we didn't duplicate files by altering our ASP.NET Core application to pull in the ASP.NET MVC 5 assets through linking:

<Content Include="..\ContosoUniversity\Content\**\*.*" Link="wwwroot\Content\%(RecursiveDir)%(Filename)%(Extension)">
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="..\ContosoUniversity\Scripts\**\*.*" Link="wwwroot\Scripts\%(RecursiveDir)%(Filename)%(Extension)">
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

These files are in the old project, but linked into the new project - a technique I've use many times in the past:

If we add a new asset file to the ASP.NET MVC 5 project, it will automatically show up here as well.

This is only a temporary shim, as once we've moved all of the controllers/actions over, we can also move all of the static assets over and remove the "linking" altogether. Our ASP.NET Core application doesn't care however, it will transparently use the files in the new location. Although it added some extra steps in the interim, we preferred this approach since it eliminated any big-bang changes.

As we migrate controllers, we'll encounter features and middleware not yet migrated. In the next post, we'll look at our first major middleware feature to share across applications - Session.