Jason Sultana

Follow this space for writings (and ramblings) about interesting things related to software development.

Upgrading from .NET Core 2.1 to .NET 6.0

30 Dec 2021 » dotnet

G’day guys!

Hope you all had a Merry Christmas! Today, since I’m in that strange limbo place between Christmas and New Years, I decided to migrate one of my old hobby projects from .NET 2.1 to .NET 6, and document the experience here. If any of you are perhaps planning on performing a similar upgrade yourself, you might find this a useful read before getting started.

If it ain’t broke, don’t fix it?

I guess the first question that must be asked is, so long as the application is working, why bother doing an upgrade at all? It’s a good question, and there are a few reasons for this:

Which version should I upgrade to?

I think this is a very good question, since there are usually at least 2-3 reasonable choices at any given time. I’d probably recommend looking at the currently supported .NET versions, take note of when each of them will reach End of Support, and then decide accordingly. I usually opt to choose an LTS (Long-Term-Support) option where possible, so I don’t have to worry about another upgrade for a couple of years, hopefully. Currently, the latest LTS version is .NET 6, which will be supported until November 2024.

Installing .NET 6

First, let’s find out if you currently have the .NET 6 SDK installed on your machine. An easy way to do this is to run the following in your favourite terminal:

dotnet --list-sdks

If you don’t see an entry for .NET 6, you’ll need to install it. You could do this using Visual Studio, but just in case you happen to be using a different code editor or IDE (vim perhaps?), here’s a link where you can download an installer from directly.

After installing the new SDK, the previous command should now include 6.0.101 (or whichever version you just installed).

Upgrading projects

After installing the SDK, the next step is to update our TargetFramework in each csproj file. If you’re migrating from .NET Core 2.1, this will likely be set to netcoreapp2.1. Change it in each project to:

    <TargetFramework>net6.0</TargetFramework>

After saving, it’s highly likely that you’ll encounter 1000 errors and every file in your solution will be granted an honorary red squiggly underscore. To fix these issues, we’ll need to start by upgrading your solution packages to .NET 6-compatible versions.

Upgrading solution

If you have a global.json file in your solution, you’ll want to upgrade the sdk property. Eg:

    "sdk": {
        "version": "6.0.0"
    }

You can read more about global.json here. Essentially, this value is just used for CLI tools, but it makes sense to keep this set to the same version as defined in the projects themselves.

Upgrading packages

If you’re using Visual Studio (or Rider), right-click on your Solution and click Manage nuget packages. Specifically, we’re interested in packages which have an update available. You could try upgrading these one at a time starting with the Microsoft packages, or you can just YOLO it and upgrade everything at once, and deal with any issues that arise post-upgrade. Up to you how you prefer to tackle this step. Since this was just a hobby project, I just upgraded all the packages at once.

Upgrading MSBuild version in IDE

This step may only be applicable for Rider users. In my case, I needed to upgrade my MSBuild directory under Preferences -> Toolset and build as described here. You’ll likely want to choose the directory with the highest MSBuild version. Visual Studio users can probably skip this step.

After completing this step, I noticed the vast majority of my errors disappear. Note that you may also need to clean and rebuild your solution.

Microsoft.AspNetCore.App warning

  Microsoft.NET.Sdk.DefaultItems.Shared.targets(111, 5): [NETSDK1080] A PackageReference to Microsoft.AspNetCore.App is not necessary when targeting .NET Core 3.0 or higher. If Microsoft.NET.Sdk.Web is used, the shared framework will be referenced automatically. Otherwise, the PackageReference should be replaced with a FrameworkReference.

This is more of a warning than an error, but it’s still good to address this. Find any existing package reference to Microsoft.AspNetCore.App and remove it, since it’s now redundant. This will likely be in your top-level API projects.

Entity Framework breaking changes

1. builder.UseMySql(string) no longer exists.

This may or may not affect other SQL variants, but the UseMysql(string) extension method on DbContextOptionsBuilder now requires a second parameter, which specifies the server version. An easy fix just to get things compiling again is to auto-detect the version. Eg: builder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));.

2. Relational() missing from IMutableProperty

If you were setting an explicit database column type (eg for decimal precision), you may have noticed that Relational() is missing from IMutableProperty. You can replace this with a call to SetColumnType(string) instead.

3. String.Format cannot be translated to SQL

In .NET Core 2.1, I had some LINQ-To-SQL queries that looked like:

