Lesson 5.9: Refactor Service Code and Create LocationTemplateService

With our implementation of ItemTemplateService and MonsterTemplateService, there is a lot of repetitive code. The act of getting data from the query string and calling the appropriate repository method is boilerplate that can happen in a lot of our services. This code is a great candidate for refactoring into a helper class that contains the shared logic. Especially as we look at our third service (the LocationTemplateService), we don’t want to repeat all of this for a third time or for the number of services we will build in this tutorial.

Duplicate code is a maintenance problem because if a fix is required to the code, it must be made in all the duplicate instances. It’s a better design to put that shared code into its own class that the services will use.

Refactor Shared Service Code

To refactor our service code, we will create the FunctionServiceHelper class in the SimpleRPG.Game.Services project and the Services folder. Then we will move the common code for retrieving elements, getting the element count, and getting a single element by Id.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SimpleRPG.Game.Services.DTO;
using SimpleRPG.Game.Services.Repositories;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Services.Services
{
    public class FunctionServiceHelper<T, TId>
        where T : NamedElement<TId>
        where TId : struct
    {
        private readonly IReadableRepository<T, TId> _repo;

        public FunctionServiceHelper(IReadableRepository<T, TId> repository)
        {
            _repo = repository ?? throw new ArgumentNullException(nameof(repository));
        }

        public async Task<IActionResult> GetEntities(
            HttpRequest httpRequest,
            ILogger log,
            [CallerMemberName] string callingMethod = "GetEntities")
        {
            try
            {
                log.LogInformation($"{callingMethod} method called.");
                _ = httpRequest ?? throw new ArgumentNullException(nameof(httpRequest));

                int? offset = null;
                int? limit = null;
                IEnumerable<NameValuePair<string>> filters = null;
                IEnumerable<NameValuePair<string>> sorts = null;

                if (httpRequest.Query != null)
                {
                    offset = httpRequest.Query.GetValue<int>("offset");
                    limit = httpRequest.Query.GetValue<int>("limit");
                    filters = httpRequest.Query.GetValues("filters");
                    sorts = httpRequest.Query.GetValues("sorts");
                }

                var items = await _repo.GetEntities(offset, limit, filters, sorts).ConfigureAwait(false);

                log.LogInformation($"{callingMethod} method completed successfully.");
                return new OkObjectResult(items);
            }
            catch (Exception ex)
            {
                log.LogError(ex, $"Error processing {callingMethod} method.");
                throw;
            }
        }

        public async Task<IActionResult> GetEntityCount(
            HttpRequest httpRequest,
            ILogger log,
            [CallerMemberName] string callingMethod = "GetEntities")
        {
            try
            {
                log.LogInformation($"{callingMethod} method called.");
                _ = httpRequest ?? throw new ArgumentNullException(nameof(httpRequest));

                IEnumerable<NameValuePair<string>> filters = null;
                if (httpRequest.Query != null)
                {
                    filters = httpRequest.Query.GetValues("filters");
                }

                var count = await _repo.GetEntityCount(filters).ConfigureAwait(false);

                log.LogInformation($"{callingMethod} method completed successfully.");
                return new OkObjectResult(count);
            }
            catch (Exception ex)
            {
                log.LogError(ex, $"Error processing {callingMethod} method.");
                throw;
            }
        }

        public async Task<IActionResult> GetEntityById(
            TId id,
            ILogger log,
            [CallerMemberName] string callingMethod = "GetEntities")
        {
            try
            {
                log.LogInformation($"{callingMethod} method called.");

                var show = await _repo.GetEntityById(id).ConfigureAwait(false);

                log.LogInformation($"{callingMethod} method completed successfully.");
                return new OkObjectResult(show);
            }
            catch (EntityNotFoundException ex)
            {
                log.LogInformation(ex, $"{callingMethod} method called with missing id - {id}.");
                return new NotFoundResult();
            }
            catch (Exception ex)
            {
                log.LogError(ex, $"Error processing {callingMethod} method.");
                throw;
            }
        }
    }
}

