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:
- Remove the creation of
GameSession ViewModel
property from MainScreen.razor page. - Register our
GameSession
type with the DI services. - Use
@inject
directive to retrieveGameSession
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 likeSingleton
services. However, the Blazor Server hosting model supports theScoped
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”