Lesson 2.5: Using Blazor Dependency Injection

Dependency injection (DI) is a software design pattern that removes hard-coded dependencies from an object and makes it possible to change those dependencies, whether at run-time or compile-time. This pattern can be used as a simple way to load plugins dynamically or to choose stubs/mock objects in test environments vs. real objects in production environments.

The Theory

This design pattern injects the depended-upon element into the destination automatically by knowing the requirements of the destination object. This way our objects are not responsible for creating one another and delegate that responsibility to an external service – usually referred to as the container.

There are several forms of DI including constructor and property injection:

Constructor Injection: When we supply the dependency object through the public class constructor with the dependencies passed in as parameters.

Property Injection: When we supply the dependency object through the public property of the destination class.

Dependency Injection is intended to be used when we wish to:

  • Inject configuration data
  • Inject the same dependency into multiple components
  • Inject different implementations of the same dependency
  • Inject the same implementation in different configurations
  • Use Container services

Blazor contains built-in Dependency Injection support (shared with ASP.NET Core). During startup, Blazor provides a builder service that we use to register and configure our types. Then, when those types are requested during application execution, Blazor DI will create and provide those types as needed.

Let’s try a real example of using this pattern. In our MainScreen.razor page, we created a hard-coded instance of our GameSession view model. This is the type of tight-coupling (page constructing view model) that DI is intended to remove. This type of coupling makes it difficult to replace the view model or try to unit test the MainScreen page in isolation. We are stuck with the implementation of GameSession that the page created. Using Dependency Injection rather than creating it, the view would be provided an instance of the GameSession view model in our page. And the page would not know where it came from or how it was constructed.

Here’s a quick overview of how we would go about removing this hard-coded dependency:

  1. Remove the creation of GameSession ViewModel property from MainScreen.razor page.
  2. Register our GameSession type with the DI services.
  3. Use @inject directive to retrieve GameSession and make it available to the page.

Register DI Types

First, we are going to register our GameSession view model with the Blazor DI services to make it available throughout the game. In the SimpleRPG.Game project, let’s edit the program.cs file:

using Blazorise;
using Blazorise.Bootstrap;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SimpleRPG.Game.Engine.ViewModels;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Threading.Tasks;

namespace SimpleRPG.Game
{
    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.AddTransient(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...
            InitializeAppServices(host.Services);

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

        private static void ConfigureAppServices(IServiceCollection services)
        {
            // add app-specific/custom services here...
            services.AddSingleton<GameSession>();
        }

        private static void InitializeAppServices(IServiceProvider serviceProvider)
        {
            // add service initialization here...
        }
    }
}

We added calls to ConfigureAppServices and InitializeAppServices within the Main function flow — at the right locations to apply these operations. Then, we created simple implementations of those two methods within the Program class. These method are where we will place all of our game-specific type registration and initialization.

    services.AddSingleton<GameSession>();

This call registers a singleton object of the type GameSession (our view model). Singleton means that only one instance of this object is ever created, and it is shared whenever the type is requested. This method only registers the type with Blazor’s DI container. No object instances are created yet. The instances are only created when they are requested.

There are three service lifetimes that can be used for types:

  • Scoped: Blazor WebAssembly apps don’t currently have a concept of DI scopes. Scoped-registered services behave like Singleton services. However, the Blazor Server hosting model supports the Scoped lifetime. In Blazor Server apps, a scoped service registration is scoped to the connection. For this reason, using scoped services is preferred for services that should be scoped to the current user, even if the current intent is to run client-side in the browser.
  • Singleton: DI creates a single instance of the service. All components requiring a Singleton service receive an instance of the same service.
  • Transient: Whenever a component obtains an instance of a Transient service from the service container, it receives a new instance of the service.

So any time we register a type/service, we need to decide which lifetime makes sense for that type.

Note: There is no special initialization required for our view model, so the InitializeAppServices method is left blank for future use.

Inject Required ViewModel

Now, we need to @inject our ViewModel property into the MainScreen page. We do that with the following code changes to MainScreen.razor file:

@page "/"
@inject GameSession ViewModel

<Row Style="height: 5vh; min-height: 32px">
    <Column ColumnSize="ColumnSize.Is12" Style="background-color: aliceblue">
        <Heading Size="HeadingSize.Is3">Simple RPG</Heading>
    </Column>
