paint-brush
A Better Implementation of Enhanced Repository Pattern in .NET C#by@ahmedtarekhasan
361 reads
361 reads

A Better Implementation of Enhanced Repository Pattern in .NET C#

by Ahmed Tarek HasanJanuary 18th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Learn how to implement a better enhanced "Repository" pattern following Best Practices to satisfy extended requirements like throttling. The most interesting part of the requirements was related to the Repository to be implemented. Some code implementation exists for demonstration purposes only. Some best practices would be ignored/dropped in order to drive the main focus to the other best practices targeted on this article.
featured image - A Better Implementation of Enhanced Repository Pattern in .NET C#
Ahmed Tarek Hasan HackerNoon profile picture

Learn how to implement a better enhanced Repository Pattern following Best Practices to satisfy extended requirements like throttling.


The Story

My team and I were going to start working on a big project. It was about a Web Application for a huge company. You can’t imagine tempted I am to reveal its name, but unfortunately, I can’t.


The client had some specific requirements and some of them were actually technical ones. I know this is not a normal thing, but, this client already had their own team of technical people so that they could cope with our team.


Long story short, let me skip the too-long list of requirements and jump to the ones I am actually interested in for this article.


Photo by Glenn Carstens-Peters on Unsplash

Requirements

As I said, our client had some specific requirements and some of them were technical ones. So, let me walk you through some of these interesting requirements and I am sure you would fall in love with them.


The application needed to have a backend to store all the data processed. And, Once I say Backend, you say… Repository.


The most interesting part of the requirements was related to the Repository to be implemented. Therefore, let me tell you about these ones.


  1. Scheduled Automatic Health Check on Repositories

    1. Requirement: The client wanted to be able to design, implement and run automatic health checks on all the repositories on scheduled tasks. The design should allow for blind health checks regardless of the repository type.
    2. Design: For the design to allow this, there should be some high-level abstraction of all the repositories in the system. This abstraction layer should expose as most as possible of the common APIs of all the repositories.
  2. APIs Throttling Restrictions on Queries

    1. Requirement: All queries should allow to apply some variable throttling restrictions to control the amount of data retrieved per call. The throttling criteria could be implemented as run-time logic instead of some hardcoded thresholds. Additionally, the caller should be informed about this and the way to get the rest of the data if possible.
    2. Design: For the design to allow this, we should implement Paging to control the amount of data retrieved per call using the throttling logic. Also, some unified return object should be returned to the caller to inform him about the paging and how to get to the next page,…

Photo by Mikael Seegen on Unsplash

Disclaimer

  1. Some best practices would be ignored/dropped in order to drive the main focus to the other best practices targeted in this article.
  2. All the code could be found on this repository so that you can easily follow it.
  3. Some details of the client and the nature of the project were intentionally not revealed as they are confidential.
  4. Some code implementation exists for demonstration purposes only and I don’t advise using it or mimicking it on production code. On such kind of occasions, a comment would be provided.
  5. Some understanding of other topics and concepts would be required to be able to fully understand the code and design in this article. In such cases, a note -and maybe a link- would be provided.
  6. The paging implementation used in our example is specific for applying paging on memory lists. It is not intended to be used with production code that is using Entity Framework or any other frameworks.
  7. Please use the code on this article as a kickstarter or mind opener. Before going to production, you need to revise the design and adapt it to your own needs.

Photo by Markus Spiske on Unsplash

Better Enhanced Implementation and Analysis


Entities

We would start with a very simple entity representing an employee. However, since our design in general would need some high-level abstraction, we need to have an abstract Entity that would be inherited by all our system entities.


Entity


namespace BetterRepository.Entities
{
    public abstract class Entity
    {
    }
}


For our solution, we will keep it simple and don’t provide any common members for the abstract Entity class. However, for your own solution, you might need to do so.


Employee


using System;
using System.Text;

namespace BetterRepository.Entities
{
    public class Employee : Entity
    {
        public int Id { get; }
        public string Name { get; }

        public Employee(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public Employee(Employee other, int id)
        {
            if (other == null) throw new ArgumentException("Other cannot be null");

            Id = id;
            Name = other.Name;
        }

        // This code is added for demonstration purposes only.
        public override string ToString()
        {
            var builder = new StringBuilder();
            builder.AppendLine($"Id: {Id}, Name: {Name}");
            return builder.ToString();
        }
    }
}


What we can notice here:

  1. This is a simple Employee class which inherits the abstract Entity class.
  2. It has Id and Name.
  3. It is immutable and that’s why we have a constructor to copy all members -except the Id- from another Employee.
  4. We have an override of the ToString method to be used for demonstration purposes only. This is not a best practice to follow.

Paging

If you wish to understand the paging code, I recommend that you first go and check that article.


I would include the paging code here for brevity and as a quick reference.


using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace BetterRepository.Models
{
    public class PagingDescriptor
    {
        public int ActualPageSize { get; private set; }
        public int NumberOfPages { get; private set; }
        public PageBoundry[] PagesBoundries { get; private set; }

        public PagingDescriptor(
            int actualPageSize,
            int numberOfPages,
            PageBoundry[] pagesBoundries)
        {
            ActualPageSize = actualPageSize;
            NumberOfPages = numberOfPages;
            PagesBoundries = pagesBoundries;
        }

        // This code is added for demonstration purposes only.
        public override string ToString()
        {
            var builder = new StringBuilder();
            builder.AppendLine($"ActualPageSize: {ActualPageSize}");
            builder.AppendLine($"NumberOfPages: {NumberOfPages}");
            builder.AppendLine("");
            builder.AppendLine($"PagesBoundries:");
            builder.AppendLine($"----------------");

            foreach (var boundry in PagesBoundries)
            {
                builder.Append(boundry.ToString());
            }

            return builder.ToString();
        }
    }

    public class PageBoundry
    {
        public int FirstItemZeroIndex { get; private set; }
        public int LastItemZeroIndex { get; private set; }

        public PageBoundry(int firstItemZeroIndex, int lastItemZeroIndex)
        {
            FirstItemZeroIndex = firstItemZeroIndex;
            LastItemZeroIndex = lastItemZeroIndex;
        }


        // This code is added for demonstration purposes only.
        public override string ToString()
        {
            var builder = new StringBuilder();
            builder.AppendLine($"FirstItemZeroIndex: {FirstItemZeroIndex}, LastItemZeroIndex: {LastItemZeroIndex}");
            return builder.ToString();
        }
    }

    public static class ListExtensionMethods
    {
        public static PagingDescriptor Page(this IList list, int pageSize)
        {
            var actualPageSize = pageSize;

            if (actualPageSize <= 0)
            {
                actualPageSize = list.Count;
            }

            var maxNumberOfPages =
                (int)Math.Round(Math.Max(1, Math.Ceiling(((float)list.Count) / ((float)actualPageSize))));

            return new PagingDescriptor(
                actualPageSize,
                maxNumberOfPages,
                Enumerable
                    .Range(0, maxNumberOfPages)
                    .Select(pageZeroIndex => new PageBoundry(
                                pageZeroIndex * actualPageSize,
                                Math.Min((pageZeroIndex * actualPageSize) + (actualPageSize - 1), list.Count - 1)
                            )).ToArray()
            );
        }
    }
}


What we can notice here:

  1. This is the same code explained on the other article about Paging.
  2. The only added parts are about overriding the ToString method.
  3. This is done for demonstration purposes only. This is not a best practice to follow.

QueryResult

This leads us to the unified object which is returned from the Get repository calls which is in our case called QueryResult.


using System.Collections.Generic;
using System.Text;
using BetterRepository.Entities;

namespace BetterRepository.Models
{
    public interface IQueryResult
    {
        PagingDescriptor PagingDescriptor { get; }
        int ActualPageZeroIndex { get; }
        IEnumerable<Entity> Results { get; }
    }

    public interface IQueryResult<out TEntity> : IQueryResult where TEntity : Entity
    {
        new IEnumerable<TEntity> Results { get; }
    }

    public class QueryResult<TEntity> : IQueryResult<TEntity> where TEntity : Entity
    {
        public QueryResult(
            PagingDescriptor pagingDescriptor,
            int actualPageZeroIndex,
            IEnumerable<TEntity> results)
        {
            PagingDescriptor = pagingDescriptor;
            ActualPageZeroIndex = actualPageZeroIndex;
            Results = results;
        }

        public PagingDescriptor PagingDescriptor { get; }

        public int ActualPageZeroIndex { get; }

        public IEnumerable<TEntity> Results { get; }

        IEnumerable<Entity> IQueryResult.Results => Results;


        // This code is added for demonstration purposes only.
        public override string ToString()
        {
            var builder = new StringBuilder();
            builder.AppendLine($"ActualPageZeroIndex: {ActualPageZeroIndex}");
            builder.AppendLine("");
            builder.AppendLine($"PagingDescriptor:");
            builder.AppendLine($"----------------");
            builder.AppendLine(PagingDescriptor.ToString());
            builder.AppendLine($"Results:");
            builder.AppendLine($"----------------");

            foreach (var entity in Results)
            {
                builder.Append(entity.ToString());
            }

            return builder.ToString();
        }
    }
}


What we can notice here:

  1. We have the IQueryResult interface which represents the object to be returned from the Get repository calls.
  2. It has PagingDescriptor and ActualPageZeroIndex properties so that the caller would know the exact paging details of the data retrieved by his call.
  3. And, the Results property which is an IEnumerable of the abstract base Entity results.
  4. We defined the IQueryResult<out TEntity> generic interface so that we can have strong typed language support for our specific repositories.
  5. On this interface, we are hiding the IEnumerable<Entity> IQueryResult.Results property and replacing it with a new typed one.
  6. Then comes the QueryResult<TEntity> class which implements the IQueryResult<TEntity> interface.
  7. We have an override of the ToString method to be used for demonstration purposes only. This is not a best practice to follow.

AddOrUpdateDescriptor

We would support different methods on our repositories including AddOrUpdate methods. This would make the caller life easier whenever needed.


For that, we need to define a unified object structure to be returned from the AddOrUpdate method.


namespace BetterRepository.Models
{
    public enum AddOrUpdate
    {
        Add,
        Update
    }

    public interface IAddOrUpdateDescriptor
    {
        AddOrUpdate ActionType { get; }
        int Id { get; }
    }

    public class AddOrUpdateDescriptor : IAddOrUpdateDescriptor
    {
        public AddOrUpdate ActionType { get; }
        public int Id { get; }

        public AddOrUpdateDescriptor(AddOrUpdate actionType, int id)
        {
            ActionType = actionType;
            Id = id;
        }
    }
}


What we can notice here:

  1. We defined the IAddOrUpdateDescriptor interface.
  2. Also defined the AddOrUpdateDescriptor class which implements IAddOrUpdateDescriptor and it is immutable.
  3. Also defined the enum AddOrUpdate.

Query Repository

Now we move to our repository definitions. We would split our repository methods into two parts; Queries and Commands. I can hear you saying Command and Query Responsibility Segregation (CQRS).


Yes, our design would embrace the same concept of CQRS but it is not fully implemented here. So, please don’t judge this design as an incomplete CQRS as it was not intended to be in the first place.


IQueryRepository


public interface IQueryRepository
{
    IQueryResult<Entity> GetAll();
    Entity Get(int id);
    IQueryResult<Entity> Get(int pageSize, int pageIndex);
}


What we can notice here:

