Lesson 5.10: Complete the Game Services

In this lesson, we’re going to build out the three remaining services quickly. We will continue to use the same patterns (DTOs, repositories, and service helper class) that we’ve used in the previous lessons. If you need to review any of the individual patterns, please review those lessons.

QuestTemplateService

1. Create the quests.json file in the SimpleRPG.Game.Service project and Data folder. 

[
  {
    "Id": 1,
    "Name": "Clear the herb garden",
    "Description": "Defeat the snakes in the Herbalist's garden",
    "RewardGold": 25,
    "RewardXP": 10,
    "RewardItems": [
      { "Id": 1002, "Qty": 1 }
    ],
    "Requirements": [
      { "Id": 9001, "Qty": 5 }
    ]
  }
]

2. Right click on the quests.json file and go to the properties for it. Set the Build Action for this file to “Embedded Resource”.

3. Create the IdQuantityItem class in the SimpleRPG.Game.Services project and DTO folder. This class keeps information about quantities of particular items and will be used for quests, recipes, and traders.

namespace SimpleRPG.Game.Services.DTO
{
    public class IdQuantityItem
    {
        public int Id { get; set; }

        public int Qty { get; set; }
    }
}

4. Create the QuestTemplate class in the SimpleRPG.Game.Services project and DTO folder.

using System.Collections.Generic;

namespace SimpleRPG.Game.Services.DTO
{
    public class QuestTemplate : NamedElement<int>
    {
        public string Description { get; set; } = string.Empty;

        public IEnumerable<IdQuantityItem> Requirements { get; set; } = new List<IdQuantityItem>();

        public int RewardGold { get; set; }

        public int RewardXP { get; set; }

        public IEnumerable<IdQuantityItem> RewardItems { get; set; } = new List<IdQuantityItem>();
    }
}

5. Create the QuestTemplateMemoryRepositoryBuilder class in the RepositoryBuilders.cs file (lines #71 – 82).

    internal class QuestTemplateMemoryRepositoryBuilder : IRepositoryBuilder
    {
        private const string _resourceNamespace = "SimpleRPG.Game.Services.Data.quests.json";

        public IRepository CreateRepository()
        {
            var entities = JsonSerializationHelper.DeserializeResourceStream<QuestTemplate>(_resourceNamespace);
            var result = new InMemoryReadRepository<QuestTemplate, int>(entities);

            return result;
        }
    }

6. Add the QuestTemplate type mapping to the RepositoryFactory._resourceBuilderMapping member (line #17).

using SimpleRPG.Game.Services.DTO;
using System;
using System.Collections.Generic;

namespace SimpleRPG.Game.Services.Repositories
{
    public static class RepositoryFactory
    {
        private const string _repoResources = "resource";
        private static readonly Dictionary<Type, IRepositoryBuilder> _resourceBuilderMapping =
            new Dictionary<Type, IRepositoryBuilder>
            {
                { typeof(ItemTemplate), new ItemTemplateMemoryRepositoryBuilder() },
                { typeof(MonsterTemplate), new MonsterTemplateMemoryRepositoryBuilder() },
                { typeof(LocationTemplate), new LocationTemplateMemoryRepositoryBuilder() },
                { typeof(TraderTemplate), new TraderTemplateMemoryRepositoryBuilder() },
                { typeof(QuestTemplate), new QuestTemplateMemoryRepositoryBuilder() },
                { typeof(RecipeTemplate), new RecipeTemplateMemoryRepositoryBuilder() },
            };

        public static IReadableRepository<T, TId> CreateRepository<T, TId>(string repoSource)
            where T : NamedElement<TId>
            where TId : struct
        {
            switch (repoSource)
            {
                case _repoResources:
                    return CreateResourceRepository<T, TId>();
                default:
                    throw new ArgumentOutOfRangeException(nameof(repoSource));
            }
        }

        private static IReadableRepository<T, TId> CreateResourceRepository<T, TId>()
            where T : NamedElement<TId>
            where TId : struct
        {
            var builder = _resourceBuilderMapping[typeof(T)];
            return builder.CreateRepository() as IReadableRepository<T, TId>;
        }
    }
}

7. Create the QuestTemplateService class in the SimpleRPG.Game.Service project and Services folder. In this service, we expose the same retrieval operations that we have in previous lessons.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using SimpleRPG.Game.Services.DTO;
using SimpleRPG.Game.Services.Repositories;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Services.Services
{
    public static class QuestTemplateService
    {
        private const string _baseRoute = "quest";
        private static readonly IReadableRepository<QuestTemplate, int> _repo =
            RepositoryFactory.CreateRepository<QuestTemplate, int>("resource");
        private static readonly FunctionServiceHelper<QuestTemplate, int> _serviceHelper =
            new FunctionServiceHelper<QuestTemplate, int>(_repo);

        [FunctionName("GetQuestTemplates")]
        public static async Task<IActionResult> GetQuestTemplates(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute)] HttpRequest req,
            ILogger log)
        {
            return await _serviceHelper.GetEntities(req, log).ConfigureAwait(false);
        }

        [FunctionName("GetQuestTemplateCount")]
        public static async Task<IActionResult> GetQuestTemplateCount(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute + "-count")]
            HttpRequest req,
            ILogger log)
        {
            return await _serviceHelper.GetEntityCount(req, log).ConfigureAwait(false);
        }

        [FunctionName("GetQuestTemplateById")]
        public static async Task<IActionResult> GetQuestTemplateById(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute + "/{id}")]
            HttpRequest req,
            ILogger log,
            int id)
        {
            return await _serviceHelper.GetEntityById(id, log).ConfigureAwait(false);
        }
    }
}