</Row>
<Row Style="height: 60vh">
    <Column ColumnSize="ColumnSize.Is3.OnWidescreen.Is12" Style="background-color: aquamarine">
        <Table Borderless="true" Narrow="true">
            <TableHeader>
                <TableHeaderCell RowSpan="2">Player Data</TableHeaderCell>
            </TableHeader>
            <TableBody>
                <TableRow>
                    <TableRowCell>Name:</TableRowCell>
                    <TableRowCell>@ViewModel.CurrentPlayer.Name</TableRowCell>
                </TableRow>
                <TableRow>
                    <TableRowCell>Class:</TableRowCell>
                    <TableRowCell>@ViewModel.CurrentPlayer.CharacterClass</TableRowCell>
                </TableRow>
                <TableRow>
                    <TableRowCell>Hit points:</TableRowCell>
                    <TableRowCell>@ViewModel.CurrentPlayer.HitPoints</TableRowCell>
                </TableRow>
                <TableRow>
                    <TableRowCell>Gold:</TableRowCell>
                    <TableRowCell>@ViewModel.CurrentPlayer.Gold</TableRowCell>
                </TableRow>
                <TableRow>
                    <TableRowCell>XP:</TableRowCell>
                    <TableRowCell>@ViewModel.CurrentPlayer.ExperiencePoints</TableRowCell>
                </TableRow>
                <TableRow>
                    <TableRowCell>Level:</TableRowCell>
                    <TableRowCell>@ViewModel.CurrentPlayer.Level</TableRowCell>
                </TableRow>
            </TableBody>
        </Table>
        <Button Color="Color.Secondary" Outline="true" Clicked="@ViewModel.AddXP">Add XP</Button>
    </Column>
    <Column ColumnSize="ColumnSize.Is9.OnWidescreen.Is12" Style="background-color: beige">
        Game Data
    </Column>
</Row>
<Row Style="height: 30vh">
    <Column ColumnSize="ColumnSize.Is3.OnWidescreen.Is12" Style="background-color: burlywood">
        Inventory/Quest
    </Column>
    <Column ColumnSize="ColumnSize.Is9.OnWidescreen.Is12" Style="background-color: lavender">
        Combat/Movement Controls
    </Column>
</Row>

@code {

}

We removed the @code line that defined the ViewModel property by creating an instance of GameSession (from Lesson 2.3).

Then added line #2: @inject GameSession ViewModel. This code uses the DI container to find the type GameSession and get an instance of it. This is a required type for the page, so if it were not registered, we would receive an exception stating that the GameSession service has not been registered. The second part of that inject directive places the instance of the object into a private property named ViewModel. We used the same name as the property we had previously defined, so that all of the binding statements on the page continue to work without change.

As we can probably guess, the container used property injection to add the GameSession object to the MainScreen page as its ViewModel property.

Test With Dependencies

When we test a page that requires injected objects, we need to provide those objects into the TestContext, so that they are available to the component rendering service.

Let’s take a look at a simple test that validates the rendering of the MainScreen page. In SimpleRPG.Game.Tests project, create a Pages folder and then a MainScreenTests class. In that class add the following code:

using Bunit;
using Microsoft.Extensions.DependencyInjection;
using SimpleRPG.Game.Engine.ViewModels;
using SimpleRPG.Game.Pages;
using SimpleRPG.Game.Tests.Mocks;
using Xunit;

namespace SimpleRPG.Game.Tests.Pages
{
    public class MainScreenTests
    {
        private readonly GameSession session = new MockGameSession();

        [Fact]
        public void SimpleRender()
        {
            // arrange
            using var ctx = new TestContext();
            ctx.Services.AddBlazoriseServices();
            ctx.Services.AddSingleton<GameSession>(session);

            // act
            var cut = ctx.RenderComponent<MainScreen>();

            // assert
            var expected = @"<th scope=""col"" class="""" style="""" blazor:onclick=""2"" rowspan=""2"">";
            Assert.Contains(expected, cut.Markup);
            Assert.Contains("Player Data", cut.Markup);
            Assert.Contains("TestPlayer", cut.Markup);
            Assert.Contains("TestClass", cut.Markup);
        }
    }
}

Line #20 adds a mock version of our GameSession to the TestContext before calling RenderComponent<MainScreen>. Then RenderComponent uses the registered services when requested by the page. We will see that the MockGameSession creates a player with the name TestPlayer and character class of TestClass. These are test values we valid in the page’s HTML markup.

By using the MockGameSession class, we are able to test the page with whatever test values we want without requiring a dependency on the production version of that class.

Add the following code in our Mocks folder and MockGameSession.cs file:

using SimpleRPG.Game.Engine.Models;
using SimpleRPG.Game.Engine.ViewModels;

namespace SimpleRPG.Game.Tests.Mocks
{
    class MockGameSession : GameSession
    {
        public MockGameSession()
        {
            this.CurrentPlayer = new Player
            {
                Name = "TestPlayer",
                CharacterClass = "TestClass",
                Level = 1,
                HitPoints = 8,
            };
        }
    }
}

As we can see, MockGameSession derives from GameSession and creates the player with the data that we validated in our test. This makes testing with known values robust across test runs. Reliable unit tests are a great productivity booster. Intermittently failing tests (because of random or live data) can lead to many frustrating debugging sessions.

In conclusion, we’ve made our MainScreen view and GameSession view model loosely coupled from one another. There is no implicit coupling of how they are created. We used Blazor DI to register and request services and objects. And we added a test that shows the power of being able to replace implementation classes with test classes when we need to.

2 thoughts on “Lesson 2.5: Using Blazor Dependency Injection

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