Lesson 5.6: Build Repository Pattern for Data Access

With our simple Azure Functions web service in place, we can now focus on providing more complex functionality from our service. We will focus on read-only service features for now, since we’re reading game data and no persisting any data yet. We want to build a service that will return lists of elements based on simple search and paging criteria… to support web app operations for filtering, sorting, and pagination. Many apps require this functionality, so it would be good to have a common mechanism to support it.

Also, because we want to investigate various backend Azure storage technologies, it makes sense to create a layer of abstraction between our service logic and the backend data access components. We will implement this abstraction using the Repository pattern, so that the storage logic can change without having to re-write the service logic.

A repository performs the tasks of an intermediary between the domain model layers and data mapping, acting in a similar way to a set of domain objects in memory. Client objects declaratively build queries and send them to the repositories for answers. Conceptually, a repository encapsulates a set of objects stored in the database and operations that can be performed on them, providing a way that is closer to the persistence layer. Repositories, also, support the purpose of separating, clearly and in one direction, the dependency between the work domain and the data allocation or mapping.

Martin Fowler

To implement this pattern, we will:

  • Define a Repository interface that our services will use when they want to get objects from our storage backend.
  • Implementations of the Repository interface for each storage technology we want to support.
  • Generic Repository implementations since the code to retrieve types is the same but the types being used or returned are specific.
  • Rich test coverage of the repository logic.

Let’s start by defining the Repository interface.

Repository Interface – IReadableRepository<T, TId>

The first design choice we made was to define an interface for repositories that support read operations (the IReadableRepository interface), which are separate from an interface for edit operations (create, update, delete). Since we will have some repositories that are read-only and others that are read-write (in future lessons), this is a separation that allows us to use the same read operations in either case.

The next design decision was using generics to build the interface and implementation classes, so that they could be used across different input and return types. T is the type of objects operated upon by the repository. TId is the type for the unique identifier of objects in the storage system, which allows us to define identifiers as ints or Guids or some other identifier depending on our requirements. Note: if you need a refresher on C# generics, please read this article.

These design choices were made before we’ve even written a line of code… Let’s take a look at the code to see how this all works.

1. We define a new base class for all of our data objects called NamedElement in the SimpleRPG.Game.Services project and DTO folder.

using System;

namespace SimpleRPG.Game.Services.DTO
{
    public abstract class NamedElement<T> : IEquatable<NamedElement<T>>
        where T : struct
    {
        private string name;

        protected NamedElement(T id, string name)
        {
            Id = id;
            Name = name;
        }

        protected NamedElement()
        {
        }

        public T Id { get; set; }

        public string Name
        {
            get => name;
            set => name = !string.IsNullOrEmpty(value) ? value : throw new ArgumentNullException(nameof(Name));
        }

        public override string ToString()
        {
            return $"{Name} [{Id}]";
        }

        public bool Equals(NamedElement<T> other)
        {
            return Id.Equals(other.Id);
        }

        public override bool Equals(object obj)
        {
            if (obj is NamedElement<T> element)
            {
                return Equals(element);
            }

            return false;
        }

        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }
    }
}

This is a simple abstract data class that all of our top-level repository data transfer objects will derive from. It ensures that we have a minimal structure for a data entity of Id and Name. As we can see the Id property uses a generic definition, so the identifier can be any reference type. Line #6 constrains the types than can be used for this class.

This class also implements the IEquatable interface, so that we can implement custom equality operations. In this code, any elements that have equal Id values are considered equal. The default implementation is reference equality, which we want to override. As we can see, we override a couple of Equals and GetHashCode methods to ensure they all follow the same behavior.

This is a simple type, but enables some powerful capabilities in equality and in our repository later.

2. Derive ItemTemplate from this new base.

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

        public int Price { get; set; }

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

        public int Heals { get; set; }
    }
}

Initially, our ItemTemplate class had its own definition of Id and Name properties, so we modified the class to derive from NamedElement with a type of int to match our existing definition. This preserves the properties we had and gives us the NamedElement implementation.