  1. This is our IQueryRepository interface which represents any non-generic Query Repository.
  2. We defined Entity Get(int id) method to get an entity by Id.
  3. We defined IQueryResult<Entity> GetAll() method to get all the entities inside the implementing repository. Please keep in mind that throttling restrictions should be applied and that’s why we are not just returning a list of entities, we are returning IQueryResult.
  4. We defined IQueryResult<Entity> Get(int pageSize, int pageIndex) method to get entities when divided into certain page size. Please keep in mind that throttling restrictions should be applied here as well. Therefore, if the throttling threshold is somehow set to 5, and the caller is requesting to get entities in pages of 8 entities per page, an adaptation would be applied and the caller would be aware of it by using the returned IQueryResult object.


IQueryRepository<TEntity>


public interface IQueryRepository<TEntity> : IQueryRepository where TEntity : Entity
{
    new IQueryResult<TEntity> GetAll();
    new TEntity Get(int id);
    new IQueryResult<TEntity> Get(int pageSize, int pageIndex);
    IQueryResult<TEntity> GetByExpression(Expression<Func<TEntity, bool>> predicate);
    IQueryResult<TEntity> GetByExpression(Expression<Func<TEntity, bool>> predicate, int pageSize, int pageIndex);
}


We defined IQueryResult<TEntity> GetByExpression(Expression<Func<TEntity, bool>> predicate) method to get entities after filtering them using the passed in predicate. Please keep in mind that throttling restrictions should be applied here as well.


Additionally, we defined IQueryResult<TEntity> GetByExpression(Expression<Func<TEntity, bool>> predicate, int pageSize, int pageIndex) method to apply paging after filtering the entities using the passed in predicate. Please keep in mind that throttling restrictions should be applied here as well.


QueryRepository<TEntity>


public abstract class QueryRepository<TEntity> : IQueryRepository<TEntity> where TEntity : Entity
{
    public abstract IQueryResult<TEntity> GetAll();

    public abstract IQueryResult<TEntity> Get(int pageSize, int pageIndex);

    public abstract TEntity Get(int id);

    public abstract IQueryResult<TEntity> GetByExpression(Expression<Func<TEntity, bool>> predicate);

    public abstract IQueryResult<TEntity> GetByExpression(Expression<Func<TEntity, bool>> predicate, int pageSize, int pageIndex);


    IQueryResult<Entity> IQueryRepository.GetAll()
    {
        return GetAll();
    }

    Entity IQueryRepository.Get(int id)
    {
        return Get(id);
    }

    IQueryResult<Entity> IQueryRepository.Get(int pageSize, int pageIndex)
    {
        return Get(pageSize, pageIndex);
    }
}


What we can notice here:

  1. This is an abstract class implementing IQueryRepository<TEntity>.
  2. The implementation of all the Get methods coming from the IQueryRepository<TEntity> interface would be delegated to the child classes inheriting from QueryRepository<TEntity>.
  3. For IQueryResult<Entity> IQueryRepository.GetAll(), it is defaulted to call the other IQueryResult<TEntity> GetAll() which would be implemented by child classes.
  4. And for Entity IQueryRepository.Get(int id), it is defaulted to call the other TEntity Get(int id) which would be implemented by child classes.
  5. And for IQueryResult<Entity> IQueryRepository.Get(int pageSize, int pageIndex), it is defaulted to call the other IQueryResult<TEntity> Get(int pageSize, int pageIndex) which would be implemented by child classes.

Command Repository

Here are the definitions related to any Command Repository.


ICommandRepository


public interface ICommandRepository
{
    int Add(Entity entity);
    IEnumerable<int> Add(IEnumerable<Entity> entities);
    void Update(Entity entity);
    void Update(IEnumerable<Entity> entities);
    IAddOrUpdateDescriptor AddOrUpdate(Entity entity);
    IEnumerable<IAddOrUpdateDescriptor> AddOrUpdate(IEnumerable<Entity> entities);
    bool Delete(int id);
    bool Delete(Entity entity);
    IDictionary<int, bool> Delete(IEnumerable<Entity> entities);
}


What we can notice here:

  1. This is our ICommandRepository interface which represents any non-generic Command Repository.
  2. We defined all the required methods to interact with our repository. Methods like AddUpdateAddOrUpdate, and Delete.


ICommandRepository<in TEntity>


public interface ICommandRepository<in TEntity> : ICommandRepository where TEntity : Entity
{
    int Add(TEntity entity);
    IEnumerable<int> Add(IEnumerable<TEntity> entities);
    void Update(TEntity entity);
    void Update(IEnumerable<TEntity> entities);
    IAddOrUpdateDescriptor AddOrUpdate(TEntity entity);
    IEnumerable<IAddOrUpdateDescriptor> AddOrUpdate(IEnumerable<TEntity> entities);
    bool Delete(TEntity entity);
    IDictionary<int, bool> Delete(IEnumerable<TEntity> entities);

