Lesson 5.7 Design REST Service for Data Retrieval

REST web services (Representational State Transfer) is a style of architecture based on a set of principles that describe how networked resources are defined and addressed. It is important to note that REST is a style of software architecture as opposed to a set of standards. As a result, applications or architectures are sometimes referred to as RESTful or REST-style. REST has been a popular choice for implementing web services because they tend to be cross-platform and accessible by many client technologies.

A service or architecture is considered RESTful when they are:

  • Stateless client/server protocol.
  • Every resource is uniquely addressable using a URI.
  • Contains minimal set of operations (typically using HTTP commands of GET, POST, PUT, or DELETE over the Internet).
  • The protocol is layered and supports caching.
  • Uses hypermedia output formats (HTML, JSON, XML).

This is essentially the architecture of the Internet and helps to explain the popularity and ease-of-use for REST.

We’re going to put these concepts into action in this lesson to implement a richer ItemTemplateService that uses an InMemoryRepository to load and retrieve our ItemTemplate data.

The Repository Factory

We are going to build a RepositoryFactory to create and return an initialized instance of the IReadableRepository. We will create the factory method because we don’t want the services to have knowledge about how to instantiate and initialize our repositories (following the separation of concerns principle). And, we will have several typed repositories (because they use generics), so the logic to handle that can be in the factory method. Finally in the future, we will support multiple types of repository sources (embedded resource files, Azure blobs, Azure Table, and CosmosDB), so we are hiding all of that complexity from our services.

For the initial RepositoryFactory, we will only provide a single IReadableRepository<ItemTemplate, int> embedded resource repository. But we are building the capability to support more types and different sources. Let’s create the RepositoryFactory class in the SimpleRPG.Game.Services project and Repositories folder.

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, Func<object>> _resourceConstructorMapping = new Dictionary<Type, Func<object>>
        {
            { typeof(ItemTemplate), () => new ItemTemplateMemoryRepository() }
        };

        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 constructor = _resourceConstructorMapping[typeof(T)];
            return constructor.Invoke() as IReadableRepository<T, TId>;
        }
    }
}