RecipeTemplateService

1. Create the recipes.json file in the SimpleRPG.Game.Service project and Data folder. 

[
  {
    "Id": 1,
    "Name": "Granola bar recipe",
    "Ingredients": [
      { "Id": 3001, "Qty": 1 },
      { "Id": 3002, "Qty": 1 },
      { "Id": 3003, "Qty": 1 }
    ],
    "OutputItems": [
      { "Id": 2001, "Qty": 1 }
    ]
  }
]

2. Right click on the recipes.json file and go to the properties for it. Set the Build Action for this file to “Embedded Resource”.

3. Create the RecipeTemplate class in the SimpleRPG.Game.Services project and DTO folder.

using System.Collections.Generic;

namespace SimpleRPG.Game.Services.DTO
{
    public class RecipeTemplate : NamedElement<int>
    {
        public IEnumerable<IdQuantityItem> Ingredients { get; set; } = new List<IdQuantityItem>();

        public IEnumerable<IdQuantityItem> OutputItems { get; set; } = new List<IdQuantityItem>();
    }
}

4. Create the RecipeTemplateMemoryRepositoryBuilder class in the RepositoryBuilders.cs file (lines #84-95).

    internal class RecipeTemplateMemoryRepositoryBuilder : IRepositoryBuilder
    {
        private const string _resourceNamespace = "SimpleRPG.Game.Services.Data.recipes.json";

        public IRepository CreateRepository()
        {
            var entities = JsonSerializationHelper.DeserializeResourceStream<RecipeTemplate>(_resourceNamespace);
            var result = new InMemoryReadRepository<RecipeTemplate, int>(entities);

            return result;
        }
    }

5. Add the RecipeTemplate type mapping to the RepositoryFactory._resourceBuilderMapping member (line #18).

using SimpleRPG.Game.Services.DTO;
using System;
using System.Collections.Generic;

namespace SimpleRPG.Game.Services.Repositories
{
    public static class RepositoryFactory
    {
        private const string _repoResources = "resource";
        private static readonly Dictionary<Type, IRepositoryBuilder> _resourceBuilderMapping =
            new Dictionary<Type, IRepositoryBuilder>
            {
                { typeof(ItemTemplate), new ItemTemplateMemoryRepositoryBuilder() },
                { typeof(MonsterTemplate), new MonsterTemplateMemoryRepositoryBuilder() },
                { typeof(LocationTemplate), new LocationTemplateMemoryRepositoryBuilder() },
                { typeof(TraderTemplate), new TraderTemplateMemoryRepositoryBuilder() },
                { typeof(QuestTemplate), new QuestTemplateMemoryRepositoryBuilder() },
                { typeof(RecipeTemplate), new RecipeTemplateMemoryRepositoryBuilder() },
            };

        public static IReadableRepository<T, TId> CreateRepository<T, TId>(string repoSource)
            where T : NamedElement<TId>
            where TId : struct
        {
            switch (repoSource)
            {
                case _repoResources:
                    return CreateResourceRepository<T, TId>();
                default:
                    throw new ArgumentOutOfRangeException(nameof(repoSource));
            }
        }

        private static IReadableRepository<T, TId> CreateResourceRepository<T, TId>()
            where T : NamedElement<TId>
            where TId : struct
        {
            var builder = _resourceBuilderMapping[typeof(T)];
            return builder.CreateRepository() as IReadableRepository<T, TId>;
        }
    }
}

6. Create the RecipeTemplateService class in the SimpleRPG.Game.Service project and Services folder.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using SimpleRPG.Game.Services.DTO;
using SimpleRPG.Game.Services.Repositories;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Services.Services
{
    public static class RecipeTemplateService
    {
        private const string _baseRoute = "recipe";
        private static readonly IReadableRepository<RecipeTemplate, int> _repo =
            RepositoryFactory.CreateRepository<RecipeTemplate, int>("resource");
        private static readonly FunctionServiceHelper<RecipeTemplate, int> _serviceHelper =
            new FunctionServiceHelper<RecipeTemplate, int>(_repo);

        [FunctionName("GetRecipeTemplates")]
        public static async Task<IActionResult> GetRecipeTemplates(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute)] HttpRequest req,
            ILogger log)
        {
            return await _serviceHelper.GetEntities(req, log).ConfigureAwait(false);
        }

        [FunctionName("GetRecipeTemplateCount")]
        public static async Task<IActionResult> GetRecipeTemplateCount(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute + "-count")]
            HttpRequest req,
            ILogger log)
        {
            return await _serviceHelper.GetEntityCount(req, log).ConfigureAwait(false);
        }

        [FunctionName("GetRecipeTemplateById")]
        public static async Task<IActionResult> GetRecipeTemplateById(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute + "/{id}")]
            HttpRequest req,
            ILogger log,
            int id)
        {
            return await _serviceHelper.GetEntityById(id, log).ConfigureAwait(false);
        }
    }
}