    abstract int ICommandRepository.Add(Entity entity);
    abstract IEnumerable<int> ICommandRepository.Add(IEnumerable<Entity> entities);
    abstract void ICommandRepository.Update(Entity entity);
    abstract void ICommandRepository.Update(IEnumerable<Entity> entities);
    abstract IAddOrUpdateDescriptor ICommandRepository.AddOrUpdate(Entity entity);
    abstract IEnumerable<IAddOrUpdateDescriptor> ICommandRepository.AddOrUpdate(IEnumerable<Entity> entities);
    abstract bool ICommandRepository.Delete(Entity entity);
    abstract IDictionary<int, bool> ICommandRepository.Delete(IEnumerable<Entity> entities);
}


What worth mentioning here is that starting from C#8.0, you can add this to an interface.

abstract int ICommandRepository.Add(Entity entity);


In our case, this means that although ICommandRepository<in TEntity> extends ICommandRepository, any class implementing ICommandRepository<in TEntity> interface would not expose int Add(Entity entity) method unless it is casted -implicitly or explicitly- into the non-generic ICommandRepository interface.


CommandRepository<TEntity>


public abstract class CommandRepository<TEntity> : ICommandRepository<TEntity> where TEntity : Entity
{
    public abstract bool Delete(int id);
    public abstract int Add(TEntity entity);
    public abstract IEnumerable<int> Add(IEnumerable<TEntity> entities);
    public abstract void Update(TEntity entity);
    public abstract void Update(IEnumerable<TEntity> entities);
    public abstract IAddOrUpdateDescriptor AddOrUpdate(TEntity entity);
    public abstract IEnumerable<IAddOrUpdateDescriptor> AddOrUpdate(IEnumerable<TEntity> entities);
    public abstract bool Delete(TEntity entity);
    public abstract IDictionary<int, bool> Delete(IEnumerable<TEntity> entities);


    int ICommandRepository.Add(Entity entity)
    {
        if (entity.GetType() == typeof(TEntity))
        {
            return Add(entity as TEntity);
        }

        throw new ArgumentException(
            $"The type \"{entity.GetType()}\" does not match the type \"{typeof(TEntity)}\"");
    }

    IEnumerable<int> ICommandRepository.Add(IEnumerable<Entity> entities)
    {
        if (entities is IEnumerable<TEntity>)
        {
            return Add(entities.Select(e => e as TEntity));
        }

        throw new ArgumentException(
            $"The type \"{entities.GetType()}\" does not match the type \"{typeof(IEnumerable<TEntity>)}\"");
    }

    void ICommandRepository.Update(Entity entity)
    {
        if (entity.GetType() == typeof(TEntity))
        {
            Update(entity as TEntity);
        }
        else
        {
            throw new ArgumentException(
                $"The type \"{entity.GetType()}\" does not match the type \"{typeof(TEntity)}\"");
        }
    }

    void ICommandRepository.Update(IEnumerable<Entity> entities)
    {
        if (entities is IEnumerable<TEntity>)
        {
            Update(entities.Select(e => e as TEntity));
        }
        else
        {
            throw new ArgumentException(
                $"The type \"{entities.GetType()}\" does not match the type \"{typeof(IEnumerable<TEntity>)}\"");
        }
    }

    IAddOrUpdateDescriptor ICommandRepository.AddOrUpdate(Entity entity)
    {
        if (entity.GetType() == typeof(TEntity))
        {
            return AddOrUpdate(entity as TEntity);
        }

        throw new ArgumentException(
            $"The type \"{entity.GetType()}\" does not match the type \"{typeof(TEntity)}\"");
    }

    IEnumerable<IAddOrUpdateDescriptor> ICommandRepository.AddOrUpdate(IEnumerable<Entity> entities)
    {
        if (entities is IEnumerable<TEntity>)
        {
            return AddOrUpdate(entities.Select(e => e as TEntity));
        }

        throw new ArgumentException(
            $"The type \"{entities.GetType()}\" does not match the type \"{typeof(IEnumerable<TEntity>)}\"");
    }

    bool ICommandRepository.Delete(Entity entity)
    {
        if (entity.GetType() == typeof(TEntity))
        {
            return Delete(entity as TEntity);
        }

        throw new ArgumentException(
            $"The type \"{entity.GetType()}\" does not match the type \"{typeof(TEntity)}\"");
    }

    IDictionary<int, bool> ICommandRepository.Delete(IEnumerable<Entity> entities)
    {
        if (entities is IEnumerable<TEntity>)
        {
            return Delete(entities.Select(e => e as TEntity));
        }

        throw new ArgumentException(
            $"The type \"{entities.GetType()}\" does not match the type \"{typeof(IEnumerable<TEntity>)}\"");
    }
}


What we can notice here:

  1. This is an abstract class implementing ICommandRepository<TEntity>.
  2. The implementation of all the methods coming from the ICommandRepository<TEntity> interface would be delegated to the child classes inheriting from CommandRepository<TEntity>.
  3. For int ICommandRepository.Add(Entity entity), it is defaulted to call the other int Add(TEntity entity) which would be implemented by child classes, however, we added an extra check on the passed in type to make sure it matches the expected one.
  4. And for IEnumerable<int> ICommandRepository.Add(IEnumerable<Entity> entities), it is defaulted to call the other IEnumerable<int> Add(IEnumerable<TEntity> entities) which would be implemented by child classes, however, we added an extra check on the passed in type to make sure it matches the expected one.
  5. And for void ICommandRepository.Update(Entity entity), it is defaulted to call the other void Update(TEntity entity) which would be implemented by child classes, however, we added an extra check on the passed in type to make sure it matches the expected one.
  6. And for void ICommandRepository.Update(IEnumerable<Entity> entities), it is defaulted to call the other void Update(IEnumerable<TEntity> entities) which would be implemented by child classes, however, we added an extra check on the passed in type to make sure it matches the expected one.
  7. And for IAddOrUpdateDescriptor ICommandRepository.AddOrUpdate(Entity entity), it is defaulted to call the other IAddOrUpdateDescriptor AddOrUpdate(TEntity entity) which would be implemented by child classes, however, we added an extra check on the passed in type to make sure it matches the expected one.
  8. And for IEnumerable<IAddOrUpdateDescriptor> ICommandRepository.AddOrUpdate(IEnumerable<Entity> entities), it is defaulted to call the other IEnumerable<IAddOrUpdateDescriptor> AddOrUpdate(IEnumerable<TEntity> entities) which would be implemented by child classes, however, we added an extra check on the passed in type to make sure it matches the expected one.
  9. And for bool ICommandRepository.Delete(Entity entity), it is defaulted to call the other bool Delete(TEntity entity) which would be implemented by child classes, however, we added an extra check on the passed in type to make sure it matches the expected one.
  10. And for IDictionary<int, bool> ICommandRepository.Delete(IEnumerable<Entity> entities), it is defaulted to call the other IDictionary<int, bool> Delete(IEnumerable<TEntity> entities) which would be implemented by child classes, however, we added an extra check on the passed in type to make sure it matches the expected one.

Employee Query and Command Repositories

Now, this is the time to implement our Employee Query and Command repositories. This is where we start inheriting from our base abstract CommandRepository<TEntity> and QueryRepository<TEntity> classes.


However, first, let’s clarify that for our example here we are not going to use a real database for storing our Employees data. What we are going to do instead, is to create a static list.


EmployeePersistence


internal static class EmployeePersistence
{
    public static readonly List<Employee> Employees = new();

