We currently define all of our game data in code. We did this for simplicity and to learn the coding concepts without worrying about data management. If we want to add a new GameItem
, we need to code that into the ItemFactory
. But as we grow our game, we really need to move all of the game data to files that can be updated without making code changes. In the next few lessons, we’re going to redesign our factory classes to load information from data files, rather than define them in code.
First, our file format of choice will be JSON (JavaScript Object Notation). JSON has become the default message format between web applications and services. There’s support in every major platform for reading and writing (serializing) objects into and from JSON format. Since our future plans include getting game data from external web services, JSON data files are a nice step in that direction.
Note: XML is still a viable format. Many older services and libraries still support XML, but since we’re learning new technologies like Blazor and Azure Functions, it makes sense to use the new message format as well. Many of the concepts and code will work for XML serialization as well, only the file reading and conversion would be different.
Data Transfer Objects
The JsonSerializer
in .NET likes to directly read and create objects of a particular type. Sometimes there is a mismatch of attributes that we want to save or read, so we don’t always want to expose all of the properties on GameItem
in order to support loading the data. And with object composition concepts, like Actions
, there may be additional data required or references from other data files. In order to deal with some of these issues, we will use the Data Transfer Object design pattern.
A Data Transfer Object (commonly known as a DTO) is usually an instance of a simple C# class used as a container to encapsulate data and pass data from one layer of an application to another. We would typically find DTOs being used in the service layer to return data back to the presentation layer. The biggest advantage of using DTOs is decoupling clients from our internal data structures. They are also useful in simplifying object serialization without affecting our business object design and game logic.
We will define a simple Data Transfer Object, ItemTemplate
, to use to load item data from a JSON file. Let’s create the ItemTemplate
class in the SimpleRPG.Game.Engine project and under the new Factories\DTO folder.
using SimpleRPG.Game.Engine.Models;
namespace SimpleRPG.Game.Engine.Factories.DTO
{
public class ItemTemplate
{
public int Id { get; set; }
public GameItem.ItemCategory Category { get; set; }
public string Name { get; set; } = string.Empty;
public int Price { get; set; }
public string Damage { get; set; } = string.Empty;
public int Heals { get; set; }
}
}
As we can see, this is a very simple class that consists only of properties and their public getters and setters. There is no logic in this class at all. Also, this class has some optional attributes that are used to construct particular actions for the item, based on the ItemCategory
. For example, weapons need a Damage roll, consumables require a Heals amount, and miscellaneous items don’t use either.
This class simplifies our serialization and matches directly what we will load from our JSON items file.
Next, we need to define the items.json file which we will hold our item data. We will make this data file an embedded resource in our project, so that it builds and publishes side-by-side with our code assemblies.
- Create a new folder in SimpleRPG.Game.Engine project called Data.
- Right click the folder and pick Add > New Item menu items.
- In the New Item dialog select JSON file and name it items.json.

- Once the file is created in the solution explorer, select the items.json file. Look at its properties. For this file’s Build Action, select the “Embedded resource” option.

