Lesson 5.17: Upgrade Blazor Project and Dependencies to .NET 5

With the release of .NET 5, it is time for us to upgrade our simple-game solution to the latest version of .NET. Luckily .NET 5 is actually the next progression of .NET Core 3.1, so there aren’t many major breaking changes to get this to work. We need to get onto the latest versions because some of our dependencies (like Blazorise and bUnit) have also upgraded to support .NET 5, so we want to get our game running on all of the latest components.

The steps for migrating a Blazor application from ASP.NET Core 3.1 to 5.0 are clearly enumerated in this article. We will follow the exact steps in that article, so we won’t enumerate each step here (though all of the changes will be reflected in the commit for this lesson). We will highlight particularly interesting steps in our migration.

Note: we are will only upgrade our Blazor game solution (simple-rpg-game.sln). At the moment, Azure Functions does not yet support .NET 5, so our services solution will remain on .NET Core 3.1 until Azure Functions provides that.

Upgrade SimpleRPG.Game to .NET 5

First, we will focus on the SimpleRPG.Game project, which needs to move to .NET 5, update dependent packages, and any required changes. And we can still use our game engine library without migrating it because it is built for .NET Standard 2.1. We will upgrade that project as a different step later.

For this project upgrade, we closely follow the ASP.NET Core 3.1 to 5.0 Migration guide.

  1. First, we must edit the SimpleRPG.Game.csproj file directly (not in the project properties window, because we cannot move from .NET Standard 2.1 to .NET 5.0).
  2. Change the project SDK from Microsoft.NET.Sdk.Web to Microsoft.NET.Sdk.BlazorWebAssembly (line #1).
    • <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  3. Change the TargetFramework from ‘netstandard2.1’ to ‘net5.0’ (line #4).
    • <TargetFramework>net5.0</TargetFramework>
  4. Remove the RazorLangVersion line.
  5. Remove the Microsoft.CodeAnalysis.FxCopAnalyzers from our project file. This has moved to a new NetAnalysers in .NET 5, so we don’t need this package any longer.
  6. We may need to reload the solution for all of these changes to take effect.
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
    <Nullable>enable</Nullable>
    <GeneratePackageOnBuild>false</GeneratePackageOnBuild>
    <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
    <AssemblyName>SimpleRPG.Game</AssemblyName>
  </PropertyGroup>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Blazorise.Bootstrap" Version="0.9.2.5" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.3" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.3" PrivateAssets="all" />
    <PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
  </ItemGroup>

  <ItemGroup>
    <Folder Include="wwwroot\images\locations\" />
    <Folder Include="wwwroot\images\monsters\" />
  </ItemGroup>

  <ItemGroup>
    <None Include="..\.editorconfig" Link=".editorconfig" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\SimpleRPG.Game.Engine\SimpleRPG.Game.Engine.csproj" />
  </ItemGroup>

</Project>

Then, we must upgrade the ASP.NET NuGet packages in our project. We will do this by going to the NuGet Package Manager.

  1. Right-click the ‘Dependencies’ folder and click the ‘Manage NuGet Packages’ menu item.
  2. In the Package Manager, switch to the ‘Updates’ tab.
  3. Select the following ASP.NET and Json packages to update:
    • Microsoft.AspNetCore.Components.WebAssembly v5.0.3
    • Microsoft.AspNetCore.Components.WebAssembly.DevServer v5.0.3
    • System.Net.Http.Json v5.0.0
  4. Click the ‘Update’ button.
  5. Approve any notices and licenses that come up along the way.
  6. With that complete, we have the appropriate packages and our csproj file should look like lines #18-20 above.

Next, we have to make a few code and content changes to work with these updates.

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>SimpleRPG.Game</title>
    <base href="/" />

    <!-- Begin: Blazorise required css files -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.12.0/css/all.css">
    <link href="_content/Blazorise/blazorise.css" rel="stylesheet" />
    <link href="_content/Blazorise.Bootstrap/blazorise.bootstrap.css" rel="stylesheet" />
    <!-- End: Blazorise required css files -->

    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="manifest.json" rel="manifest" />
    <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
</head>

<body>
    <div id="app" style="margin: 0 auto; width: 100vw; height: 100vh; background-color: #004A00">
        <div style="text-align:center; color: white; font-size: 16pt; padding-top: 12px">Loading...</div>
        <div style="text-align:center; color: white; margin-top: 12px"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="font-size: 16pt; width: 36px; height: 36px;"></span></div>
        <div style="height:660px; line-height:660px; text-align:center"><img src="/images/SplashScreenLogo.png" style="vertical-align:middle" /></div>
    </div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script>navigator.serviceWorker.register('service-worker.js');</script>
    <script src="game-scripts.js"></script>

    <!-- Begin: Blazorise required script files -->
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>

    <script src="_content/Blazorise/blazorise.js"></script>
    <script src="_content/Blazorise.Bootstrap/blazorise.bootstrap.js"></script>
    <!-- End: Blazorise required script files -->
</body>

</html>
  • The index.html file must update how we specify the app section of the page (line #24). We move from using the <app> tag to use a <div> with an id of “app”.
  • Using the div also allows us to collapse one layer in our UI, so we made a minor padding change as well (line #25).
using Blazorise;
using Blazorise.Bootstrap;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SimpleRPG.Game.Engine.Factories;
using SimpleRPG.Game.Engine.Services;
using SimpleRPG.Game.Engine.ViewModels;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading.Tasks;

namespace SimpleRPG.Game
{
    /// <summary>
    /// Main program class.
    /// </summary>
    [ExcludeFromCodeCoverage]
    public sealed class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);

            builder.Services
              .AddBlazorise(options =>
              {
                  options.ChangeTextOnKeyPress = false;
              })
              .AddBootstrapProviders();

            builder.RootComponents.Add<App>("#app");
            builder.Services.AddScoped(sp => new HttpClient
            {
                BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
            });

            // add app-specific/custom services and view models here...
            ConfigureAppServices(builder.Services);

            var host = builder.Build();
            host.Services
              .UseBootstrapProviders();

            // initialize app-specific/custom services and view models here...
            await InitializeAppServices(host.Services).ConfigureAwait(false);

            await host.RunAsync().ConfigureAwait(false);
        }

        private static void ConfigureAppServices(IServiceCollection services)
        {
            // add app-specific/custom services here...
            services.AddSingleton<IDiceService>(DiceService.Instance);
            services.AddSingleton<IGameSession, GameSession>();
            services.AddTransient<TraderViewModel>();
        }

        private static async Task InitializeAppServices(IServiceProvider serviceProvider)
        {
            // add service initialization here...
            var httpClient = serviceProvider.GetRequiredService<HttpClient>();
            await FactoryLoader.Load(httpClient).ConfigureAwait(false);
        }
    }
}
  • The Program.Main method must update to find the new root component (line #32). Since “app” is now an element id, it needs to be prepended with #.
  • And, we must update how we register HttpClient in our dependency injection container (line #33).
@inherits LayoutComponentBase

<div class="page">
    @Body
</div>

The MainLayout.razor file needs to update the class for the main content area to “page” (line #3).

@page "/"
@inject IJSRuntime jsRuntime
@inject IGameSession ViewModel

<div @onkeydown="@KeyDown" tabindex="0" @ref="pageRoot">
    <Row Margin="Margin.Is0" Style="height: 5vh; min-height: 32px">
        <Column ColumnSize="ColumnSize.Is12" Style="background-color: lightgrey">
            <Heading Size="HeadingSize.Is3">Simple RPG Online</Heading>
        </Column>
    </Row>
    <Row Margin="Margin.Is0" Style="height: 60vh">
        <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12" Style="background-color: aquamarine">
            <PlayerComponent Player="@ViewModel.CurrentPlayer" />
        </Column>
        <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12" Style="background-color: beige">
            <Row Margin="Margin.Is2.OnY">
                <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12">
                    <DisplayMessageListView Messages="@ViewModel.Messages" />
                </Column>
                <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12">
                    <LocationComponent Location="@ViewModel.CurrentLocation" />
                    <MonsterComponent Monster="@ViewModel.CurrentMonster" />
                    <TraderComponent Trader="@ViewModel.CurrentTrader" Player="@ViewModel.CurrentPlayer"
                                     InventoryChanged="@StateHasChanged" />
                </Column>
            </Row>
        </Column>
    </Row>
    <Row Margin="Margin.Is0" Style="height: 33vh">
        <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12" Padding="Padding.Is2.OnY"
                Style="background-color: burlywood">
            <PlayerTabs Player="@ViewModel.CurrentPlayer" CraftItemClicked="@ViewModel.CraftItemUsing"
                        DisplayMessageCreated="@ViewModel.AddDisplayMessage" />
        </Column>
        <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12" Style="background-color: lavender">
            <Row Margin="Margin.Is2.OnY">
                <Column ColumnSize="ColumnSize.Is8.OnWidescreen.Is12">
                    <CombatComponent WeaponList="@ViewModel.CurrentPlayer.Inventory.Weapons"
                                     AttackClicked="@ViewModel.AttackCurrentMonster"
                                     LocationHasMonster="@ViewModel.HasMonster"
                                     ConsumableList="@ViewModel.CurrentPlayer.Inventory.Consumables"
                                     ConsumeClicked="@ViewModel.ConsumeCurrentItem" />
                </Column>
                <Column ColumnSize="ColumnSize.Is4.OnWidescreen.Is12">
                    <MovementComponent Movement="@ViewModel.Movement"
                                       LocationChanged="@ViewModel.OnLocationChanged" />
                </Column>
            </Row>
        </Column>
    </Row>
</div>

@code {
    protected ElementReference pageRoot;  // set the @ref for attribute

    protected async override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await jsRuntime.InvokeVoidAsync("SetFocusToElement", pageRoot);
        }
    }

    protected override void OnInitialized()
    {
        DisplayMessageBroker.Instance.OnMessageRaised += OnGameMessageRaised;
    }

    [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
    private void KeyDown(KeyboardEventArgs args) =>
        ViewModel.ProcessKeyPress(args.ToKeyProcessingEventArgs());

    private void OnGameMessageRaised(object? sender, DisplayMessage message) =>
        ViewModel.AddDisplayMessage(message);
}

The MainScreen.razor file needs to update an event handler for better nullability support (line #73).

@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');

html, body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

a, .btn-link {
    color: #0366d6;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

#app {
    position: relative;
    display: flex;
    flex-direction: column;
}

.top-row {
    height: 3.5rem;
    display: flex;
    align-items: center;
}

.page {
    position: relative;
    display: flex;
    flex-direction: column;
    padding-top: 0.5rem;
}

.main {
    flex: 1;
}

    .main .top-row {
        background-color: #f7f7f7;
        border-bottom: 1px solid #d6d5d5;
        justify-content: flex-end;
    }

        .main .top-row > a, .main .top-row .btn-link {
            white-space: nowrap;
            margin-left: 1.5rem;
        }

.main .top-row a:first-child {
    overflow: hidden;
    text-overflow: ellipsis;
}

.sidebar {
    background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}

    .sidebar .top-row {
        background-color: rgba(0,0,0,0.4);
    }

    .sidebar .navbar-brand {
        font-size: 1.1rem;
    }

    .sidebar .oi {
        width: 2rem;
        font-size: 1.1rem;
        vertical-align: text-top;
        top: -2px;
    }

    .sidebar .nav-item {
        font-size: 0.9rem;
        padding-bottom: 0.5rem;
    }

        .sidebar .nav-item:first-of-type {
            padding-top: 1rem;
        }

        .sidebar .nav-item:last-of-type {
            padding-bottom: 1rem;
        }

        .sidebar .nav-item a {
            color: #d7d7d7;
            border-radius: 4px;
            height: 3rem;
            display: flex;
            align-items: center;
            line-height: 3rem;
        }

            .sidebar .nav-item a.active {
                background-color: rgba(255,255,255,0.25);
                color: white;
            }

            .sidebar .nav-item a:hover {
                background-color: rgba(255,255,255,0.1);
                color: white;
            }

.content {
    padding-top: 0.5rem;
}

.navbar-toggler {
    background-color: rgba(255, 255, 255, 0.1);
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid red;
}

.validation-message {
    color: red;
}

#blazor-error-ui {
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

@media (max-width: 767.98px) {
    .main .top-row:not(.auth) {
        display: none;
    }

    .main .top-row.auth {
        justify-content: space-between;
    }

    .main .top-row a, .main .top-row .btn-link {
        margin-left: 0;
    }
}

@media (min-width: 768px) {
    app {
        flex-direction: row;
    }

    .sidebar {
        width: 250px;
        height: 100vh;
        position: sticky;
        top: 0;
    }

    .main .top-row {
        position: sticky;
        top: 0;
    }

    .main > div {
        padding-left: 0.5rem !important;
        padding-right: 0.5rem !important;
    }

    .navbar-toggler {
        display: none;
    }

    .sidebar .collapse {
        /* Never collapse the sidebar for wide screens */
        display: block;
    }
}

/* Add game-specific styles here... */
.my-custom-scrollbar {
    position: relative;
    height: 25vh;
    overflow: auto;
}

.table-wrapper-scroll-y {
    display: block;
}

Finally, there were a couple of CSS changes made to app.css to correspond with name/id changes in our source files.

After completing these changes, we can build our SimpleRPG.Game project. We can also run our game at this point locally to ensure it works as expected.

Upgrade Blazorise Package to 0.9.2.5

While we are upgrading our Blazor project, we are taking this opportunity to also upgrade to the latest Blazorise package. Remember that Blazorise is a component library that provides useful controls and encapsulations of raw HTML/CSS/JavaScript elements. There are some minor updates with this version, but most importantly it is updated to target .NET 5 too.

Going back to the NuGet Package Manager for this project, we update the Blazorise package to version 0.9.2.5.

There was a minor change to how we specify button sizes, which required 3 changes to the PlayerTabs.razor and TraderComponent.razor files: Size="ButtonSize.Small" must be changed to Size="Size.Small".

With this updated, we can build and verify our game once again.

Upgrade SimpleRPG.Game.Tests Project

With our game project upgraded and building, it is time to upgrade our SimpleRPG.Game.Tests project. This project inherits the dependency on .NET 5 and package updates, so there are several changes needed in response to that.

First, we need to make similar changes to the SimpleRPG.Game.Tests.csproj file to upgrade versions and packages.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>

    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="bunit" Version="1.0.0-preview-01-gfe6f6e0ac0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
    <PackageReference Include="Moq" Version="4.16.0" />
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="coverlet.collector" Version="3.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\SimpleRPG.Game\SimpleRPG.Game.csproj" />
  </ItemGroup>

</Project>

The important change there is to update the framework version again to ‘net5.0’ (line #4). We will cover the package updates in the coming steps.

With the changes to Blazorise, we have one breaking change to fix in our test project. The TestServiceProviderExtensions class needs to update the BlazoriseOptions constructor (line #23).

using Blazorise;
using Blazorise.Bootstrap;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Moq;

namespace SimpleRPG.Game.Tests.Mocks
{
    public static class TestServiceProviderExtensions
    {
        public static void AddBlazoriseServices(this TestServiceProvider services)
        {
            var mockJsRuntime = new Mock<IJSRuntime>().Object;

            services.AddSingleton<IClassProvider>(new BootstrapClassProvider());
            services.AddSingleton<IStyleProvider>(new BootstrapStyleProvider());
            services.AddSingleton<IJSRunner>(new BootstrapJSRunner(mockJsRuntime));
            services.AddSingleton<IJSRuntime>(mockJsRuntime);
            services.AddSingleton<IComponentMapper>(new ComponentMapper());
            services.AddSingleton<IThemeGenerator>(new BootstrapThemeGenerator());
            services.AddSingleton<IIconProvider>(new Mock<IIconProvider>().Object);
            services.AddSingleton<BlazoriseOptions>(new BlazoriseOptions(services, null));
        }
    }
}

Before we can fix any more issues, we need to upgrade bUnit testing framework package.

Upgrade bUnit Package to Preview-01

With the move to .NET 5, we also need a new version of the bUnit test package. We use bUnit to support test context and services for Blazor components.

To update the package, we need to go to the NuGet Package Manager for this test project. Then update the bUnit package to version=1.0.0-preview-01-gfe6f6e0ac0 or greater. This component is still in preview, but this version supports .NET 5.

After getting the latest bUnit package, when we run our tests, we get a lot of failures. Because of some fixes in the HTML emitters in Blazorise, our expected HTML no longer matches what is actually produced. The fixes stopped emitting empty attributes for class and style when none were specified in the components. This helps reduce our HTML and keep it clean. But it does mean we have to update our failing tests. You can see all of the detailed changes to our tests in the commit for this lesson.

Upgrade Remaining Packages

The remaining packages are for the test infrastructure, xUnit, Moq, and code coverage components. We’re not required to update those packages, but we may as well get current on all of them while we are doing this work.

Return to the NuGet Package Manager for this project and upgrade the remaining packages. Once they complete, we will build the test project, run all of the tests, and verify that they all work as expected. With our tests green again, we are confident that our game functionality is working, so we are going to commit our changes as Lesson 5.17A. The migration is a lot of small changes so we want to make sure we’re saving our work along the way.

Upgrade SimpleRPG.Game.Engine to .NET 5

Although we could continue to use our game engine as a .NET Standard library in our Blazor game, we also want to update the game engine project. We may as well do all the migrations in one step rather than waiting to do it later.

First, we need to update the SimpleRPG.Game.Engine.csproj file in the editor:

<Project Sdk="Microsoft.NET.Sdk">

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

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="d20tek-dicenotation" Version="3.2.3" />
    <PackageReference Include="Microsoft.AspNetCore.Blazor.HttpClient" Version="3.2.0-preview3.20168.3" />
    <PackageReference Include="Microsoft.AspNetCore.Components" Version="5.0.3" />
  </ItemGroup>

</Project>

Again, we need to change the TargetFramework to net5.0 (line #4).

For our game engine, we need to upgrade our ASP.NET NuGet package to the .NET 5 version. Let’s launch the NuGet Package Manager for this project (on the context menu for the ‘Dependencies’ node).

Fig 1 – Game Engine Package Upgrades
  • Select the Microsoft.AspNetCore.Components package to v5.0.3 (line # 15 above).
  • Click the ‘Update’ button to install that version.
  • Click ‘OK’ on any preview and license dialogs.
  • We also will switch the DiceNotation package. The current one (D20Tek.DiceNotation.Standard) is for .NET Standard. We also have a .NET 5 version, so install d20tek-notation in its place (line #13 above).

Due to the Nullability setting in our project, there are behavior changes between .NET Core 3.1 and .NET 5, and some code changes are also necessary to the game engine library. Let’s look at these small changes.

1. The NamedElement class Equals methods need to be able to accept null (line #33 and #43).

using System;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public abstract class NamedElement<T> : IEquatable<NamedElement<T>>
        where T : struct
    {
        private string _name = string.Empty;

        protected NamedElement(T id, string name)
        {
            Id = id;
            Name = name;
        }

        protected NamedElement()
        {
        }

        public T Id { get; set; }

        public string Name
        {
            get => _name;
            set => _name = !string.IsNullOrEmpty(value) ? value : throw new ArgumentNullException(nameof(Name));
        }

        public override string ToString()
        {
            return $"{Name} [{Id}]";
        }

        public bool Equals(NamedElement<T>? other)
        {
            if (other is null)
            {
                return false;
            }

            return Id.Equals(other.Id);
        }

        public override bool Equals(object? obj)
        {
            if (obj is NamedElement<T> element)
            {
                return Equals(element);
            }

            return false;
        }

        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }
    }
}

2. The Inventory.RemoveItem method needs a nullable GroupedInventoryItem (line #68).

        public void RemoveItem(GameItem item)
        {
            _ = item ?? throw new ArgumentNullException(nameof(item));

            _backingInventory.Remove(item);

            if (item.IsUnique == false)
            {
                GroupedInventoryItem? groupedInventoryItemToRemove =
                    _backingGroupedInventory.FirstOrDefault(gi => gi.Item.ItemTypeID == item.ItemTypeID);

                if (groupedInventoryItemToRemove != null)
                {
                    if (groupedInventoryItemToRemove.Quantity == 1)
                    {
                        _backingGroupedInventory.Remove(groupedInventoryItemToRemove);
                    }
                    else
                    {
                        groupedInventoryItemToRemove.Quantity--;
                    }
                }
            }
        }

3. The GameServiceClient.GetEntityById method needs to deal with null deserialization result (line #58).

        public async Task<T> GetEntityById(TId id)
        {
            string fullUrl = $"{_serviceUrl}/{id}";
            var response = await _httpClient.GetAsync(fullUrl).ConfigureAwait(false);
            if (response.IsSuccessStatusCode)
            {
                string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                var result = JsonSerializer.Deserialize<T>(json, this.options) ?? throw new InvalidOperationException();
                return result;
            }
            else if (response.StatusCode == HttpStatusCode.NotFound)
            {
                throw new EntityNotFoundException("Id", id);
            }
            else
            {
                throw new HttpRequestException(
                    $"Response status code does not indicate success: {response.StatusCode}.");
            }
        }

4. The GameSession.CompleteQuestsAtLocation method needs to support nullable quest (line #175).

        private void CompleteQuestsAtLocation()
        {
            foreach (Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                QuestStatus? questToComplete =
                    CurrentPlayer.Quests.FirstOrDefault(q => q.PlayerQuest.Id == quest.Id &&
                                                             !q.IsCompleted);

                if (questToComplete != null)
                {
                    if (CurrentPlayer.Inventory.HasAllTheseItems(quest.ItemsToComplete))
                    {
                        // Remove the quest completion items from the player's inventory
                        CurrentPlayer.Inventory.RemoveItems(quest.ItemsToComplete);

                        // give the player the quest rewards
                        var messageLines = new List<string>();
                        CurrentPlayer.AddExperience(quest.RewardExperiencePoints);
                        messageLines.Add($"You receive {quest.RewardExperiencePoints} experience points");

                        CurrentPlayer.ReceiveGold(quest.RewardGold);
                        messageLines.Add($"You receive {quest.RewardGold} gold");

                        foreach (ItemQuantity itemQuantity in quest.RewardItems)
                        {
                            GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemId);

                            CurrentPlayer.Inventory.AddItem(rewardItem);
                            messageLines.Add($"You receive a {rewardItem.Name}");
                        }

                        AddDisplayMessage($"Quest Completed - {quest.Name}", messageLines);

                        // mark the quest as completed
                        questToComplete.IsCompleted = true;
                    }
                }
            }
        }

With these code changes, we can build the SimpleRPG.Game.Engine project successfully again.

Upgrade SimpleRPG.Game.Engine.Tests Project

First, we need upgrade the SimpleRPG.Game.Tests.csproj file to .NET 5 as well (line #4).

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>

    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <None Remove="Data\testdata-invalid.json" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="Data\testdata-invalid.json" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
    <PackageReference Include="Moq" Version="4.16.0" />
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="coverlet.collector" Version="3.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\SimpleRPG.Game.Engine\SimpleRPG.Game.Engine.csproj" />
  </ItemGroup>

</Project>

Then, we must update packages for the test infrastructure, xUnit, Moq, and code coverage components. We’re not required to update those packages, but we will upgrade them now too. Return to the NuGet Package Manager for this project and update the packages.

Once the updates complete, we will build the test project, run all of the tests, and verify that they all work as expected. At this point we can run our game locally and try out all of the features to see everything running on .NET 5.

Build Script Change

With .NET 5 migration complete, we also need to change our azure-pipelines.yml to require the latest .NET version (line #12).

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml

trigger:
- '*'

# all of the variables used by this pipeline and dependent templates.
variables:
  buildConfiguration: 'Release'
  dotnetSdkVersion: '5.0.x'
  releaseBranchName: 'main'
  localPackageFeed: 'd20Tek'

# define the image to use for the whole pipeline... can be overridden by specific jobs.
pool:
    vmImage: 'ubuntu-16.04'

stages:
# Build
- stage: 'Build'
  displayName: 'Build app'
  jobs:
  - job: 'Build'
    displayName: 'Build job'

    steps:
    # ensure the right version of .NET Core is installed -- defaults to 3.1.
    - task: UseDotNet@2
      displayName: 'Use .NET Core SDK $(dotnetSdkVersion)'
      inputs:
        version: '$(dotnetSdkVersion)'

    # restore NuGet packages used by the projects.
    - task: DotNetCoreCLI@2
      displayName: 'Restore project dependencies'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'
        feedsToUse: 'select'
        vstsFeed: '$(localPackageFeed)'

    # build all projects in this repo... defined by folders with .csproj files.
    - task: DotNetCoreCLI@2
      displayName: 'Build the project - $(buildConfiguration)'
      inputs:
        command: 'build'
        arguments: '--no-restore --configuration $(buildConfiguration)'
        projects: '**/*.csproj'
        versioningScheme: byBuildNumber
    
    # publish all artifacts from the builds.
    - task: DotNetCoreCLI@2
      displayName: 'Publish the project - $(buildConfiguration)'
      inputs:
        command: 'publish'
        projects: '**/*.csproj'
        publishWebProjects: false
        arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/$(buildConfiguration)'
        zipAfterPublish: false

    # runs tests for all projects in this repo... defined by folders with .Test.csproj files.
    - task: DotNetCoreCLI@2
      displayName: 'Run unit tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --no-restore --configuration $(buildConfiguration)'
        # publish the test pass/fail results to the pipeline, so that they are available in the Azure DevOps pipeline dashboard.
        publishTestResults: true
        projects: '**/*.Tests.csproj'

    # publish the artifacts created by this build in the drop location.
    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'
      condition: succeeded()

We will commit these set of changes to Git labeled as 5.17B. As these changes are submitted to our DevOps server, the automated build will run with the appropriate .NET version. By following the pull request and release steps in Lesson 5.16, we should release the SimpleRPG game again.

In conclusion, these were a lot of small changes, but being on .NET 5 means we’re on the latest version and can continue to take updates as they come. With this release Blazor should be fairly stable, so we shouldn’t see many breaking changes updating to newer versions (at least not until we get to .NET 6).

Note: The majority of our code works whether it’s .NET Core 3.1 or .NET 5. But from this lesson forward, we will expect .NET 5 as our target platform.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s