    static EmployeePersistence()
    {
        Reset();
    }

    public static void Reset()
    {
        Employees.Clear();
        Employees.Add(new Employee(0, "Ahmed"));
        Employees.Add(new Employee(1, "Tarek"));
        Employees.Add(new Employee(2, "Patrick"));
        Employees.Add(new Employee(3, "Mohamed"));
        Employees.Add(new Employee(4, "Sara"));
        Employees.Add(new Employee(5, "Ali"));
    }
}


It is as simple as a list of employees.


EmployeeQueryRepository


public class EmployeeQueryRepository : QueryRepository<Employee>
{
    private static int MaxResultsCountPerPage = 5;

    public override IQueryResult<Employee> GetAll()
    {
        return Get(emp => true, null, null);
    }

    public override Employee Get(int id)
    {
        return EmployeePersistence.Employees.FirstOrDefault(e => e.Id == id);
    }

    public override IQueryResult<Employee> Get(int pageSize, int pageIndex)
    {
        return Get(emp => true, pageSize, pageIndex);
    }

    public override IQueryResult<Employee> GetByExpression(Expression<Func<Employee, bool>> predicate)
    {
        return Get(predicate, null, null);
    }

    public override IQueryResult<Employee> GetByExpression(Expression<Func<Employee, bool>> predicate, int pageSize, int pageIndex)
    {
        return Get(predicate, pageSize, pageIndex);
    }

    private static IQueryResult<Employee> Get(Func<Employee, bool> predicate, int? pageSize, int? pageIndex)
    {
        var filteredItems = 
            predicate != null ? 
                EmployeePersistence.Employees.AsQueryable().Where(predicate).ToList() : 
                EmployeePersistence.Employees;

        var finalPageSize = Math.Min(MaxResultsCountPerPage, filteredItems.Count);
        var finalPageIndex = 0;

        if (pageSize != null)
        {
            if (pageSize <= MaxResultsCountPerPage)
            {
                finalPageSize = pageSize.Value;
                finalPageIndex = pageIndex ?? 0;
            }
            else
            {
                finalPageSize = MaxResultsCountPerPage;

                if (pageIndex != null)
                {
                    var oldPagingDescriptor = filteredItems.Page(pageSize.Value);
                    var oldPageBoundries = oldPagingDescriptor.PagesBoundries[pageIndex.Value];
                    var targetedItemZeroIndex = oldPageBoundries.FirstItemZeroIndex;

                    var newPagingDescriptor = filteredItems.Page(finalPageSize);

                    finalPageIndex =
                        newPagingDescriptor
                            .PagesBoundries
                            .ToList()
                            .FindIndex(i => i.FirstItemZeroIndex <= targetedItemZeroIndex && i.LastItemZeroIndex >= targetedItemZeroIndex);
                }
            }
        }

        var pagingDescriptor = filteredItems.Page(finalPageSize);
        var pageBoundries = pagingDescriptor.PagesBoundries[finalPageIndex];
        var from = pageBoundries.FirstItemZeroIndex;
        var to = pageBoundries.LastItemZeroIndex;

        return new QueryResult<Employee>(pagingDescriptor, finalPageIndex, filteredItems.Skip(from).Take(to - from + 1));
    }
}


What we can notice here:

  1. The EmployeeQueryRepository class is inheriting from QueryRepository<Employee>.
  2. We don’t have to implement the methods coming from the non-generic IQueryRepository interface as they have been implemented in the abstract base QueryRepository<TEntity> class.
  3. All the Get methods are finally calling the centralized logic in the IQueryResult<Employee> Get(Func<Employee, bool> predicate, int? pageSize, int? pageIndex) method where the magic happens.
  4. On this method, the throttling is happening using the static MaxResultsCountPerPage and the Page extension method we implemented on the Paging section.


EmployeeCommandRepository


public class EmployeeCommandRepository : CommandRepository<Employee>
{
    public override int Add(Employee entity)
    {
        var newEmployee = new Employee(entity, EmployeePersistence.Employees.Count);
        EmployeePersistence.Employees.Add(newEmployee);
        return newEmployee.Id;
    }

    public override IEnumerable<int> Add(IEnumerable<Employee> entities)
    {
        return entities.Select(Add).ToList();
    }