This class follows the static Factory pattern that we’ve used throughout this tutorial. It contains the following code:

  • Definition of the resource repository identifier (line #9).
  • Dictionary for mapping DTO types to their corresponding repository constructor (lines # 10-13). We start with only the ItemTemplate type, but as we add more supported data types (like monsters and locations), we will add more mappings here.
  • The CreateRepository method (lines #15-26) is the main factory entry point. It takes the type of repository we wish to create as a parameter (currently we only support in-memory repositories, but in the future we will investigate others). It checks the repository type and calls the specific Create… method. If we request an unknown repository type, then the method throw an exception.
  • Finally, the CreateResourceRepository method (lines #28-34) is responsible for creating a typed InMemoryReadRepository using the mapping dictionary we defined above.
    • Based on the DTO type, we find the corresponding constructor.
    • Then we invoke that constructor and return the result mapped to the IReadableRepository interface.
    • This code is ready to support other types and repositories in the future.
    • If an unsupported type is requested, then it when throw an exception when that type is not found in the dictionary.

As we can see, we use a default constructor to create a type-specific implementation of the InMemoryRepository. Let’s create the ItemTemplateReadableRepository class in the Repositories folder.

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

namespace SimpleRPG.Game.Services.Repositories
{
    internal class ItemTemplateMemoryRepository : InMemoryReadRepository<ItemTemplate, int>
    {
        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 ItemTemplateMemoryRepository()
            :base (null, _knownFilters, _knownSorts)
        {
            Entities = JsonSerializationHelper.DeserializeResourceStream<ItemTemplate>(_resourceNamespace);
        }
    }
}

This class contains static data for the filters and sorting options that are supported for the ItemTemplate DTO; also a static member for the full namespace to the items.json resource file; and the default constructor that uses this static data to initialize and load the ItemTemplates data. We use this constructor to hide the complexity of creating the type-specific repository. This works well for now, but we may revisit this design in future lessons to show how we could do this with the Strategy design pattern instead.

IQueryCollection Extension Methods

As we saw in our last lesson, our ItemTemplateService methods have an HttpRequest parameter. This has the full HTTP request data, which include the query string for the url that was called. That query string is represented as an IQueryCollection, which is the parsed elements of the query string in name-value pairs. However, for our querying, we will have complex data for filtering and sorting, so we will extend the functionality of IQueryCollection with the behavior we need.

For our complex query string parameters, we want to support the ability to specify them as a concatenation of multiple clauses: filter1:value1|filter2:value2|…|filterN:valueN. To interpret these query string parameters, we need the ability to:

  • Parse the full text and split clauses separated by the ‘|’ symbol.
  • Loop through each clause.
  • Parse the clause into corresponding name value pair separated by ‘:’ or “-” symbol.
  • Provide exceptions and errors for strings that cannot be parsed or converted.

To provide this behavior in our services, we will create extension methods for the IQueryCollection interface. Let’s do that by creating the QueryCollectionExtensions class in the Services folder.

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using SimpleRPG.Game.Services.DTO;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace SimpleRPG.Game.Services.Services
{
    public static class QueryCollectionExtensions
    {
        private static readonly char[] TypeSplitters = new char[] { ':', '-' };

        public static T? GetValue<T>(this IQueryCollection source, string key)
            where T : struct
        {
            T? result = null;
            string text = source.GetValue(key);
            if (string.IsNullOrEmpty(text) == false)
            {
                if (typeof(T) == typeof(Guid))
                {
                    var obj = new Guid(text);
                    result = obj as T?;
                }
                else
                {
                    var format = CultureInfo.CurrentUICulture.GetFormat(typeof(T)) as IFormatProvider;
                    var obj = Convert.ChangeType(text, typeof(T), format);
                    result = obj as T?;
                }
            }

            return result;
        }

        public static string GetValue(this IQueryCollection source, string key)
        {
            _ = source ?? throw new ArgumentNullException(nameof(source));

            string result = null;
            if (source.TryGetValue(key, out StringValues value))
            {
                result = value.FirstOrDefault();
            }

            return result;
        }

        public static IList<NameValuePair<string>> GetValues(this IQueryCollection source, string key)
        {
            var result = new List<NameValuePair<string>>();
            string text = source.GetValue(key);
            if (string.IsNullOrEmpty(text) == false)
            {
                var pairs = text.Split('|');
                foreach (var element in pairs)
                {
                    var r = element.SplitCompoundEntry<string>();
                    result.Add(r);
                }
            }

            return result;
        }

        public static NameValuePair<T> SplitCompoundEntry<T>(this string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                throw new ArgumentNullException("Type identifier cannot be null or empty.");
            }

            var parts = text.Split(TypeSplitters);
            var value = default(T);
            if (text.Any(ch => TypeSplitters.Contains(ch)))
            {
                if (parts.Count() != 2)
                {
                    throw new FormatException("Type identifier is not the correct format.");
                }
                else
                {
                    value = (T)Convert.ChangeType(parts[1], typeof(T));
                }
            }

            return new NameValuePair<T>(parts[0], value);
        }
    }
}
  • GetValue<T> method (lines #15-36) gets the specified value from the IQueryCollection and then converts it to the requested type. This gives us the ability to get a typed value from the collection without having to sprinkle string to type conversion logic within our service. If the specified key is not in the collection, a null result is returned. And if the type conversion fails, an exception is thrown.
  • GetValue method (lines #38-49) just gets the element with the specified key as a string. If the key isn’t found, it returns a null. This method gets called by the GetValue<T> method to retrieve the string representation before trying to convert it.
  • GetValues method (lines #51-66) returns a list of name-value pairs that represent a complex query string parameter. It separates each clause with the ‘|’ symbol and then calls SplitCompoundEntry with the string of each one… adding each result to the returned list.
  • The SplitCompoundEntry (lines #68-90) method takes a string and attempts to break it up into a name value pair by splitting it using the ‘:’ and ‘-‘ symbols. This is fairly simple parsing and if text doesn’t meet the exact format, then we fail. However, since we control how the client code will create the query strings, then we can make some simplifying assumptions in this method. If we wanted a more robust parsing of the query string, we could certainly implement that in these extension methods.

The code in this class does some complex, repetitive code to access and type some query string data. This is another perfect class for a shared library because it can be used in all of our services. The IQueryCollection mechanism is pretty generalized so we can adapt it to many different service requirements. So we also built a good set a test cases to ensure we cover how this code works in full, not just how we use it for our service.

ItemTemplateService Query Capabilities

Now that we have some basic plumbing in place, it will be straightforward to create the service endpoints we want to expose via Azure Functions. We will support the following features from our service:

  • Ability to query for ItemTemplates with some simple filters (Name and Category) and sorts (Name, Category, and Price).
  • Ability to query the count of ItemTemplates using the same filters.
  • Ability to query for a single ItemTemplate based on its unique id.

When we review the service code, we will see that all of these methods use “item” as the base route for the request, only perform GET requests (since they are all queries so far), and the only difference between the list and single ItemTemplate request is that when the id is passed in the route, we only return the single entry, for example:

These characteristics are part of the REST specification and by following this convention, we are building RESTful service APIs. When we build out write/delete functionality, we will see that we continue to use the same route but different operations: POST (to create an ItemTemplate), PUT (to update an ItemTemplate), DELETE (to delete an ItemTemplate).

Now that we understand the concepts, let update the ItemTemplateService class with these new capabilities.

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;
using System.Collections.Generic;
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");

        [FunctionName("GetItemTemplates")]
        public static async Task<IActionResult> GetItemTemplates(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute)] HttpRequest req,
            ILogger log)
        {
            try
            {
                log.LogInformation("GetItemTemplates method called.");
                _ = req ?? throw new ArgumentNullException(nameof(req));

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

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

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

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

        [FunctionName("GetItemTemplateCount")]
        public static async Task<IActionResult> GetItemTemplateCount(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute + "-count")]
            HttpRequest req,
            ILogger log)
        {
            try
            {
                log.LogInformation("GetItemTemplateCount method called.");
                _ = req ?? throw new ArgumentNullException(nameof(req));

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

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

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

        [FunctionName("GetItemTemplateById")]
        public static async Task<IActionResult> GetItemTemplateById(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = _baseRoute + "/{id}")]
            HttpRequest req,
            ILogger log,
            int id)
        {
            try
            {
                log.LogInformation("GetItemTemplateById method called.");

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

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

Let’s break down each of the major sections of this class:

  • First, we define the base route for our service url (line #17)
  • Then, we get the ItemTemplate repository from our RepositoryFactory (line #18-19). That repository is used throughout this service. It is static so it lives for the lifetime of the Azure Function app. And, we are using the embedded resource type of repository.
  • We updated the GetItemTemplates method (lines #21-54) to use the IReadableRepository rather than directly accessing the embedded data file:
    • We check that the request parameter was not null.
    • We check that there is a Request.Query variable available (which means we have a query string).
    • Then we get the offset, limit, filters, and sorts variables from the query string by using the QueryStringExtensions defined above.
    • We call the IReadableRepository.GetEntities method to retrieve the filtered/sorts items that were requested.
    • If all goes well, we return the list of ItemTemplates and an OK result.
    • If there are any errors, we log them and continue to throw the exception. The logging helps with tracking issues in production, and the exception means the operation fails and the web service call will result in a 500 error to the client.
  • The GetItemTemplateCount method (lines # 56-83) is similar in structure to GetItemTemplates. If the same filters are passed to the two methods the count and the number of elements returned should be the same.
    • Then passes the filters into the IReadableRepository.GetEntityCount method.
    • This just returns a count of the items that exist in the repository.
    • And logs any errors along the way.
    • In many cases with pagination, GetItemTemplateCount returns the full list of possible results, and GetItemTemplates gets called multiple times for each page (up to the maximum count in the list).
  • The GetItemTemplateById method gets the id for the entity from the url and uses it to find that ItemTemplate in the repository.
    • Using the ASP.NET notation for defining routes, “item/{id}” means that the url must have the form and the {id} portion maps to the id parameter in this method definition.
    • If the id is missing or cannot be converted to an int, then the Azure Functions runtime will throw an error.
    • Then we use the id parameter to pass to the IReadableRepository.GetEntityById method.
    • If all works well, we return the ItemTemplate in an OK result.
    • If the entity id is missing from the repository, we return a NotFound result… which result in a 404 in the service response.
    • If any other error occurs, we log the issue and rethrow the exception.

The ItemTemplateService now represents a fully functional query service for ItemTemplates. We can query and filter lists of templates and retrieve a specific one by id. We have built the capabilities to support pagination in our WebApp. And we have built this in a generic way that will allow us to quickly build the other services that we need to support all of our game data.

If we build and run our Azure Functions app now, we will see that there are now 3 functions running with the access URLs for each.

Fig 1 – Azure Functions List

If we run the count url in our browser (https://localhost:7071/api/list-count), we will get a response of 15. Note: the port in the urls will be different for your local run. You can find the proper urls in your Azure Functions runtime window.

If we run a filtered query for miscellaneous items sorted by name (http://localhost:7071/api/item?filters=category:0), we will get the following:

Fig 2 – Filtered Query for Miscellaneous Items

And if we query for a single ItemTemplate with the id:1001 (https://localhost:7071/api/item/1001), then we will see the following result:

Fig 3 – Query by Id

Our ability to use the web service for read operations is now fairly complete. We built some robust unit tests to validate our expectations of the service code as well. That code is available as part of the commit for this lesson, please review it at your leisure.

With the locally running ItemTemplateService, we have built a our first real web service on Azure Functions. As we can see, the Azure Functions runtime has removed some of the boilerplate code that we would need to build web services. The code in our project is mainly the logic of our service, all of the other magic to map a url request to the method call, serializing the service response, and handling error responses are implemented in the runtime. This gives us a very clean programming model for our service code.

In future lessons we will see how the Azure Functions runtime works in the Azure environment, but for now we can see how it behaves locally.

For our next lesson, we’re going to learn more about microservices and implement a service for reading MonsterTemplates.

2 thoughts on “Lesson 5.7 Design REST Service for Data Retrieval

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