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 typedInMemoryReadRepository
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 theIQueryCollection
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 theGetValue<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 callsSplitCompoundEntry
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:
- [GET] https://localhost:7071/api/item (retrieves list of
ItemTemplates
). - [GET] https://localhost.7071/api/item/1001 (retrieves the
ItemTemplate
for element 1001).
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 ourRepositoryFactory
(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 theIReadableRepository
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.
- We check that the
- The
GetItemTemplateCount
method (lines # 56-83) is similar in structure toGetItemTemplates
. If the same filters are passed to the two methods the count and the number of elements returned should be the same.- The route defined for this method is: https://7071/api/item-count.
- It uses the query string to find any filters.
- 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, andGetItemTemplates
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 thatItemTemplate
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.

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:

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:

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”