    public override void Update(Employee entity)
    {
        var foundIndex = EmployeePersistence.Employees.FindIndex(e => e.Id == entity.Id);

        if (foundIndex == -1)
        {
            throw new InvalidOperationException($"Employee with Id \"{entity.Id}\" does not exist.");
        }

        var foundEmployee = EmployeePersistence.Employees[foundIndex];
        var newEmployee = new Employee(entity, foundEmployee.Id);
        EmployeePersistence.Employees.RemoveAt(foundIndex);
        EmployeePersistence.Employees.Insert(foundIndex, newEmployee);
    }

    public override void Update(IEnumerable<Employee> entities)
    {
        foreach (var employee in entities)
        {
            Update(employee);
        }
    }

    public override IAddOrUpdateDescriptor AddOrUpdate(Employee entity)
    {
        var foundIndex = EmployeePersistence.Employees.FindIndex(e => e.Id == entity.Id);

        if (foundIndex != -1)
        {
            Update(entity);
            return new AddOrUpdateDescriptor(Models.AddOrUpdate.Update, entity.Id);
        }
        else
        {
            return new AddOrUpdateDescriptor(Models.AddOrUpdate.Add, Add(entity));
        }
    }

    public override IEnumerable<IAddOrUpdateDescriptor> AddOrUpdate(IEnumerable<Employee> entities)
    {
        return entities.Select(AddOrUpdate).ToList();
    }

    public override bool Delete(int id)
    {
        var foundIndex = EmployeePersistence.Employees.FindIndex(e => e.Id == id);

        if (foundIndex == -1) return false;

        EmployeePersistence.Employees.RemoveAt(foundIndex);
        return true;
    }

    public override bool Delete(Employee entity)
    {
        return Delete(entity.Id);
    }

    public override IDictionary<int, bool> Delete(IEnumerable<Employee> entities)
    {
        return entities
               .ToDictionary(emp => emp.Id, Delete);
    }
}


What we can notice here:

  1. The EmployeeCommandRepository class is inheriting from CommandRepository<Employee>.
  2. We don’t have to implement the methods coming from the non-generic ICommandRepository interface as they have been implemented in the abstract base CommandRepository<TEntity> class.
  3. The methods are simply implemented to use the static employees list.

Photo by Bruno van der Kraan on Unsplash

Moment of Truth

Now, it is time to test our design. I created a Console Application for quick testing.


Testing would be divided into three parts:

  1. Demonstrating Basic Operations.
  2. Demonstrating Wrong Casting Checks.
  3. Demonstrating Dealing With Abstractions.


Therefore, we will end up with a Program class as follows:


using System;
using BetterRepository.Entities;
using BetterRepository.Models;
using BetterRepository.Repositories;

namespace BetterRepository
{
	internal class Program
	{
		private static readonly EmployeeQueryRepository m_EmployeeQueryRepository = new();
		private static readonly EmployeeCommandRepository m_EmployeeCommandRepository = new();

		static void Main(string[] args)
		{
			DemonstratingBasicOperation();
			DemonstratingWrongCastingChecks();
			DemonstratingDealingWithAbstractions();
			Console.ReadLine();
		}
	}

