The Obligatory "How I Upgraded To .NET Core 3.1" Post
And No, It Wasn't Much Fun
by JamesQMurphy | December 28, 2019
Scott Hanselman did it. Rick Strahl did it. So I guess I needed to do it as well -- and, of course, blog about it. Yep, it was time to put aside the normal development work and upgrade my ASP .NET Core 2.2 site to .NET Core 3.1.
And with good reason; .NET Core 2.2 has officially reached end-of-support status, which means that Microsoft will no longer release patch updates for .NET Core 2.2. .NET Core 3.0 awaits the same fate on March 23, 2020. But .NET Core 3.1 is a Long-Term Support (LTS) version, which means it will be supported for three years.
So basically, if you're on .NET Core 2.2 or 3.01, now is the time to upgrade. I went through it, and although it wasn't bad, it wasn't exactly fun either. Make sure you plan enough time to go through the upgrade process, and don't forget to test your app. Microsoft's official migration documention is a good starting place, and it's what I used to guide myself through the migration.
All of the changes I did for the upgrade are in release v0.3.10. Most of the changes that I had to do are the same well-documented changes that everybody else had to do, but there were a few issues that I had to figure out on my own.
The Journey Of 1,000 Miles
The very first step was to update the project files to target netcoreapp31
. You can also remove the explicit references to the Microsoft.AspNetCore.*
packages and the Microsoft.VisualStudio.Web.CodeGeneration.Design
package, since they are now included when you use the netcoreapp31
Target Framework Moniker. While I was in there, I also updated the Amazon.Lambda.*
packages to the latest versions.
Before
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
.
.
.
<ItemGroup>
<PackageReference Include="Amazon.AspNetCore.DataProtection.SSM" Version="1.1.0" />
<PackageReference Include="Amazon.Lambda.AspNetCoreServer" Version="3.1.0" />
<PackageReference Include="Amazon.Lambda.Core" Version="1.1.0" />
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.0.0" />
<PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.6.0" />
<PackageReference Include="AWSSDK.DynamoDBv2" Version="3.3.101.64" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.3" />
</ItemGroup>
After
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
.
.
.
<ItemGroup>
<PackageReference Include="Amazon.AspNetCore.DataProtection.SSM" Version="1.1.0" />
<PackageReference Include="Amazon.Lambda.AspNetCoreServer" Version="4.1.0" />
<PackageReference Include="Amazon.Lambda.Core" Version="1.1.0" />
<PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.1.0" />
<PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.7.0" />
<PackageReference Include="AWSSDK.DynamoDBv2" Version="3.3.101.64" />
</ItemGroup>
Easy enough, but it wasn't the only project file I had to change. I had several other assemblies that targetted netstandard20
. But for assemblies that also contained references to Microsoft.AspNetCore.Identity
(like JamesQMurphy.Auth
, for example), I needed to upgrade those projects as well to target netcoreapp31
. In addition, since the project SDK was Microsoft.NET.Sdk
and not Microsoft.NET.Sdk.Web
, I needed to add a Framework reference2.
Before
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
</ItemGroup>
</Project>
After
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
Changes To Startup.cs
Most of the changes that I needed to do were in Startup.cs
, and that's where you'll likely have to do the majority of your changes.
Fixing Syntax Errors
First, there were a few syntax changes that I needed to take care of. IHostingEnvironment
has been replaced by IWebHostEnvironment
, so the signature of Configure
needed to change:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
The IsDevelopment()
extension method is in a different namespace now (Microsoft.Extensions.Hosting
), so I needed to add a using
statement3.
using Microsoft.Extensions.Hosting;
MVC Service Registration
ASP.NET Core 3.0 offers some flexibility in configuring just how much MVC you want to use. The details can be found here, but in short, the "new" way to do services.AddMvc()
is like this:
services.AddControllersWithViews(options =>
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())
);
services.AddRazorPages();
Endpoint Routing
Microsoft recommends switching to Endpoint Routing "if possible" (source). Thankfully, they have made it pretty straightforward as you can see below. Note that the Health Check endpoint also moved into the call.
Before
app.UseMvc(routes =>
{
routes.MapControllerRoute(
name: "blogIndex",
template: "blog/{year?}/{month?}",
defaults: new { controller = "blog", action = "index" });
routes.MapControllerRoute(
name: "blogDetails",
template: "blog/{year}/{month}/{slug}",
defaults: new { controller = "blog", action = "details" });
routes.MapControllerRoute(
name: "default",
template: "{controller=home}/{action=index}/{id?}");
});
After
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks(Configuration["WarmUrl"]);
endpoints.MapControllerRoute(
name: "blogIndex",
pattern: "blog/{year?}/{month?}",
defaults: new { controller = "blog", action = "index" });
endpoints.MapControllerRoute(
name: "blogDetails",
pattern: "blog/{year}/{month}/{slug}",
defaults: new { controller = "blog", action = "details" });
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=home}/{action=index}/{id?}");
});
Using Authorization and Ordering of Services
Heed Microsoft's warning about the ordering of the services:
For most apps, calls to
UseAuthentication
,UseAuthorization
, andUseCors
must appear between the calls toUseRouting
andUseEndpoints
to be effective.
Because of this warning, I shuffled around the calls to the various services:
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/home/error");
app.UseHsts();
}
app.UseStaticFiles(); // abridged; see my source code for the full version
app.UseRouting(); // New for ASP.NET Core 3.0
app.UseAuthentication();
app.UseAuthorization(); // New for ASP.NET Core 3.0
app.UseHttpsRedirection();
app.UseCookiePolicy();
app.UseEndPoints(endpoints => ...);
I also needed one other change in Startup.cs
, but in order to explain it, I need to talk about a breaking change introduced in the SignInManager
class.
Changes to ApplicationSignInManager.cs
This is a breaking change that will only affect you if you are providing your own implementation of SignInManager
:
Identity: SignInManager constructor accepts new parameter
Starting with ASP.NET Core 3.0, a new
IUserConfirmation<TUser>
parameter was added to theSignInManager
constructor. For more information, see aspnet/AspNetCore#8356. (Source)
And since I am, in fact, implementing my own SignInManager
, I followed Microsoft's recommended action:
If manually constructing a
SignInManager
, provide an implementation ofIUserConfirmation
or grab one from dependency injection to provide.
Hence, ApplicationUserConfirmation
was born:
public class ApplicationUserConfirmation : IUserConfirmation<ApplicationUser>
{
public Task<bool> IsConfirmedAsync(UserManager<ApplicationUser> manager, ApplicationUser user)
{
return Task.FromResult(user.EmailConfirmed);
}
}
Since this class is completely stateless and depends only on the ApplicationUser
object passed in, a Singleton instance is an appropriate choice for dependency injection:
services.AddSingleton<ApplicationUserConfirmation>();
A Change Out Of Left Field
And then there was the breaking change that nobody mentioned4. ASP.NET Core defines an interface ILookupNormalizer
which allows you to define how you want to "normalize" case-insensitive comparisons. For example, the default implementation simply converts all strings to uppercase; the strings username and UserName both normalize to USERNAME, and thus compare as equal.
What's the breaking change? The interface completely changed from ASP.NET Core 2.2:
# ASP.NET Core 2.2
public interface ILookupNormalizer
{
string Normalize(string key);
}
To ASP.NET Core 3.0:
# ASP.NET Core 3.0
public interface ILookupNormalizer
{
string NormalizeEmail(string email);
string NormalizeName(string name);
}
In the old days of Microsoft, this would have been named ILookupNormalizer2
or something silly like that. I'm assuming that since this particular interface is rarely called by client code, they felt justified in breaking it. I'm totally fine with the change -- it allows implementers to use a different normalization mechanism for email and usernames -- but I would have liked to seen it listed somewhere.
This also means that the NormalizeKey()
method on UserManager
has been replaced with NormalizeEmail()
and NormalizeName()
methods. As it turns out, I had one call to NormalizeKey()
inside of AccountController.cs
, and I don't know why I even had it -- it wasn't necessary, since FindByEmailAsync()
will call NormalizeEmail()
for you.
var user = await _userManager.FindByEmailAsync(model.Email);
So after all that, the interface change really shouldn't have affected my website code. That said, I did have several instances of ILookupNormalizer.NormalizeKey()
in my unit test project that had to be changed to either NormalizeEmail()
or NormalizeName()
.
Build Changes
Aside from a typo in the endpoint routing pattern, the updated code worked immediately on my desktop. Surely I wouldn't need any changes to the CI/CD pipeline, right? Well, I shouldn't have, but unfortunately I did.
It appears that, at the time of this writing, the Azure Pipelines Build Agents (at least the Linux ones) have .NET Core 3.0 installed, not .NET Core 3.1. Fortunately, user Bernard Vander Beken on StackOverflow posted this answer, which was to add a UseDotNet
step to the build process:
- task: UseDotNet@2
displayName: 'Install .NET Core sdk'
inputs:
packageType: sdk
version: 3.1.100
installationPath: $(Agent.ToolsDirectory)/dotnet
And that did the trick.
Deployment Changes
Ok, so the build was working. What about the deployment process to AWS?
Fortunately, no changes were needed. In an older blog post, I showed how the build process currently produces a self-contained deployment, which means that the build artifact has everything it needs for the target platform. The Lambda function is configured as a "custom" image, which means it doesn't have anything special. AWS Lambda doesn't know nor care that this is a different version of ASP.NET Core.
At least, that's the case for now. The AWS Lambda team supports LTS versions of .NET Core, which is why they support .NET Core 2.1. They will support .NET Core 3.1, but it will take some time. Once it happens, I'll be able to publish a framework-dependent version of the website, which will reduce the artifact size dramatically and even speed load times. I'll be watching the associated GiHub issue (https://github.com/aws/aws-lambda-dotnet/issues/554).
Summary
Like I said, the upgrade wasn't bad, but it wasn't fun either. But it's what I do as a DevOps engineer. Looking back, I see that I referenced no less than four StackOverflow questions and a couple of official documentation pages. And for a relatively young technology, that's about par for the course.
I'm glad it's done. Now I can get back to working on the comments feature for this blog!
-
If you're still on .NET Core 2.1, and you don't need the features of the newer version, then there's no hurry to upgrade since .NET Core 2.1 is also an LTS version. The full details can be found here.↩
-
Thanks to user Twenty at StackOverflow for this answer.↩
-
Thanks to user Rena at StackOverflow for this answer.↩
-
Except for user VahidN at StackOverflow and this answer.↩