await _dbContext.SomeDbSet
    .Where(a => a.SomeID == id)
    .Select(a => new
    {
        a.SomeID,
        // ...

        Name = a.NavigationProperty == null ?
            "" :
            $"{a.NavigationProperty.FirstName} {a.NavigationProperty.LastName}"

In .NET 6 (or somewhere along the line), this stopped being supported, so I need to retrieve both values and perform the concatenation in-code after the query results are returned.

4. Unable to return nested queryables

I had a data structure that looks like:

    private class ListItemsQuery
    {
        public Item Item { get; set; }
        public IQueryable<SubItem> SubItems { get; set; }
    }

And in .NET Core 2.1, I was able to return an IQueryable<ListItemsQuery> which itself contains an IQueryable<SubItem> property. In .NET 6 though, this was producing the following:

The query contains a projection '<>h__TransparentIdentifier1 => <>h__TransparentIdentifier1.a' of type 'IQueryable<SubItem>'. Collections in the final projection must be an 'IEnumerable<T>' type such as 'List<T>'. Consider using 'ToList' or some other mechanism to convert the 'IQueryable<T>' or 'IOrderedEnumerable<T>' into an 'IEnumerable<T>'.

Changing the nested IQueryable to a List worked fine - though it means that part of the query does need to be materialised earlier than it was before. There were a couple of other complex LINQ queries that needed to be fixed up as well.


Swagger breaking changes

5. Cannot resolve symbol Info

Simply change to OpenApiInfo.

6. Cannot resolve symbol ApiKeyScheme

Simply change to OpenApiSecurityScheme. However, the In and Type properties also have breaking changes.

In was previously a string, and is now a type of ParameterLocation. Replace the string "header" with ParameterLocation.Header. Type was also previously a string, and is now a type of SecuritySchemeType. Replace the previous string with SecuritySchemeType.Http, or other value as appropriate.

7. DocExpansion signature change

This changed from a string to a DocExpansion enum value.

8. SwaggerResponse attribute missing

This can simply be replaced with ProducesResponseType.

9. SwaggerOperation attribute missing

Basically, all of the Swagger-specific attributes were extracted to Swashbuckle.AspNetCore.Annotations. Often, these aren’t needed and you can get away with just using .NET Core attrubutes (like ProducesResponseType), but you can install the swashbuckle annotations package if you want to.


Startup breaking changes

10. CorsAuthorizationFilterFactory missing

We can simply remove the Mvc filter.

11. CORS Policy changes

If you were using CorsPolicyBuilder with a very open policy, allowing both a header and credentials, you’ll likely receive something similar to the following exception:

Application startup exception: System.InvalidOperationException: The CORS protocol does not allow specifying a wildcard (any) origin and credentials at the same time. Configure the CORS policy by listing individual origins if credentials needs to be supported.

This can be easily resolved by removing either the call to AllowCredentials or the call to AllowAnyOrigin, depending on your circumstances. Eg:

    var corsBuilder = new CorsPolicyBuilder();
    corsBuilder.AllowAnyHeader();
    corsBuilder.AllowAnyMethod();
    corsBuilder.AllowAnyOrigin(); 
    //corsBuilder.AllowCredentials();

12. IHostingEnvironment obsolete

Simply replace with IWebHostEnvironment.

13. MVC Changes

Replace AddMvc() with AddControllers() (assuming you have an API project). The difference is discussed in a bit more detail here.

You’ll also want to replace your app.UseMvc() line with:

    app.UseRouting();
    app.UseEndpoints(opts =>
    {
        opts.MapControllers();
    });

This is basically for performance reasons, and consistency with newer code samples and practices. UseRouting() must appear before UseEndpoints() to avoid an exception on startup.


Miscellaneous

14. NodeServices GONE

It looks like though a lot of developers were using NodeServices to invoke custom NodeJS logic from C#, the main purpose of NodeServices was actually for SSR of SPAs served from .NET APIs. Since the landscape of most SPAs has now changed to include command line tooling (eg: Angular, React), NodeServices has been removed. This is discussed in a bit more detail here. At this stage, it looks like the simplest replacement for NodeServices is a 3rd-party package called Javascript.NodeJS, though it does seem to be well maintained.

The instead of injecting INodeServices, you inject INodeJSService. The API is quite similar, but the internals do have some pretty big differences. In my case, I was using a NodeJS library to render reports, so I was passing in an HTML template string and expecting back a byte array. This doesn’t play well out-of-the-box with Javascript.NodeJS, as it expects the input to be a JSON string. You can get it working, but it does take some tweaking.


15. Incompatible packages

So far, only MSBump, though this has since been archived by the project maintainer anyway.


16. ServiceFilter stack overflow

Quite similar to this post, I was previously using service filters to create filters that would support controller dependencies. According to the reply from Microsoft, they recommend implementing an IFilterFactory, but I found that simply removing ServiceFilterAttribute from the attribute implementation solved the problem. i.e - simply have your attribute implement IAsyncAuthorizationFilter or IAsyncActionFilter as needed, without extending ServiceFilterAttribute, which was the recommended approach in .NET Core 2.1 (or at least it worked in 2.1).

17. Model Binding

It looks like numbers can no longer be implicitly deserialised as strings. Eg given the following data model:

    public class AddressModel
    {
        [StringLength(4)]
        public string PostCode { get; set; } = "";
    }

and the following payload:

{ "postCode":2145 }

This results in the following 400 response:

{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"00-9d782bbecbc2107262258e79a7df080e-8d29b1c98b0ea23e-00","errors":{"$.address.postCode":["The JSON value could not be converted to System.String. Path: $.address.postCode | LineNumber: 0 | BytePositionInLine: 151."]}}

This looks like a side-effect of .NET Core 3 swapping out Newtonsoft.Json for System.Text.Json as the default JSON serialiser. Installing Microsoft.AspNetCore.Mvc.NewtonsoftJson and adding AddNewtonsoftJson() to your existing AddControllers() line in Startup::ConfigureServices() appears to fix this. If you were using .NET Core 2.1, Newtonsoft.Json was the default JSON serialiser then anyway, so it might make sense to keep this consistent anyway just to prevent little issues like this from popping up.

Anyway, that was pretty much it! It took me about an hour to do the upgrade and then a few hours to fix the issues that cropped up. The biggest breaking change was by far the dropping of Node Services, but there were also some Entity Framework and JSON serialisation changes to be on the look out for. Long story short, the upgrade itself doesn’t take long - but you should be prepared to spend some time catching and fixing little things that broke as a result of the upgrade.

Have you noticed anything else that broke or changed significantly between .NET Core 2.1 and .NET 6? Let me know in the comments! Have a happy new year, and see you in 2022!

Catch ya!