Then, we will add all of our items and their attributes to this file:
[
{
"Id": 1001, "Category": 1, "Name": "Pointy stick", "Price": 1, "Damage": "1d2"
},
{
"Id": 1002, "Category": 1, "Name": "Rusty sword", "Price": 5, "Damage": "1d3"
},
{
"Id": 1501, "Category": 1, "Name": "Snake fang", "Price": 0, "Damage": "1d2"
},
{
"Id": 1502, "Category": 1, "Name": "Rat claw", "Price": 0, "Damage": "1d2"
},
{
"Id": 1503, "Category": 1, "Name": "Spider fang", "Price": 0, "Damage": "1d4"
},
{
"Id": 2001, "Category": 2, "Name": "Granola bar", "Price": 5, "Heals": 2
},
{
"Id": 3001, "Category": 0, "Name": "Oats", "Price": 1
},
{
"Id": 3002, "Category": 0, "Name": "Honey", "Price": 2
},
{
"Id": 3003, "Category": 0, "Name": "Raisins", "Price": 2
},
{
"Id": 9001, "Category": 0, "Name": "Snake fang", "Price": 1
},
{
"Id": 9002, "Category": 0, "Name": "Snakeskin", "Price": 2
},
{
"Id": 9003, "Category": 0, "Name": "Rat tail", "Price": 1
},
{
"Id": 9004, "Category": 0, "Name": "Rat fur", "Price": 2
},
{
"Id": 9005, "Category": 0, "Name": "Spider fang", "Price": 1
},
{
"Id": 9006, "Category": 0, "Name": "Spider silk", "Price": 2
}
]
As we can see, the JSON format is expressive and easy to read. Each JSON attribute name exactly matches the ItemTemplate
property. There are type differences between integers and strings, and how they are defined. And, this JSON file has a list of items which is denoted by the [ ] symbols that start and end the file.
We wrote the JSON more compactly by putting each object’s properties on a single line of text. In more complex objects, we will typically see the text expanded out with each property on its own line. But the JSON parser is flexible in how it can read these files.
Next, we implement the JsonSerializationHelper
class and DeserializeResourceStream
method in the SimpleRPG.Game.Engine project and Factories folder.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.Json;
namespace D20Tek.Common.Helpers
{
public static class JsonSerializationHelper
{
/// <summary>
/// Deserializes the specified type and returns a list of items.
/// Reads the json file from the assembly's resource manifest.
/// </summary>
/// <typeparam name="T">Type of entity to deserialize.</typeparam>
/// <param name="resourceNamespace">Full namespace path to the resource file.</param>
/// <returns>List of entities deserialized.</returns>
public static IList<T> DeserializeResourceStream<T>(string resourceNamespace)
{
try
{
var assembly = typeof(T).GetTypeInfo().Assembly;
var resourceStream = assembly.GetManifestResourceStream(resourceNamespace);
StreamReader reader;
using (reader = new StreamReader(resourceStream, Encoding.UTF8))
{
var json = reader.ReadToEnd();
var elements = JsonSerializer.Deserialize<IList<T>>(json);
return elements;
}
}
catch (JsonException)
{
throw;
}
catch (Exception ex)
{
throw new InvalidOperationException(
$"Error trying to load embedded resource for: {resourceNamespace}.", ex);
}
}
}
}
The DeserializeResourceStream
method is a static helper that encapsulates how embedded resource files are loaded, read into memory, and then converted from a JSON string into the desired object list. We put this code into a helper method because it will be shared by all of our factories to load embedded data files.
DeserializeResourceStream<T>
is a generic method, so its caller defines the type T that is used and returned by the method. (We will see an example of how this is called shortly.) We use a generic method because the bulk of this code is type agnostic. Only theJsonSerializer
and the method return type cares about which class is used.- The method takes a parameter to the fully-qualified name to the embedded resource.
- We use the
resourceNamespace
in theAssembly.GetManifestResourceStream
method. This loads the resource element as aStream
. - Then, we read the
Stream
and get out its data in text format (UTF8 to be precise). - Up to this point, we could use this code to read any embedded resource text file (including an XML file).
- Then we call
JsonSerializer.Deserialize<IList<T>>
on the JSON text string. This method will return a list of our objects. If the data file only had a single root element defined and not an array, this conversion would fail. - If there are JSON parsing and conversion exceptions, we would surface those to the callers.
- If there are file loading or resource location errors, we wrap those in a generic
InvalidOperationException
… passing the original exception as our inner exception.
This small snippet of code is pretty powerful in what it does. We use the System.Text.Json
implementation of JsonSerializer
rather than using external components, like NewtonSoft.Json
package. They are comparable in functionality, but NewtonSoft.Json
supports more advanced scenarios (feature comparison). But we wanted to learn the .NET Core implementation.
Loading the JSON File in ItemFactory
We already have a creation factory in our code that encapsulates how GameItems
are created and retrieved. This layer of abstraction helps us now as we change how these items are created. Rather than being created in code, they will be loaded from file. But their retrieval via the ItemFactory.CreateGameItem
method remains the same. And doesn’t affect how other parts of our code use GameItems
.
Let’s make these changes to the ItemFactory
class:
using D20Tek.Common.Helpers;
using SimpleRPG.Game.Engine.Actions;
using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;
using System.Linq;
namespace SimpleRPG.Game.Engine.Factories
{
internal static class ItemFactory
{
private const string _resourceNamespace = "SimpleRPG.Game.Engine.Data.items.json";
private static readonly List<GameItem> _standardGameItems = new List<GameItem>();
static ItemFactory()
{
Load();
}
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 ?? "";
}
private static void Load()
{
var templates = JsonSerializationHelper.DeserializeResourceStream<ItemTemplate>(_resourceNamespace);
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);
}
}
}
- The first thing you will notice is the creation code in the constructor has been deleted, and replaced by a call to the
Load
method (line #17). - Then we define the
_resourceNamespace
constant (line #12) to be the fully qualified namespace to the items.json file. This includes the namespace to the file concatenated with the filename. - The
Load
method (lines #32-50) starts by calling ourJsonSerializationHelper.DeserializeResourceStream
method with the_resourceNamespace
parameter. And it calls the generic method with theItemTemplate
type. - This returns a list of
ItemTemplates
that were loaded from our embedded resource file. - Then, we go through each
ItemTemplate
and depending on itsItemCategory
, we build the appropriateGameItem
andAction
(if necessary). - This code still uses the
BuildWeapon
,BuildHealingItem
, andBuildMiscellaneousItem
methods that we already had in this class. Just in our object conversion code, rather than to hard-code each item. - Finally, there were changes to use the
GameItem.SetAction
method (lines #58 & 65) rather than the property setter (we will discuss that refactoring shortly).
The internals of this class radically changed, but its public methods and how other object consume GameItems
didn’t have to change at all.
Refactoring GameItem
The code changes we made so far only affected the ItemFactory
and some of our test code. But we are also going to take this opportunity to refactor the GameItem
class. It is currently too permissive… all of our properties have public setters and a default constructor, but we really don’t want that. We want to force callers to use a GameItem
constructor. I left these open earlier because I wasn’t sure which serialization strategy we would use in these lessons. But now that we’re moving forward with DTOs, we can make our logic classes more restrictive, because we don’t have to worry about the JsonSerializer
requiring a setter on those properties.
So let’s refactor our GameItem
as follows:
using SimpleRPG.Game.Engine.Actions;
using System;
namespace SimpleRPG.Game.Engine.Models
{
public class GameItem
{
public enum ItemCategory
{
Miscellaneous,
Weapon,
Consumable
}
public GameItem(int itemTypeID, ItemCategory category, string name, int price, bool isUnique = false, IAction? action = null)
{
ItemTypeID = itemTypeID;
Category = category;
Name = name;
Price = price;
IsUnique = isUnique;
Action = action;
}
public int ItemTypeID { get; }
public ItemCategory Category { get; }
public string Name { get; }
public int Price { get; }
public bool IsUnique { get; }
public IAction? Action { get; private set; }
public virtual GameItem Clone() =>
new GameItem(ItemTypeID, Category, Name, Price, IsUnique, Action);
internal void SetAction(IAction? action)
{
this.Action = action;
}
internal DisplayMessage PerformAction(LivingEntity actor, LivingEntity target)
{
if (Action is null)
{
throw new InvalidOperationException("CurrentWeapon.Action cannot be null");
}
return Action.Execute(actor, target);
}
}
}
- We removed the static property for Empty
GameItem
. - We removed the default constructor for the
GameItem
. - We removed the setters from most of the properties (lines #25-33). Without the setter, these properties are read-only and can only be set in the class’s constructor.
- We made the
Action
setter private (line #35). This hides it from other classes, but allows it to be set by this class. - Finally, we implemented the
SetAction
method (lines #40-43), so that when external classes need to set an item’s action, it is an explicit method call.
This code change causes quite a number of breaks in our test code, so we had to clean those all up and get our tests running again.
There was also a couple of ripples of this refactoring in our game engine code. We will remove the use of GameItem.Empty
from the GroupedInventoryItem
class.
namespace SimpleRPG.Game.Engine.Models
{
public class GroupedInventoryItem
{
public GroupedInventoryItem(GameItem item, int quantity)
{
Item = item;
Quantity = quantity;
}
public GameItem Item { get; }
public int Quantity { get; set; }
}
}
To address this change, we defined a constructor for the GroupedInventoryItem
and removed the Item
property setter.
Then there were a couple of changes to the Inventory
class to use the GroupedInventoryItem
constructor (lines #47 & 53):
using System;
using System.Collections.Generic;
using System.Linq;
namespace SimpleRPG.Game.Engine.Models
{
public class Inventory
{
private readonly List<GameItem> _backingInventory = new List<GameItem>();
private readonly List<GroupedInventoryItem> _backingGroupedInventory = new List<GroupedInventoryItem>();
public Inventory(IEnumerable<GameItem> items)
{
if (items == null)
{
return;
}
foreach (GameItem item in items)
{
AddItem(item);
}
}
public Inventory()
{
}
public IReadOnlyList<GameItem> Items => _backingInventory.AsReadOnly();
public IReadOnlyList<GroupedInventoryItem> GroupedItems => _backingGroupedInventory.AsReadOnly();
public IList<GameItem> Weapons =>
Items.Where(i => i.Category == GameItem.ItemCategory.Weapon).ToList();
public List<GameItem> Consumables =>
Items.Where(i => i.Category == GameItem.ItemCategory.Consumable).ToList();
public void AddItem(GameItem item)
{
_ = item ?? throw new ArgumentNullException(nameof(item));
_backingInventory.Add(item);
if (item.IsUnique)
{
_backingGroupedInventory.Add(new GroupedInventoryItem(item, 1));
}
else
{
if (_backingGroupedInventory.All(gi => gi.Item.ItemTypeID != item.ItemTypeID))
{
_backingGroupedInventory.Add(new GroupedInventoryItem(item, 0));
}
_backingGroupedInventory.First(gi => gi.Item.ItemTypeID == item.ItemTypeID).Quantity++;
}
}
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--;
}
}
}
}
public void RemoveItems(IList<ItemQuantity> itemQuantities)
{
_ = itemQuantities ?? throw new ArgumentNullException(nameof(itemQuantities));
foreach (ItemQuantity itemQuantity in itemQuantities)
{
for (int i = 0; i < itemQuantity.Quantity; i++)
{
RemoveItem(Items.First(item => item.ItemTypeID == itemQuantity.ItemId));
}
}
}
public bool HasAllTheseItems(IEnumerable<ItemQuantity> items)
{
return items.All(item => Items.Count(i => i.ItemTypeID == item.ItemId) >= item.Quantity);
}
}
}
Again there are some breaking changes to our tests, so we need to spend some time cleaning those up and using constructors where we were using object instantiation and properties before. But fixing the build breaks should get us to working tests. And the test behavior hasn’t changed, so they should all pass once the compiler errors are addressed.
Once all of the tests are building and running successfully, we are in a state to run our game again. Everything should work as before, but now our items data is loading from a file rather than defined in our source code.
We could make changes or add items to the embedded resource file and run the game again to see the changes or new items… without any additional coding changes. This makes our game engine more flexible and game data can be added by non-developers. This is a great design separation for our game engine, so we will continue moving hard-coded data into files over the next few lessons.
4 thoughts on “Lesson 4.11: Load Item Data From JSON File”