	public class Student : Entity
	{
	}
}


We also defined a Student class which inherits from Entity and we are going to use it for testing at some point.


Now, we move to the implementation of the DemonstratingBasicOperation()DemonstratingWrongCastingChecks(), and DemonstratingDealingWithAbstractions() one by one.


Demonstrating Basic Operations


public static void DemonstratingBasicOperation()
{
    Console.WriteLine("Started DemonstratingBasicOperation");

    var result = m_EmployeeQueryRepository.GetAll();
    Console.WriteLine(result);

    /*
    ActualPageZeroIndex: 0
    PagingDescriptor:
    ----------------
    ActualPageSize: 5
    NumberOfPages: 2
    PagesBoundries:
    ----------------
    FirstItemZeroIndex: 0, LastItemZeroIndex: 4
    FirstItemZeroIndex: 5, LastItemZeroIndex: 5
    Results:
    ----------------
    Id: 0, Name: Ahmed
    Id: 1, Name: Tarek
    Id: 2, Name: Patrick
    Id: 3, Name: Mohamed
    Id: 4, Name: Sara
    */


    result = m_EmployeeQueryRepository.Get(result.PagingDescriptor.ActualPageSize,
        result.ActualPageZeroIndex + 1);

    Console.WriteLine(result);

    /*
    ActualPageZeroIndex: 1
    PagingDescriptor:
    ----------------
    ActualPageSize: 5
    NumberOfPages: 2
    PagesBoundries:
    ----------------
    FirstItemZeroIndex: 0, LastItemZeroIndex: 4
    FirstItemZeroIndex: 5, LastItemZeroIndex: 5
    Results:
    ----------------
    Id: 5, Name: Ali
    */


    result = m_EmployeeQueryRepository.Get(6, 0);
    Console.WriteLine(result);

    /*
    ActualPageZeroIndex: 0
    PagingDescriptor:
    ----------------
    ActualPageSize: 5
    NumberOfPages: 2
    PagesBoundries:
    ----------------
    FirstItemZeroIndex: 0, LastItemZeroIndex: 4
    FirstItemZeroIndex: 5, LastItemZeroIndex: 5
    Results:
    ----------------
    Id: 0, Name: Ahmed
    Id: 1, Name: Tarek
    Id: 2, Name: Patrick
    Id: 3, Name: Mohamed
    Id: 4, Name: Sara
    */


    var tarek = m_EmployeeQueryRepository.Get(2);
    Console.WriteLine(tarek);

    /*
    Id: 1, Name: Tarek
    */


    result = m_EmployeeQueryRepository.GetByExpression(emp => emp.Name.ToLower().Contains("t"));
    Console.WriteLine(result);

    /*
    ActualPageZeroIndex: 0
    PagingDescriptor:
    ----------------
    ActualPageSize: 2
    NumberOfPages: 1
    PagesBoundries:
    ----------------
    FirstItemZeroIndex: 0, LastItemZeroIndex: 1
    Results:
    ----------------
    Id: 1, Name: Tarek
    Id: 2, Name: Patrick
    */


    var erikId = m_EmployeeCommandRepository.Add(new Employee(0, "Erik"));
    Console.WriteLine(erikId);

    /*
    6
    */


    var added = m_EmployeeCommandRepository.Add(new Employee[]
    {
        new Employee(0, "Hasan"),
        new Employee(0, "Mai"),
        new Employee(0, "John")
    });

    Console.WriteLine("");
    Console.WriteLine(String.Join("\r\n", added));

    /*
    7
    8
    9
    */


    m_EmployeeCommandRepository.Update(new Employee(1, "Tarek - Updated"));
    var tarekUpdated = m_EmployeeQueryRepository.Get(1);
    Console.WriteLine("");
    Console.WriteLine(tarekUpdated);

    /*
    Id: 1, Name: Tarek - Updated
    */


    m_EmployeeCommandRepository.AddOrUpdate(new Employee(1, "Tarek - Updated - Updated"));
    var tarekUpdatedUpdated = m_EmployeeQueryRepository.Get(1);
    Console.WriteLine("");
    Console.WriteLine(tarekUpdatedUpdated);

    /*
    Id: 1, Name: Tarek - Updated - Updated
    */


    var deletedTarek = m_EmployeeCommandRepository.Delete(1);
    Console.WriteLine("");
    Console.WriteLine(deletedTarek);

    /*
    True
    */


    var checkTarek = m_EmployeeQueryRepository.Get(1);
    Console.WriteLine("");
    Console.WriteLine(checkTarek != null);

    /*
    False
    */

    Console.WriteLine("Finished DemonstratingBasicOperation");
}


We are calling var result = m_EmployeeQueryRepository.GetAll(); followed by Console.WriteLine(result); and the result is:


ActualPageZeroIndex: 0

PagingDescriptor:
— — — — — — — —
ActualPageSize: 5
NumberOfPages: 2

PagesBoundries:
— — — — — — — —
FirstItemZeroIndex: 0, LastItemZeroIndex: 4
FirstItemZeroIndex: 5, LastItemZeroIndex: 5

Results:
— — — — — — — —
Id: 0, Name: Ahmed
Id: 1, Name: Tarek
Id: 2, Name: Patrick
Id: 3, Name: Mohamed
Id: 4, Name: Sara


So, the Get All is working fine.


Then we are calling result = m_EmployeeQueryRepository.Get(result.PagingDescriptor.ActualPageSize, result.ActualPageZeroIndex + 1); followed by Console.WriteLine(result); and the result is:


ActualPageZeroIndex: 1

PagingDescriptor:
— — — — — — — —
ActualPageSize: 5
NumberOfPages: 2

PagesBoundries:
— — — — — — — —
FirstItemZeroIndex: 0, LastItemZeroIndex: 4
FirstItemZeroIndex: 5, LastItemZeroIndex: 5

Results:
— — — — — — — —
Id: 5, Name: Ali


So, the Get With Paging is working fine.


Then we are calling result = m_EmployeeQueryRepository.Get(6, 0); followed by Console.WriteLine(result); and the result is:


ActualPageZeroIndex: 0

PagingDescriptor:
— — — — — — — —
ActualPageSize: 5
NumberOfPages: 2

PagesBoundries:
— — — — — — — —
FirstItemZeroIndex: 0, LastItemZeroIndex: 4
FirstItemZeroIndex: 5, LastItemZeroIndex: 5

Results:
— — — — — — — —
Id: 0, Name: Ahmed
Id: 1, Name: Tarek
Id: 2, Name: Patrick
Id: 3, Name: Mohamed
Id: 4, Name: Sara


Here we need to keep in mind the applied throttling restriction of 5 items. Analyzing this, we would get convinced that the Get With Paging is working fine.


Then we are calling var tarek = m_EmployeeQueryRepository.Get(2); followed by Console.WriteLine(tarek); and the result is:


Id: 1, Name: Tarek


So, the Get By Id is working fine.


Then we are calling result = m_EmployeeQueryRepository.Get(emp => emp.Name.ToLower().Contains(“t”)); followed by Console.WriteLine(result); and the result is:


ActualPageZeroIndex: 0

PagingDescriptor:
— — — — — — — —
ActualPageSize: 2
NumberOfPages: 1

PagesBoundries:
— — — — — — — —
FirstItemZeroIndex: 0, LastItemZeroIndex: 1

Results:
— — — — — — — —
Id: 1, Name: Tarek
Id: 2, Name: Patrick


So, the Get With Predicate Filter is working fine.


Then we are calling var erikId = m_EmployeeCommandRepository.Add(new Employee(0, “Erik”)); followed by Console.WriteLine(erikId); and the result is:


6


So, the Add is working fine.


Then we are calling


var added = m_EmployeeCommandRepository.Add(new []
{ new Employee(0, “Hasan”), new Employee(0, “Mai”), new Employee(0, “John”) });


followed by Console.WriteLine(String.Join(“\r\n”, added)); and the result is:


7
8
9


So, the Add Collection is working fine.


Then we are calling


m_EmployeeCommandRepository.Update(new Employee(1, “Tarek — Updated”));
var tarekUpdated = m_EmployeeQueryRepository.Get(1);


followed by Console.WriteLine(tarekUpdated); and the result is:


Id: 1, Name: Tarek — Updated


So, the Update is working fine.


Then we are calling


m_EmployeeCommandRepository.AddOrUpdate(new Employee(1, “Tarek — Updated — Updated”));
var tarekUpdatedUpdated = m_EmployeeQueryRepository.Get(1);


followed by Console.WriteLine(tarekUpdatedUpdated); and the result is:


Id: 1, Name: Tarek — Updated — Updated


So, the Add Or Update is working fine.


Then we are calling var deletedTarek = m_EmployeeCommandRepository.Delete(1); followed by Console.WriteLine(deletedTarek); and the result is:


True


And calling var checkTarek = m_EmployeeQueryRepository.Get(1); followed by Console.WriteLine(checkTarek != null); and the result is:


False


So, the Delete is working fine.


Demonstrating Wrong Casting Checks


public static void DemonstratingWrongCastingChecks()
{
    Console.WriteLine("");
    Console.WriteLine("");
    Console.WriteLine("Started DemonstratingWrongCastingChecks");

    var queryRepository = m_EmployeeQueryRepository as IQueryRepository;
    var commandRepository = m_EmployeeCommandRepository as ICommandRepository;

    try
    {
        commandRepository.Add(new Student());
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);

        /*
        System.ArgumentException: 'The type "BetterRepository.Student" does not match the type 
        "BetterRepository.Entities.Employee"'
        */
    }

