In this lesson, we will refactor the ItemFactory
to use the GameServiceClient
to load item data from our game services rather than from the local resource file. We will fetch the ItemTemplates
during the application startup so that any latency is part of the launch. We already have loading logic and a progress indicator at that time, so it makes sense to load the game data there as well.
The ItemTemplate
data is then cached in memory by the ItemFactory
and lives for the lifetime of the app. When the game restarts, the data is fetched again. This keeps our remaining game code the same and we minimize any latency issues during gameplay.
Because we are fetching data from a web service, we will refactor our loading logic to become asynchronous. We don’t want to block our UI thread during these operations, so the async methods allow us to update the UI and progress indicator while we make the web service calls.
Refactor ItemFactory
To enable asynchronous fetching of ItemTemplates
, we removed the Load
call from the ItemFactory
constructor. Constructors cannot be made async, so we need the Load
method called from another component.
Let’s update the ItemFactory
class with the following code:
using SimpleRPG.Game.Engine.Actions;
using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using SimpleRPG.Game.Engine.Services;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace SimpleRPG.Game.Engine.Factories
{
internal static class ItemFactory
{
private const string _serviceUrl = "https://simple-rpg-services.azurewebsites.net/api/item";
private static readonly List<GameItem> _standardGameItems = new List<GameItem>();
public static GameItem CreateGameItem(int itemTypeID)
{
var standardItem = _standardGameItems.First(i => i.ItemTypeID == itemTypeID);
return standardItem.Clone();
}
public static string GetItemName(int itemTypeId)
{
return _standardGameItems.FirstOrDefault(i => i.ItemTypeID == itemTypeId)?.Name ?? "";
}
public static async Task Load(HttpClient httpClient)
{
if (_standardGameItems.Any())
{
return;
}
var client = new GameServiceClient<ItemTemplate, int>(httpClient, _serviceUrl);
var templates = await client.GetAllEntities().ConfigureAwait(false);
foreach (var tmp in templates)
{
switch (tmp.Category)
{
case GameItem.ItemCategory.Weapon:
BuildWeapon(tmp.Id, tmp.Name, tmp.Price, tmp.Damage);
break;
case GameItem.ItemCategory.Consumable:
BuildHealingItem(tmp.Id, tmp.Name, tmp.Price, tmp.Heals);
break;
default:
BuildMiscellaneousItem(tmp.Id, tmp.Name, tmp.Price);
break;
}
}
}
private static void BuildMiscellaneousItem(int id, string name, int price) =>
_standardGameItems.Add(new GameItem(id, GameItem.ItemCategory.Miscellaneous, name, price));
private static void BuildWeapon(int id, string name, int price, string damageDice)
{
var weapon = new GameItem(id, GameItem.ItemCategory.Weapon, name, price, true);
weapon.SetAction(new Attack(weapon, damageDice));
_standardGameItems.Add(weapon);
}
private static void BuildHealingItem(int id, string name, int price, int healPoints)
{
GameItem item = new GameItem(id, GameItem.ItemCategory.Consumable, name, price);
item.SetAction(new Heal(item, healPoints));
_standardGameItems.Add(item);
}
}
}
- We removed the
ItemFactory
constructor. - We removed the
_resourceNamespace
member variable since we won’t need that any longer. - We also deleted the items.json file from the Data folder. We did this to ensure we weren’t inadvertently still loading data locally.
- We added the
_serviceUrl
member variable (line #14) that holds the full url string to the item resource endpoint in our game services. - We changed the definition of the
Load
method to be public now, async, and return aTask
(line #29). We also pass in theHttpClient
to use for our web service calls. TheHttpClient
is defined by the caller and will use the system cached service in our Blazor game. - We check if there are any items already loaded. If there are, then we have already previously loaded items and can skip this operation.
- Then, we create an instance of
GameServiceClient<ItemTemplate, int>
to communicate with the service endpoint forItemTemplates
. We initialize the client with the specifiedHttpClient
and the service url for this factory. - Finally, we call the
GameServiceClient.GetAllEntities
method to retrieve all of theItemTemplates
from the game service endpoint. By using theGameServiceClient
, our factory code doesn’t need to be aware of how to communicate with web services, retrieve json data, and deserialize it into objects. That is all encapsulated in the client class.
The remainder of the ItemFactory
code remains exactly the same. And our game logic code doesn’t need to change how it calls the ItemFactory
throughout our library.
Create FactoryLoader Class
Since our factory classes are internal (and we don’t want to expose that implementation outside of our library) and we need a central place to load data for all factories, we are going to define a public helper class, FactoryLoader
, that will manage that for us.
Create the FactoryLoader
class in the SimpleRPG.Game.Engine project and Factories folder.
using System.Net.Http;
using System.Threading.Tasks;
namespace SimpleRPG.Game.Engine.Factories
{
public static class FactoryLoader
{
public static async Task Load(HttpClient httpClient)
{
await ItemFactory.Load(httpClient).ConfigureAwait(false);
}
}
}
- This is a static class that can be called from anywhere. We will use it both in our unit tests and in the Blazor game project.
- We define a public static
Load
method. This method is also async so that we can call our factoryLoad
methods asynchronously as well. - This method also takes an
HttpClient
as a parameter to pass to individual factoryLoad
methods. - Finally, it calls the
ItemFactory.Load
method. In this lesson, we only load theItemFactory
because that is the only factory we changed thus far. But in the next lesson, we will load all 6 of our factory classes here.
This code encapsulates all of our factory loading and provides a public method to perform this operation for all of our upstream callers – Blazor game app and unit tests.
Update Tests to Handle Async Loading
By changing how we Load
the factory data, we have broken all of our ItemFactory
tests and other tests that were dependent on ItemFactory
. We need to take the opportunity to fix all of our tests now. We are not going to go through all of the test updates in this lesson, but we will focus on getting the ItemFactory
tests fixed. The other test fixes are included in the commit for this lesson.
First let’s create the EngineTestFixture
class in the root folder of the SimpleRPG.Game.Engine.Test project.
using SimpleRPG.Game.Engine.Factories;
using System.Net.Http;
using System.Threading.Tasks;
namespace SimpleRPG.Game.Engine.Tests
{
internal static class EngineTestFixture
{
public static async Task InitializeServices()
{
var httpClient = new HttpClient();
// initialize factory services
await FactoryLoader.Load(httpClient).ConfigureAwait(false);
}
}
}
This test fixture is used to setup the services needed for our test context. For its implementation, we just create an HttpClient
and call the FactoryLoader.Load
method. This ensures that our factories are configured correctly before any tests are run.
Then we update the ItemFactoryTests
class to use the EngineTextFixture
to setup each test.
using SimpleRPG.Game.Engine.Factories;
using System;
using System.Threading.Tasks;
using Xunit;
namespace SimpleRPG.Game.Engine.Tests.Factories
{
public class ItemFactoryTests
{
[Fact]
public async Task CreateGameItem_WithValidItemTypeId()
{
// arrange
await EngineTestFixture.InitializeServices().ConfigureAwait(false);
// act
var item = ItemFactory.CreateGameItem(1001);
// assert
Assert.NotNull(item);
Assert.Equal(1001, item.ItemTypeID);
Assert.Equal("Pointy stick", item.Name);
Assert.Equal(1, item.Price);
}
[Fact]
public async Task CreateGameItem_WithInvalidItemTypeId()
{
// arrange
await EngineTestFixture.InitializeServices().ConfigureAwait(false);
// act - asset
Assert.Throws<InvalidOperationException>(() => ItemFactory.CreateGameItem(1));
}
[Fact]
public async Task GetItemName()
{
// arrange
await EngineTestFixture.InitializeServices().ConfigureAwait(false);
// act
var name = ItemFactory.GetItemName(3001);
// assert
Assert.Equal("Oats", name);
}
[Fact]
public async Task GetItemName_WithInvalidId()
{
// arrange
await EngineTestFixture.InitializeServices().ConfigureAwait(false);
// act
var name = ItemFactory.GetItemName(42);
// assert
Assert.Empty(name);
}
}
}
For each test, we changed the test method signature to be async and return a Task
… to make the tests asynchronous. And we call the EngineTestFixture.InitializeServices
method in the setup of each test method. We followed this same pattern to fix the other tests that need ItemFactory
data loaded. But test test execution and validation code remained as it was.
After all of the test changes, we can build our game project again and run all of the tests successfully.
Update Game Launch to Load Data
With our engine code and test back to green, we are ready to update the SimpleRPG.Game project to load data from our game services. To enable this, we will update the Program.cs file.
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.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...
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);
}
}
}
We updated the InitializeAppServices
method (lines #59-63) in the file to follow the async-await pattern by labeling it with the async keyword and returning a Task
. Then, we get the application’s HttpClient
that was registered with the dependency injection engine. Finally, we use the FactoryLoader
to load the data into the ItemFactory
… and will load other factory data in the future.
Finally, we update line #46 to add an await keyword since the method call is now also asynchronous.
These are all of the code changes required to add this support to our game project. If we now build and try to run our game locally, we will get an exception thrown during the app initialization. Our client call to the game services will thrown an exception. Let’s see why…
Enable CORS on Azure Functions App
The service request from our game fails because our web app is running on the https://localhost domain and our web services are running at https://simple-rpg-services.azurewebsites.net. Browsers and JavaScript engines ensure that requests are not made across domains (or origins). This was done as security fixes to ensure malicious sites were not taking users off to bad services or making requests to services that they were not given access to. Therefore, by default, browsers fail this sort of request.
But the W3C also defined a mechanism to allow this form of communication between apps and services called Cross-Origin Resource Sharing (CORS). This is an opt-in mechanism defined in the web service for known calling domains. Basically, we can say that our local domain web app is allowed to communicate with our game services.
Let’s see how we do that in our Azure Functions App:
- Go to the Azure Portal: https://portal.azure.com/.
- Navigate to the Functions App resource: ‘simple-rpg-services’.

- In the nav bar (on the left… you may need to scroll down), click the CORS option.

- This page starts with a list of three origins that are already supported by the Azure Functions framework.
- In the empty line, we add our local web address: https://localhost:44370 (note: the port number may be different in your local project)
- Also be aware that these addresses cannot contain a trailing ‘/’ (backslash) character. If that is part of the address, the service calls will continue to fail.
- Click the ‘Save’ button at the top of the page for the changes to take hold.
As a side note, we can also define a wildcard (*) in the ‘Allowed Origins’ list. This would make the service callable by any other domains. It completely disables the security aspects of this scenario, so it is better to add your known origins to this list. We will need to add more allowed CORS domains when we deploy our game to Azure again.
Let’s try to run the game locally again. This time, the game should load up to our familiar screen. We can see our Pointy Stick in the weapon dropdown and items in our inventory. So we successfully retrieved those items from our game services. Let’s go kill a few snakes to make sure our game still works!

With our ItemTemplates
loading from our game services, our game is truly online and making service calls. In the next lesson we will look at retrieving the remaining factory data from our game services.
2 thoughts on “Lesson 5.14: Use Service Client to Retrieve ItemTemplates From Game Services”