TraderTemplateService

1. Create the traders.json file in the SimpleRPG.Game.Service project and Data folder.

[
  {
    "Id": 101,
    "Name": "Susan",
    "Inventory": [
      { "Id": 1001, "Qty": 1 }
    ]
  },
  {
    "Id": 102,
    "Name": "Farmer Ted",
    "Inventory": [
      { "Id": 1001, "Qty": 1 }
    ]
  },
  {
    "Id": 103,
    "Name": "Pete the Herbalist",
    "Inventory": [
      { "Id": 1001, "Qty": 1 }
    ]
  }
]

2. Right click on the traders.json file and go to the properties for it. Set the Build Action for this file to “Embedded Resource”.

3. Create the TraderTemplate class in the SimpleRPG.Game.Services project and DTO folder.

using System.Collections.Generic;

namespace SimpleRPG.Game.Services.DTO
{
    public class TraderTemplate : NamedElement<int>
    {
        public IEnumerable<IdQuantityItem> Inventory { get; set; } = new List<IdQuantityItem>();
    }
}

4. Create the TraderTemplateMemoryRepositoryBuilder class in the RepositoryBuilders.cs file (lines #58-69).

    internal class TraderTemplateMemoryRepositoryBuilder : IRepositoryBuilder
    {
        private const string _resourceNamespace = "SimpleRPG.Game.Services.Data.traders.json";

        public IRepository CreateRepository()
        {
            var entities = JsonSerializationHelper.DeserializeResourceStream<TraderTemplate>(_resourceNamespace);
            var result = new InMemoryReadRepository<TraderTemplate, int>(entities);

            return result;
        }
    }

5. Add the TraderTemplate type mapping to the RepositoryFactory._resourceBuilderMapping member (line #16).

using SimpleRPG.Game.Services.DTO;
using System;
using System.Collections.Generic;

namespace SimpleRPG.Game.Services.Repositories
{
    public static class RepositoryFactory
    {
        private const string _repoResources = "resource";
        private static readonly Dictionary<Type, IRepositoryBuilder> _resourceBuilderMapping =
            new Dictionary<Type, IRepositoryBuilder>
            {
                { typeof(ItemTemplate), new ItemTemplateMemoryRepositoryBuilder() },
                { typeof(MonsterTemplate), new MonsterTemplateMemoryRepositoryBuilder() },
                { typeof(LocationTemplate), new LocationTemplateMemoryRepositoryBuilder() },
                { typeof(TraderTemplate), new TraderTemplateMemoryRepositoryBuilder() },
                { typeof(QuestTemplate), new QuestTemplateMemoryRepositoryBuilder() },
                { typeof(RecipeTemplate), new RecipeTemplateMemoryRepositoryBuilder() },
            };

        public static IReadableRepository<T, TId> CreateRepository<T, TId>(string repoSource)
            where T : NamedElement<TId>
            where TId : struct
        {
            switch (repoSource)
            {
                case _repoResources:
                    return CreateResourceRepository<T, TId>();
                default:
                    throw new ArgumentOutOfRangeException(nameof(repoSource));
            }
        }

        private static IReadableRepository<T, TId> CreateResourceRepository<T, TId>()
            where T : NamedElement<TId>
            where TId : struct
        {
            var builder = _resourceBuilderMapping[typeof(T)];
            return builder.CreateRepository() as IReadableRepository<T, TId>;
        }
    }
}

6. Create the TraderTemplateService class in the SimpleRPG.Game.Service project and Services folder.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using SimpleRPG.Game.Services.DTO;
using SimpleRPG.Game.Services.Repositories;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Services.Services
{
    public static class TraderTemplateService
    {
        private const string _baseRoute = "trader";
        private static readonly IReadableRepository<TraderTemplate, int> _repo =
            RepositoryFactory.CreateRepository<TraderTemplate, int>("resource");
        private static readonly FunctionServiceHelper<TraderTemplate, int> _serviceHelper =
            new FunctionServiceHelper<TraderTemplate, int>(_repo);

        [FunctionName("GetTraderTemplates")]
        public static async Task<IActionResult> GetTraderTemplates(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute)] HttpRequest req,
            ILogger log)
        {
            return await _serviceHelper.GetEntities(req, log).ConfigureAwait(false);
        }

        [FunctionName("GetTraderTemplateCount")]
        public static async Task<IActionResult> GetTraderTemplateCount(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute + "-count")]
            HttpRequest req,
            ILogger log)
        {
            return await _serviceHelper.GetEntityCount(req, log).ConfigureAwait(false);
        }

        [FunctionName("GetTraderTemplateById")]
        public static async Task<IActionResult> GetTraderTemplateById(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute + "/{id}")]
            HttpRequest req,
            ILogger log,
            int id)
        {
            return await _serviceHelper.GetEntityById(id, log).ConfigureAwait(false);
        }
    }
}

Validate Services

With the code for all of the service complete, we can now build our Azure Functions services. When we run the services, we will see all of the service endpoints available in our local Azure Functions runtime command line.

Fig 1 – Azure Functions Runtime

We can take any of the new endpoint urls and validate the new services return the data we expect. Here’s what we get when we try to retrieve all quests (localhost:7071/api/quest):

Fig 2 – Query Quests

Next let’s test the recipes returned (localhost:7071/api/recipe):

Fig 3 – Query Recipes

Finally, let’s retrieve the traders in the game (localhost:7071/api/trader):

Fig 4 – Query Traders

With this update, we have created service endpoints for all of the game data that we have defined in the game thus far. We will be able to replace the local files in our game, with service calls to get the game data. This will separate our game runtime from the game data. And make it easier for us to provide updated game content without having to update the game code. Our services are still very simple and don’t use any good read-write storage systems yet, but it’s a start down that path, which we will explore in future lessons.

In in the next couple of lessons, we are going to create continuous integration (CI) and continuous delivery (CD) pipelines for the Azure Functions code… just like we did earlier in the chapter for the game website.

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 )

Google photo

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

Twitter picture

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

Facebook photo

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

Connecting to %s