For this class, we define it as a generic as well with element type and Id type (like we did for the repositories – lines #13-15). We don’t want the shared code to be type specific, so that it will be easier to reuse.

Then we define a member to hold the IReadableRepository (in line #17). And we define a constructor that provides that repository (lines #19-22). This helper class now knows which repository to use for its operations.

Next, we extract the three methods from the ItemTemplateService and copy them into this helper class. We renamed the methods to remove the type name (i.e. GetItemTemplates to GetEntities). And, nearly all of the code is already type-independent, so it works in this method… we just need to clean.

Finally, we add an optional parameter named callingMethod to each method. This parameter is used in logging calls to ensure that we log the public service methods, rather than the shared method to make it easier for us to track which service may be having production errors. We use the [CallerMemberName] attribute to automatically provide this member name without passing it in the function call (i.e. _serviceHelper.GetEntities(req, log) call). [CallerMemberName] uses the compiler context to figure out the calling method. This behavior can be useful in many places.

If you would like to review how each method is implemented, you can read lesson 5.7.

Update Service Implementations

With the helper class in place, we need to update the implementation of our two services to use it. Let’s take a look at the changes to ItemTemplateService.

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 SimpleRPG.Game.Services.Services;
using System.Threading.Tasks;

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

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

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

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

As we can see, this class has been greatly simplified. It still has the 3 service methods: GetItemTemplates, GetItemTemplateCount, and GetItemTemplateById. But, each method now just calls the corresponding FunctionServiceHelper method with the appropriate parameters. Each method also keeps its signature and attributes because that is how the Azure Functions runtime maps HTTP requests to these methods. And we created a static instance of the FunctionServiceHelper that is used for all service method calls.

This is a much simpler service… all duplicated code has been removed. All that is left is the entry point definitions. When we update or create another service, we will follow this same pattern. Let’s take a look at that with the MonsterTemplateService changes.

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 SimpleRPG.Game.Services.Services;
using System.Threading.Tasks;

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

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

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

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

Again this service class is the minimum needed route messages and call the corresponding FunctionServiceHelper method.

This concludes the service refactoring to extract the FunctionServiceHelper class. Now on to building the next service.

Create Data Classes LocationTemplate

As with the previous services, we need to define the required DTOs, create a new repository builder, and map the type and builder in the RepositoryFactory,

1. Create the locations.json file in the SimpleRPG.Game.Service project and Data folder. (Notice that we added an Id to the location data to match our expectation of deriving from NamedElement.)

[
  {
    "Id": 101,
    "X": -2,
    "Y": -1,
    "Name": "Farmer's Field",
    "ImageName": "/images/locations/FarmFields.png",
    "Description": "There are rows of corn here, with giant rats hiding between them.",
    "Monsters": [
      {
        "Id": 2,
        "Perc": 100
      }
    ]
  },
  {
    "Id": 102,
    "X": -1,
    "Y": -1,
    "Name": "Farmer's House",
    "ImageName": "/images/locations/Farmhouse.png",
    "Description": "This is the house of your neighbor, Farmer Ted.",
    "TraderId": 102
  },
  {
    "Id": 103,
    "X": 0,
    "Y": -1,
    "Name": "Home",
    "ImageName": "/images/locations/Home.png",
    "Description": "This is your home."
  },
  {
    "Id": 104,
    "X": -1,
    "Y": 0,
    "Name": "Trading Shop",
    "ImageName": "/images/locations/Trader.png",
    "Description": "The shop of Susan, the trader.",
    "TraderId": 101
  },
  {
    "Id": 105,
    "X": 0,
    "Y": 0,
    "Name": "Town Square",
    "ImageName": "/images/locations/TownSquare.png",
    "Description": "You see a fountain here."
  },
  {
    "Id": 106,
    "X": 1,
    "Y": 0,
    "Name": "Town Gate",
    "ImageName": "/images/locations/TownGate.png",
    "Description": "There is a gate here, protecting the town from giant spiders."
  },
  {
    "Id": 107,
    "X": 2,
    "Y": 0,
    "Name": "Spider Forest",
    "ImageName": "/images/locations/SpiderForest.png",
    "Description": "The trees in this forest are covered with spider webs.",
    "Monsters": [
      {
        "Id": 3,
        "Perc": 100
      }
    ]
  },
  {
    "Id": 108,
    "X": 0,
    "Y": 1,
    "Name": "Herbalist's Hut",
    "ImageName": "/images/locations/HerbalistsHut.png",
    "Description": "You see a small hut, with plants drying from the roof.",
    "TraderId": 103,
    "Quests": [ 1 ]
  },
  {
    "Id": 109,
    "X": 0,
    "Y": 2,
    "Name": "Herbalist's Garden",
    "ImageName": "/images/locations/HerbalistsGarden.png",
    "Description": "There are many plants here, with snakes hiding behind them.",
    "Monsters": [
      {
        "Id": 1,
        "Perc": 100
      }
    ]
  }
]

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

3. Create the MonsterEncounterItem class in the SimpleRPG.Game.Services project and DTO folder. This class keeps information about the likelihood of a monster appearing at the specified location.

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

        public int Perc { get; set; }
    }
}

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