    try
    {
        commandRepository.Add(new Student[]
        {
            new Student(),
            new Student()
        });
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);

        /*
        System.ArgumentException: The type "BetterRepository.Student[]" does not match the type 
        "System.Collections.Generic.IEnumerable`1[BetterRepository.Entities.Employee]"
        */
    }

    try
    {
        bool FilterStudents(object obj)
        {
            return true;
        }

        // Compiler would not allow it as Func<Entity, bool> predicate is contravariant
        // This means that for the predicate parameter, you can only provide Func<Entity, bool>
        // or Func<Parent of Entity, bool>. In our case, the only available parent of Entity is
        // the Object class.
        //queryRepository.Get(FilterStudents);
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }

    Console.WriteLine("Finished DemonstratingWrongCastingChecks");
}


We are defining the local variable queryRepository which is of type IQueryRepository by casting m_EmployeeQueryRepository.


var queryRepository = m_EmployeeQueryRepository as IQueryRepository;


And defining the local variable commandRepository which is of type ICommandRepository by casting m_EmployeeCommandRepository.


var commandRepository = m_EmployeeCommandRepository as ICommandRepository;


Then when trying to execute commandRepository.Add(new Student()); it fails with the exception System.ArgumentException: ‘The type “BetterRepository.Student” does not match the type “BetterRepository.Entities.Employee”’.


Then when trying to execute commandRepository.Add(new Student[] { new Student(), new Student() }); it fails with the exception System.ArgumentException: The type “BetterRepository.Student[]” does not match the type “System.Collections.Generic.IEnumerable`1[BetterRepository.Entities.Employee]”.


So, the Wrong Casting Check is working fine.


Demonstrating Dealing With Abstractions


public static void DemonstratingDealingWithAbstractions()
{
    Console.WriteLine("");
    Console.WriteLine("");
    Console.WriteLine("Started DemonstratingDealingWithAbstractions");

    // Resetting the employees collection
    EmployeePersistence.Reset();

    var queryRepository = m_EmployeeQueryRepository as IQueryRepository;
    var commandRepository = m_EmployeeCommandRepository as ICommandRepository;

    // Getting first two Employees when we actually don't know their type
    // and we don't care about their type
    var firstTwoItems = queryRepository.Get(2, 0);
    Console.WriteLine(firstTwoItems);

    /*
    Id: 0, Name: Ahmed
    Id: 1, Name: Tarek
    */

    // Now we are deleting the first two items blindly when we don't know their type
    // and we don't care about their type
    commandRepository.Delete(firstTwoItems.Results);

    // Now we get the first two Employees again to check if it worked
    firstTwoItems = queryRepository.Get(2, 0);
    Console.WriteLine(firstTwoItems);

    /*
    Id: 2, Name: Patrick
    Id: 3, Name: Mohamed
    */

    Console.WriteLine("Finished DemonstratingDealingWithAbstractions");
}


First, we are resetting the Employees list EmployeePersistence.Reset();.

Then, we are defining the local variable queryRepository which is of type IQueryRepository by casting m_EmployeeQueryRepository.


var queryRepository = m_EmployeeQueryRepository as IQueryRepository;


And defining the local variable commandRepository which is of type ICommandRepository by casting m_EmployeeCommandRepository.


var commandRepository = m_EmployeeCommandRepository as ICommandRepository;


Then we execute:


/ Getting first two Employees when we actually don’t know their type
// and we don’t care about their type
var firstTwoItems = queryRepository.Get(2, 0);
Console.WriteLine(firstTwoItems);


And the result is:


Id: 0, Name: Ahmed
Id: 1, Name: Tarek


And then we execute:


// Now we are deleting the first two items blindly when we don’t know their type
// and we don’t care about their type
commandRepository.Delete(firstTwoItems.Results);

// Now we get the first two Employees again to check if it worked
firstTwoItems = queryRepository.Get(2, 0);
Console.WriteLine(firstTwoItems);


And the result is:


Id: 2, Name: Patrick
Id: 3, Name: Mohamed


So, Dealing With Abstractions is working fine.


Photo by Arthur Chauvineau on Unsplash

Final Words

Wow, a long trip. Now, let’s take a break, relax, and take a step back to see the big picture.


What we implemented here is not rocket science. The concepts are not that hard, it is only a matter of having steady hands while implementing them.


That’s it, hope you found reading this article as interesting as I found writing it.


Also Published Here