Few Steps to Migrate Your .NET Core 3.1 to .NET 5 – Windows ASP.NET Core Hosting 2024 | Review and Comparison

In this article I will explain steps needed to upgrade project to .NET 5, EF Core 5.0 and C# 9.0 on real life project.

First thing needed is Visual Studio 16.8. I will not work in earlier versions of VS. Next, download .NET 5 SDK and Runtime from https://dotnet.microsoft.com/download/dotnet/5.0.

Once you open your projects inside new Visual Studio first thing needed is to update Target framework moniker inside your csproj file. So instead of netcoreapp3.1 it should read .net5.0 :

<PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>  
    <Nullable>enable</Nullable>
     <WarningsAsErrors></WarningsAsErrors>
  </PropertyGroup>

In my case I have nullable and warning as errors enabled. You may want to have different settings but I suggest them also.

If your project target some window specific API you will get build error:

So you will need to change target framewoel modifier to net5.0-windows( or even net5.0-windows7.0, depending on what compiler will tell you)

Also, you cannot reference net5.0 project from .netcoreapp3.1 project. I found those errors little bit strange because even after adding net5.0-windows to all projects I still received those errors. After closing visual studio, nuget cache rebuild and solution rebuild those errors went away.

Now, other errors showed up. In my case most of them were related to some nullable warnings that were not there previously. This is because .NET 5 comes with code analysis built into .NET 5 SDK. Before you would need to install some nuget packages. Also, .NET 5 introduced some thing called AnalysisLevel New default is AnalysisLevel 5 which will introduce more errors but hopefully safer code. You can read more about .NET 5 code analysis features here: https://devblogs.microsoft.com/dotnet/automatically-find-latent-bugs-in-your-code-with-net-5/

Now you can update dependencies also. Biggest dependency in my app is EF Core and later it will explained what features EF Core 5.0 brings. For now you can update dependencies using Manage Nuget packages command.

One nice tool I use to see at one screen what dependencies needs to be updates is dotnet outdated. If yuu don’t have it installed use this command to install the tool. dotnet tool install --global dotnet-outdated-tool. After that if you run dotnet outdated you should see something similar to this:

Of course before going any further it is good idea to run your unit and integration tests.

One one the biggest changes EF Core 5.0 introduced is that is not anymore necessary to define join table for many-to-many relationships.

For example, if we have post and tags entities it was necessary to define entities like this:

public class Post{
     public int Id { get; set; }
     public ICollection<PostTag> PostTags { get; set; }

}
public class Tag {
     public int Id { get; set; }
     public ICollection<PostTag> PostTags { get; set; }

}
public class PostTag {
     public int PostId{ get; set; }
     public int TagId{ get; set; }
     public Post Post{ get; set; }
     public Tag Tag{ get; set; }
}
and in OnModelCreating have this kinf of configuration:

     _ = modelBuilder.Entity<PostTag>()
                 .HasKey(bc => new { bc.PostId, bc.TagId});

            _ = modelBuilder.Entity<PostTag>()
                .HasOne(bc => bc.Post)
                .WithMany(b => b.PostTags)
                .HasForeignKey(bc => bc.PostId);

            _ = modelBuilder.Entity<PostTag>()
                .HasOne(bc => bc.Tag)
                .WithMany(c => c.PostTags)
                .HasForeignKey(bc => bc.TagId);

Now, this kind of realtionship can be expressed in more natural way like this:

public class Post{
     public int Id { get; set; }
     public ICollection<Tag> Tags{ get; set; }

}
public class Tag {
     public int Id { get; set; }
     public ICollection<Post> Posts { get; set; }

}

Also, configuration inside OnModelCreating is much simpler:

 _ = modelBuilder.Entity<Post>().HasMany(x => x.Tags).WithMany(x => x.Posts);

And it is necessary to define relationships only from one direction.

How seed data is now defined? In .NET Core 3.1 we used to have

 var postTags= new List<PostTag>
       {
         new PostTag{Post=post1, Tag=tag1},
         new PostTag{Post=post2, Tag=tag2},
         new PostTag{Post=post3, Tag=tag3},
         new PostTag{Post=post4, Tag=tag4},
         new PostTag{Post=post5, Tag=tag5},
        };
            db.CANNetworkECUs.AddRange(canNetworkEcus);
       db.SaveChanges();

Now, since we don’t need PostTag class anymore we can define tags that belong to post as list property of that class or other way around. For example,

Post p1= new Post 
        {  
            Tags = new List<Tag> { tag1 }
        };

tag1 is instance of class Tag and it is defined and also inserted before PostTag relation is inserted. All you need to do is to add list of tags to post and EF will automatically insert entries in join table.

Another change introduced with .NET Core 5.0 is that Owned entities are not always nullable. In .NET Core 3.1 if you had not nullable strings for example inside class (and even if you specify them as required in OnModelCreating) it would result in nullable columns in database.

For example,

public class ServiceShop 
    {
        public ServiceShop()
        {
            Technicians = new List<User>();
        }
        public Address Address { get; set; } = default!;
    }
 [Owned]
    public class Address
    {
        [MaxLength(250)]
        public string? StreetAddress { get; set; } = default!;

        [MaxLength(250)]
        public string? City { get; set; } = default!;

        [MaxLength(250)]
        public string? State { get; set; } = default!;

        [MaxLength(250)]
        public string? ZipCode { get; set; } = default!;
    }

Here Address is Owned type and if you have for example StreetAddress marked as nullable it will be nullable in database, and if it was not null it would be not null in database. In .NET Core 3.1 was not that way.

This change I discovered by doing some migration just to see what it will be changed after running it. Once you have empty migration that would mean that upgrading to .NET Core 5.0 did not introduced any changes to your database that you are not aware of.