using System.Collections.Generic;

namespace SimpleRPG.Game.Services.DTO
{
    public class LocationTemplate : NamedElement<int>
    {
        public int X { get; set; }

        public int Y { get; set; }

        public string Description { get; set; } = string.Empty;

        public string ImageName { get; set; } = string.Empty;

        public int? TraderId { get; set; }

        public IEnumerable<int> Quests { get; set; } = new List<int>();

        public IEnumerable<MonsterEncounterItem> Monsters { get; set; } = new List<MonsterEncounterItem>();
    }
}

At this stage, we have the locations.json resource file and can load its data using the JsonSerializataionHelper class.

Add Support to RepositoryFactory

Next, we need to surface the LocationTemplate type through the RepositoryFactory. We will start by creating the LocationTemplateMemoryRepositoryBuilder class in the RepositoryBuilders.cs file (lines #45-56).

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

namespace SimpleRPG.Game.Services.Repositories
{
    internal class ItemTemplateMemoryRepositoryBuilder : IRepositoryBuilder
    {
        private const string _resourceNamespace = "SimpleRPG.Game.Services.Data.items.json";
        private static readonly List<NameValuePair<Func<ItemTemplate, string, bool>>> _knownFilters =
            new List<NameValuePair<Func<ItemTemplate, string, bool>>>
            {
                new NameValuePair<Func<ItemTemplate, string, bool>>("category", (f, v) => f.Category == Convert.ToInt32(v)),
            };

        private static readonly List<NameValuePair<Func<ItemTemplate, object>>> _knownSorts =
            new List<NameValuePair<Func<ItemTemplate, object>>>
            {
                new NameValuePair<Func<ItemTemplate, object>>("category", p => Convert.ToInt32(p.Category)),
                new NameValuePair<Func<ItemTemplate, object>>("price", p => p.Price)
            };

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

            return result;
        }
    }

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

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

            return result;
        }
    }

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

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

            return result;
        }
    }
}

Then, add that type mapping to the RepositoryFactory._resourceBuilderMapping member (line #15). That is the only change required to enable the repository in this factory.

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() }
            };

        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>;
        }
    }
}

At this stage, we can retrieve the LocationTemplate-typed repository from this factory class, so it is exposed to the rest of our service code.

Create LocationTemplateService

As we did in the service refactoring code above, we will use the FunctionServiceHelper class to easily build our new LocationTemplateService. Create the LocationTemplateService 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 LocationTemplateService
    {
        private const string _baseRoute = "location";
        private static readonly IReadableRepository<LocationTemplate, int> _repo =
            RepositoryFactory.CreateRepository<LocationTemplate, int>("resource");
        private static readonly FunctionServiceHelper<LocationTemplate, int> _serviceHelper =
            new FunctionServiceHelper<LocationTemplate, int>(_repo);

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

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

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

This code follows the exact same pattern as our other service classes. We define the entry points for the service and map the calls to the helper class. This is all the plumbing we need to make our new service accessible via Azure Functions.

We can now build and run our services project again, and we will get the following displayed in the Azure Functions runtime console.

Fig 1 – Azure Functions Console with LocationTemplateService

As we can see the 3 new location service endpoints are now available. As we did with the previous services, we can use their URI in the browser to see that the services return the information that we expect.

Fig 2 – GetLocationTemplates Results

With this lesson complete, we have built the core repository pattern for all of our resource files. We have pulled in the shared code into base implementations of the InMemoryReadRepository and FunctionServiceHelper classes. By doing so, we have simplified how we can build repositories and service endpoints to read all the game data that we need. In the next lesson, we’re going to fast forward to build the 3 remaining services for our data.

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