3. Define a NameValuePair data class in the DTO folder. We are going to use property bags to pass some unstructured filter and sorting data between our service and repository, so the NameValuePair class helps us do that.

using System;

namespace SimpleRPG.Game.Services.DTO
{
    public class NameValuePair<TValue> : IEquatable<string>
    {
        private string name;

        public NameValuePair(string name, TValue value)
        {
            Name = name;
            Value = value;
        }

        public NameValuePair()
        {
        }

        public string Name
        {
            get => name;
            set => name = !string.IsNullOrEmpty(value) ? value : throw new ArgumentNullException(nameof(Name));
        }

        public TValue Value { get; set; }

        public override string ToString()
        {
            return $"[{Name}] = {Value}";
        }

        public bool Equals(string other)
        {
            return Name.Equals(other, StringComparison.InvariantCulture);
        }

        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }

            var other = (NameValuePair<TValue>)obj;
            return Equals(other.Name);
        }

        public override int GetHashCode()
        {
            return Name.GetHashCode();
        }
    }
}

This defines a pair with a string name and any type as its value. This is similar in structure to elements in a Dictionary, but is a public type that we can use in multiple places. We could have also used the .NET Core Tuple class for this as well (since the structure isn’t extremely important), but I prefer to use a specific type for clarity of purpose.

This class also implements the IEquatable interface to test for equal objects by their Name property.

4. Define the IReadableRepository interface in the SimpleRPG.Game.Services project and Repositories folder.

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

namespace SimpleRPG.Game.Services.Repositories
{
    public interface IReadableRepository<T, TId>
        where T : NamedElement<TId>
        where TId : struct
    {
        Task<IEnumerable<T>> GetEntities(
            int? offset,
            int? limit,
            IEnumerable<NameValuePair<string>> filters,
            IEnumerable<NameValuePair<string>> sorts);

        Task<int> GetEntityCount(IEnumerable<NameValuePair<string>> filters);

        Task<T> GetEntityById(TId id);

        Task<IEnumerable<T>> GetBulkEntitiesById(IEnumerable<TId> ids);
    }
}

