Tales from the .NET Migration Trenches - Our First Controller
Posts in this series:
- Intro
- Cataloging
- Empty Proxy
- Shared Library
- Our First Controller
- Migrating Initial Business Logic
- Our First Views
- Session State
- Hangfire
- Authentication
- Middleware
- Turning Off the Lights
In the last post, we prepped for our first set of pages migrated by extracting common logic into a shared library. With that in place, we're now ready to migrate our first controller. I like to do a single controller at a time rather than individual actions because controllers are a natural grouping of common behavior that often have interdependencies amongst actions, may share components, may share logic, etc.
Our first controller needs to be one that has enough representative front-end dependencies - but not too many actions going on. Ideally, we can migrate something with minimum business logic. That was one of the goals of cataloging our application - to help figure out where we should start. From that analysis, we can see that the "HomeController" only has one dependency and most of the actions do basically nothing but show a view with no real work being done.
One of the things that the latest drop of the .NET Upgrade Assistant is that it includes a controller migration action. We can right-click a controller in the MVC 5 application and there's an "Upgrade Controller" option:
If we run this on a "stock" demo application, it can give us an idea of what this option actually does. Here's the original controller:
using System.Web.Mvc;
namespace UpgradeSample.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}
And after the upgrade, our new controller:
using Microsoft.AspNetCore.Mvc;
namespace UpgradeSample.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult About()
{
ViewBag.Message = "Your application description page.";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}
Looks nearly identical, just a using
change. In practice, I found much of the same. Most of the time it's namespace changes, with other small changes (HtmlString
to IHtmlString
, little things like that). None of these are particularly difficult to work with.
When we go look at the migrated views, that's when we start to see some...issues. Here's the head
section of our original layout:
<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>
We're using the built-in bundler and minifier of ASP.NET MVC 5. Here's the ASP.NET Core version:
<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>
Now we're hit with our first big problem - ASP.NET Core does not have any built-in bundling and minification. As a feature, it just does not and will not ever exist. There's not any docs in the migration sections but we can check to see how ASP.NET Core recommends to do bundling and minification. Your situation might be different, but we wanted minimum changes to our apps so we decided to go the WebOptimizer route.
The HtmlHelper
extensions mostly work, but if you have custom extensions those are manual migrations.
Dealing with Feature Folders
One other big difference in our sample application is that it uses feature folders with a custom view engine. All of the views and code are placed together:
Now comes our first difficult decision - do we keep this structure in the new application? Or try to maintain it? I do like feature folders BUT if it makes migration harder I'm OK ditching it (for now). The existing custom view engine isn't too complicated:
public class FeatureViewLocationRazorViewEngine : RazorViewEngine
{
public FeatureViewLocationRazorViewEngine()
{
ViewLocationFormats = new[]
{
"~/Features/{1}/{0}.cshtml",
"~/Features/{1}/{0}.vbhtml",
"~/Features/Shared/{0}.cshtml",
"~/Features/Shared/{0}.vbhtml",
};
MasterLocationFormats = ViewLocationFormats;
PartialViewLocationFormats = new[]
{
"~/Features/{1}/{0}.cshtml",
"~/Features/{1}/{0}.vbhtml",
"~/Features/Shared/{0}.cshtml",
"~/Features/Shared/{0}.vbhtml",
};
}
}
But maybe there's a better way? I'll skip to the solution because I have integrated feature folders in ASP.NET Core and it's just a Razor configuration option:
builder.Services.Configure<RazorViewEngineOptions>(opt =>
{
opt.ViewLocationFormats.Add("/Features/{1}/{0}" + RazorViewEngine.ViewExtension);
opt.ViewLocationFormats.Add("/Features/Shared/{0}" + RazorViewEngine.ViewExtension);
});
With that in place, we can't really use the automatic tooling to migrate but that's OK, I'd rather be very careful about each step in the migration and tackle each problem as they come instead of all at once. We'll use our old friend "Cut and Paste" as our tool of choice.
Migrating the Home Controller
Another choice we have to make along feature folders is the controller location. In the original application, the controllers lived alongside the views as well. We can still do this in our new application BUT I'm making one small change - the name of the controller I'm correcting to FooController
instead of UiController
. It's just easier not to get too cute with controller names.
First up is moving the controller by itself. Since I'm just doing Cut-and-Paste, the namespaces won't automatically correct themselves. But again it's just some easy corrections of namespaces:
using ContosoUniversity.Features.Home;
using MediatR;
- using System.Web.Mvc;
+ using Microsoft.AspNetCore.Mvc;
namespace ContosoUniversity.DotNetCore.Controllers
{
- public class UiController : Controller
+ public class HomeController : Controller
{
private readonly IMediator _mediator;
public HomeController(IMediator mediator)
{
_mediator = mediator;
}
public ActionResult Index()
{
return View();
}
public ActionResult Chat()
{
return View();
}
public async Task<ActionResult> About()
{
var data = await _mediator.Send(new About.Query());
return View(data);
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}
Not too bad, but we still have to worry about the code referenced here and the views. In the next post, we'll migrate the code used by our controller before circling back to our views.