Let’s look at each section of this interface definition:

  • Line #7, the interface is defined with 2 generic parameters. The first is for the type we retrieve via the read operations. The second is the type of the entity’s Id property, which allows us to support multiple forms of identity – i.e. int and Guid.
  • Line #8, constrains the element type to only allow classes derived from NamedElement<TId>. Therefore all of our top-level data transfer objects returned from our repositories must derive from NamedElement.
  • Line #9, constrains the id type to be a value type.
  • All of the repository methods are async, so they have a Task<T> return type. Because these methods are returning data that can be retrieved from external data sources, we want the operations to be asynchronous so they don’t block the processing of our Azure Functions.
  • The GetEntities method (line #11-15) retrieves a list of elements. If it’s called with all null parameters, then it would return the list of all entities in the repository. Many times repositories have an upper limit to prevent large query results from degrading performance.
    • offset is in pagination to set the starting position of the retrieval cursor. For example if we wanted the start of the third page (with 10 items per page), the value would be 21.
    • limit is also used for pagination or to limit the total result returned. For example if we wanted to get the top 10 results, the value would be 10.
    • filters is a list of name-value pairs that represents simple filters we apply to our query. These are typically filters that users can specify in the user interface.
    • sorts is also a list of name-value pairs that represents how the return data should be sorted (which properties to sort by).
  • This method definition is a bit complex, but gives great flexibility to our web service API. For simple queries, these filters and sorts can be used. If we have a custom query with complex logic and requirements, then we could be a specific GetMyComplexData method, but that would be an extension to the IReadableRespository interface.
  • The GetEntityCount method (line #17) returns the count of the list represented by the filters. This call is used many times in conjunction with pagination and multiple GetEntities methods to build that functionality.
  • GetEntityById method (line #19) retrieves a single entity by its Id property. The search parameter is of type TId. There should only be a single item with a given id.
  • Finally, the GetBulkEntitiesById method (line #21) takes a list of ids and returns the list of corresponding entities. The entities are returned in the order of requested ids. This could be useful in returning the list of item information that our player is carrying… all in one request, without having to do a query for each item.

This is a very general purpose interface that can be used for all of our game data repositories. We just have to specify the data transfer type that we are intending to retrieve. This repository interface may also be useful in other web service projects… I typically define an interface like this in a common library for all of my services to share.

Build InMemoryReadRepository Class

With our interface and types defined, we need an implementation class to make use of interface. Since so much of our code can be shared between repository implementations, we are going to build a base class to manage in-memory repositories. These are lists kept in memory (or easily loaded) and manipulated by our web services – this is how we will use the repository pattern for our embedded resource data files.

The InMemoryReadRepository class is also a generic class that implements the IReadableRepository interface. That way consuming code only communicates via the IReadableRepository interface and doesn’t know which repository implementation is actually being used. This layer of abstraction makes our service logic portable to different storage technologies.

We’ll start with the full source listing in the SimpleRPG.Game.Services project and Repositories folder, and then review each method for what it does.

using SimpleRPG.Game.Services.DTO;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Services.Repositories
{
    public class InMemoryReadRepository<T, TId> : IReadableRepository<T, TId>
        where T : NamedElement<TId>
        where TId : struct
    {
        private readonly Dictionary<string, Func<T, string, bool>> filterChecks = new Dictionary<string, Func<T, string, bool>>();
        private readonly Dictionary<string, Func<T, object>> sortChecks = new Dictionary<string, Func<T, object>>();

        public InMemoryReadRepository(
            IList<T> entities,
            IList<NameValuePair<Func<T, string, bool>>> knownFilters = null,
            IList<NameValuePair<Func<T, object>>> knownSorts = null)
            : this()
        {
            Entities = entities ?? throw new ArgumentNullException(nameof(entities));

            if (knownFilters != null)
            {
                foreach (var f in knownFilters)
                {
                    filterChecks.Add(f.Name, f.Value);
                }
            }

            if (knownSorts != null)
            {
                foreach (var s in knownSorts)
                {
                    sortChecks.Add(s.Name, s.Value);
                }
            }
        }

        public InMemoryReadRepository()
        {
            filterChecks.Add("Name", (f, v) => f.Name.Contains(v, StringComparison.InvariantCultureIgnoreCase));

            sortChecks.Add("Name", p => p.Name);
            sortChecks.Add("Id", p => p.Id);
        }

        protected virtual IList<T> Entities { get; set; } = new List<T>();

        public Task<IEnumerable<T>> GetEntities(
            int? offset,
            int? limit,
            IEnumerable<NameValuePair<string>> filters,
            IEnumerable<NameValuePair<string>> sorts)
        {
            var o = offset ?? 0;
            var l = limit ?? Entities.Count;
            var filtered = ApplyFilters(Entities, filters);
            var sorted = GetSortedEntities(filtered, sorts);

            return Task.FromResult(sorted.Skip(o).Take(l).ToList().AsEnumerable());
        }

        public Task<int> GetEntityCount(IEnumerable<NameValuePair<string>> filters)
        {
            var filtered = ApplyFilters(Entities, filters);
            return Task.FromResult(filtered.Count());
        }

        public Task<T> GetEntityById(TId id)
        {
            var spell = Entities.FirstOrDefault(t => t.Id.Equals(id));
            if (spell == null)
            {
                throw new EntityNotFoundException(nameof(id), id);
            }

            return Task.FromResult(spell);
        }

        public Task<IEnumerable<T>> GetBulkEntitiesById(IEnumerable<TId> ids)
        {
            _ = ids ?? throw new ArgumentNullException(nameof(ids));

            var entities = (List<T>)Entities;
            var list = entities.FindAll(p => ids.Contains(p.Id));
            return Task.FromResult(list.OrderBy(e => e.Name).ToList().AsEnumerable());
        }

        protected virtual IEnumerable<T> ApplyFilters(
            IEnumerable<T> items,
            IEnumerable<NameValuePair<string>> filters)
        {
            if (filters == null)
            {
                return items;
            }

            foreach(var f in filters)
            {
                if (filterChecks.TryGetValue(f.Name, out var filterAction))
                {
                    items = items.Where(p => filterAction.Invoke(p, f.Value)).ToList();
                }
            }

            return items;
        }

        protected virtual IEnumerable<T> GetSortedEntities(
            IEnumerable<T> items,
            IEnumerable<NameValuePair<string>> sorts)
        {
            if (sorts == null)
            {
                if (sortChecks.TryGetValue("Name", out var sortAction))
                {
                    items = items.OrderBy(p => sortAction.Invoke(p)).ToList();
                }

                return items;
            }

            foreach (var s in sorts)
            {
                if (sortChecks.TryGetValue(s.Name, out var sortAction))
                {
                    if (s.Value.ToLower() == "dsc")
                    {
                        items = items.OrderByDescending(p => sortAction.Invoke(p)).ToList();
                    }
                    else
                    {
                        items = items.OrderBy(p => sortAction.Invoke(p)).ToList();
                    }
                }
            }

            return items;
        }
    }
}
  • Right from the class declaration (lines 39-11), we see that InMemoryReadRepository is a generic class with the same types and constraints as the IReadableRepository interface. The class implements the methods required by the interface too.
  • Then we declare two member variables (lines # 13-14), which are dictionaries that hold functions responsible to filtering and sorting our types. These functions contain the type specific logic for corresponding Linq operations. These are anonymous functions that will be called later. We will describe their use in later methods.
  • We provide 2 constructors:
    • One initializes the Entities list and the filtering/sorting functions. This allows derived types and factories to provide type specific checks.
    • Also the default constructor which starts with an empty Entities list. Then it adds filter logic for filtering by element Name, and sorting functions for Id and Name. (Since we are working with NamedElement derived classes, we know that they all have Id and Name properties).
  • The protected Entities property (line #49) is only accessible to this or derived classes. It holds our list in memory and operations are performed upon it.
  • GetEntities method (lines # 51-63) orchestrates the steps to querying for the entities:
    • first it ensures values for offset and limit, even though they are optional (by using defaults).
    • then calls the ApplyFilters method over the whole list to get a filtered list.
    • then calls the GetSortedEntities method over the filtered list.
    • finally it returns the Linq query of the filtered list with the appropriate offset and limit.
    • because this method is async while all of its internal code is synchronous, we return a Task with our result. When we use asynchronous storage APIs (for Azure Store or CosmosDB), the async methods will make more sense.
  • GetEntityCount method (lines #65-69) just applies the specified filters (via the same ApplyFilters method) to the full Entities list and then returns its count. We only apply filters in this code path because sorting doesn’t affect the number of entities returned.
  • GetEntityById method (lines #71-80) uses the specified id parameter to search the Entities list for that item. It uses the First operation, so will only return the first, single element with that id, but they should be unique. If the id specified cannot be found in this repository, we throw an EntityNotFoundException (we will describe that exception class later).
  • GetBulkEntitiesById method (lines #82-89) takes a list of ids and finds the list of Entities that they represent. This uses the Linq FindAll operation to find the set that match.
  • ApplyFilters method (lines #91-109) does the heavy lifting of finding and applying filters using the Linq Where operation.
    • If no filters were applied, the method just returns the full list.
    • For every filter that was passed into the call, it looks up the filter function in the filterChecks dictionary using its Name.
    • If that filter check is found in the dictionary, then we add a Where clause and invoke the filter function from that dictionary as the predicate… thereby reducing our return list. Where operations will be chained for multiple filters.
    • If a filter is passed to this method but it is not found in the filterChecks dictionary, then that filter operation is skipped.
  • GetSortedEntities method (lines # 111-141), similar to ApplyFilters, performs the logic of sorting the specified list of entities.
    • First, if there is no sorting specified, then the default behavior is to sort by Name. Without any sort applied the elements would return in their order in the list, but that’s not usually a very intuitive order, so sorting by name is a reasonable default.
    • For every sort entry that was passed into the call, it looks up the sort function in the sortChecks dictionary using its Name.
    • If that sort check is found in the dictionary, then we add an OrderBy or OrderByDescending clause and invoke the sort function from that dictionary as the predicate… thereby sorting our return list. OrderBy and OrderByDescending operations will be chained for multiple sort entries.
    • The sort entry Value can either specify asc (ascending) or dsc (descending) sort order, but it defaults to ascending (if dsc is not specifically used).
    • If a sort entry is passed to this method but it is not found in the sortChecks dictionary, then that sort operation is skipped.

This is a very generalized class with a lot of code, but it performs some powerful operations on in-memory data lists. Because of its general implementation, it can and will be used as the base class for our in-memory repositories in future lessons.

Finally, let’s look at the simple EntityNotFoundException class in the Repositories folder. We use a specific exception for this rather than the general .NET exception that could have been sent, so that we can take actions on it in our service.

using System;

namespace SimpleRPG.Game.Services.Repositories
{
    public class EntityNotFoundException : Exception
    {
        public EntityNotFoundException(string entityIdName, object entityIdValue, Exception innerException = null)
            : base($"Entity with {entityIdName} = {entityIdValue} was not found in repository.", innerException)
        {
            this.EntityIdName = entityIdName;
            this.EntityIdValue = entityIdValue;
        }

        public string EntityIdName { get; private set; }

        public object EntityIdValue { get; private set; }
    }
}

This repository implementation is the type of code that I like to implement once and use everywhere, so it is also a good candidate for code in a common library. The beauty of sharing this code is that we can fully test the behavior with all of the cases we need to support and then just use them in all of our service projects without having to worry about the implementation details.

Testing the InMemoryReadRepository

To verify the functionality of the in-memory repository, we created a set of tests that go through the various capabilities described above and ensure that the InMemoryReadRepository class meets all of those requirements. We’re not going to exhaustively describe all of the tests, but just how to get the testing mocks and infrastructure in place with a few example tests.

We’ll start with a data class that derives from NamedElement called TestElement.

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

namespace SimpleRPG.Game.Services.Tests.Mocks
{
    public class TestEntity : NamedElement<int>
    {
        public int Value { get; set; }

        public DateTime ModifiedDate { get; set; }

        public string Author { get; set; }
    }
}

Then, we define a derived repository called MemoryTestRepository. It derives from InMemoryReadRepository and just configures it with the right starting data (5 test entities), known filters for Value and Author, and known sorting for Value and Author.

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

namespace SimpleRPG.Game.Services.Tests.Mocks
{
    public class MemoryTestRepository : InMemoryReadRepository<TestEntity, int>
    {
        private static readonly List<TestEntity> AllElements = new List<TestEntity>
        {
            new TestEntity
            {
                Id = 1,
                Name = "Element1",
                Value = 42,
                ModifiedDate = DateTime.Today,
                Author = "DarthPedro",
            },
            new TestEntity
            {
                Id = 2,
                Name = "Element2",
                Value = 3,
                ModifiedDate = DateTime.Today,
                Author = "DarthPedro",
            },
            new TestEntity
            {
                Id = 33,
                Name = "Element3",
                Value = 17,
                ModifiedDate = DateTime.Today,
                Author = "DarthPedro",
            },
            new TestEntity
            {
                Id = 4,
                Name = "Element4",
                Value = 45,
                ModifiedDate = DateTime.Today,
                 Author = "DarthPedro",
            },
            new TestEntity
            {
                Id = 5,
                Name = "Element5",
                Value = 46,
                ModifiedDate = DateTime.Today,
                Author = "DarthPedro-2",
            },
        };

        private static readonly List<NameValuePair<Func<TestEntity, string, bool>>> knownFilters = 
            new List<NameValuePair<Func<TestEntity, string, bool>>>
            { 
                new NameValuePair<Func<TestEntity, string, bool>>("Value", (f, v) => f.Value == Convert.ToInt32(v)),
                new NameValuePair<Func<TestEntity, string, bool>>("Author", (f, v) => f.Author.Contains(v, StringComparison.InvariantCultureIgnoreCase)),
            };

        private static readonly List<NameValuePair<Func<TestEntity, object>>> knownSorts =
            new List<NameValuePair<Func<TestEntity, object>>>
            {
                new NameValuePair<Func<TestEntity, object>>("Value", p => Convert.ToInt32(p.Value)),
                new NameValuePair<Func<TestEntity, object>>("Author", p => p.Author)
            };

        public MemoryTestRepository(List<TestEntity> initialElements)
            : base(initialElements)
        {
        }

        public MemoryTestRepository()
            : base(new List<TestEntity>(AllElements), knownFilters, knownSorts)
        {
        }
    }
}

Finally let’s look at a couple of tests to see how these mock classes are used, and how we validate the InMemoryReadRepository functionality.

using SimpleRPG.Game.Services.DTO;
using SimpleRPG.Game.Services.Repositories;
using SimpleRPG.Game.Services.Tests.Mocks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace SimpleRPG.Game.Services.Tests.Repositories
{
    public class InMemoryReadRepositoryTests
    {
...
        [Fact]
        public async Task GetEntityById()
        {
            // arrange
            var id = 2;
            var repo = new MemoryTestRepository();

            // act
            var result = await repo.GetEntityById(id).ConfigureAwait(false);

            // assert
            Assert.NotNull(result);
            Assert.Equal(id, result.Id);
            Assert.Equal("Element2", result.Name);
            Assert.Equal(3, result.Value);
            Assert.Equal(DateTime.Today, result.ModifiedDate);
            Assert.Equal("DarthPedro", result.Author);
        }

        [Fact]
        public async Task GetEntities_FirstPage()
        {
            // arrange
            var repo = new MemoryTestRepository();

            // act
            var spells = await repo.GetEntities(0, 2, null, null).ConfigureAwait(false);

            // assert
            Assert.Equal(2, spells.Count());
            Assert.Equal(1, spells.First().Id);
            Assert.Equal(2, spells.Last().Id);
        }

        [Fact]
        public async Task GetEntities_FilteredByMultiple()
        {
            // arrange
            var repo = new MemoryTestRepository();
            var filters = new List<NameValuePair<string>>
            {
                new NameValuePair<string>("Name", "element"),
                new NameValuePair<string>("Author", "DarthPedro-2"),
            };

            // act
            var spells = await repo.GetEntities(null, null, filters, null).ConfigureAwait(false);
            var count = await repo.GetEntityCount(filters);

            // assert
            Assert.NotEmpty(spells);
            Assert.Equal(1, count);
            Assert.Equal(5, spells.First().Id);
        }
...
    }
}

These 3 tests show the structure of the rest. They show how to search for an item by id, get a paged list of items, and filtering items to a sub-list. As we can see the code for retrieving data is very simple – much of its complexity is contained in the InMemoryRepository. The setup code in these tests are also a great blueprint for the code we will need to write in our service layer to integrate with this repository.

If you wish to see the comprehensive test suite for this repository, please review the commit associated with this lesson. It contains test for all of the the scenarios described in this lesson.

In conclusion, we’ve built a powerful abstraction for our data sources using the Repository pattern. We defined a generic interface and class for the repository. And we built tests that show how the repository can be used. In the next lesson, we will update the ItemTemplateService to retrieve its data from a repository rather than directly from the resource file.

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 )

Facebook photo

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

Connecting to %s