DEV Community

Ben Witt
Ben Witt

Posted on

Implementing the Cached Repository Pattern in C#

This article will guide you through the detailed implementation of the Cached Repository Pattern in C#. The Cached Repository Pattern combines the proven principles of the Repository Pattern with the performance enhancements of the Cache Pattern. The goal is to efficiently organize data access and improve application performance by caching frequently retrieved data.

Introduction to the Cached Repository Pattern

The Cached Repository Pattern combines the Repository Pattern with the Cache Pattern to structure data access and enhance performance. The Repository Pattern centralizes data access logic in separate classes, while the Cache Pattern stores frequently accessed data in memory to reduce access time.

What is the Cached Repository Pattern?

The Cached Repository Pattern utilizes a repository for data access and integrates a cache to optimize performance. When retrieving data, the repository first checks the cache. If the data is found there, it is retrieved from the cache. Otherwise, access to the data source is made, and the retrieved data is cached.

Benefits of the Cached Repository Pattern

Improved performance by caching frequently used data in the cache.
Reduction of database queries, minimizing server load.
Decoupling data access logic from application logic, enhancing maintainability and testability.
Applications of the Cached Repository Pattern
The Cached Repository Pattern is primarily used in applications with frequent data access where performance is critical. Typical use cases include web applications, APIs, and systems with large datasets.

Implementing the Repository Pattern

Before diving into the implementation of the Cached Repository Pattern, it’s essential to implement the Repository Pattern itself. The Repository Pattern acts as an abstraction layer for data access, allowing a clear separation between application logic and data access logic.

In implementing the Repository Pattern, typically two main components are created: a repository interface and a concrete implementation of this interface.

The repository interface defines the methods needed for data access, which can include methods for retrieving, adding, updating, or deleting data. These methods provide a uniform interface for data access, regardless of the underlying data source.

The concrete implementation of the repository contains the actual logic for accessing the data source. This implementation might, for example, execute SQL queries to retrieve or update data from a database.

By using the Repository Pattern, we can isolate the application logic from the details of the data access implementation. This not only facilitates the maintenance and extension of the application but also enables improved testability by using mock implementations of the repository during testing.

Once the Repository Pattern has been successfully implemented, we can then integrate the cache into the data access logic to further enhance the application’s performance.

Creating the Repository Interface

  1. Open your development environment (e.g., Visual Studio) and create a new C# project.
  2. Create an interface for your repository. This interface should define methods to retrieve, update, insert, and delete data, as well as any other specific methods relevant to your application.

For example:

public interface IRepository<T>
{
  T GetById(int id);
  IEnumerable<T> GetAll();
  void Add(T entity);
  void Update(T entity);
  void Delete(int id);
  // Other specific methods as needed
}
Enter fullscreen mode Exit fullscreen mode

Implementing the Concrete Repository

  1. Now, create a class that implements this interface. This class contains the concrete implementation of the methods to access the data source.

For example:

public class MyEntityRepository : IRepository<MyEntity>
{
  private DbContext _dbContext; // Use your own DbContext class

  public MyEntityRepository(DbContext dbContext)
  {
    _dbContext = dbContext;
  }

  public MyEntity GetById(int id)
  {
    return _dbContext.MyEntities.Find(id);
  }

  public IEnumerable<MyEntity> GetAll()
  {
    return _dbContext.MyEntities.ToList();
  }

  public void Add(MyEntity entity)
  {
    _dbContext.MyEntities.Add(entity);
    _dbContext.SaveChanges();
  }

  public void Update(MyEntity entity)
  {
    _dbContext.Entry(entity).State = EntityState.Modified;
    _dbContext.SaveChanges();
  }

  public void Delete(int id)
  {
    var entity = _dbContext.MyEntities.Find(id);
    _dbContext.MyEntities.Remove(entity);
    _dbContext.SaveChanges();
  }
  // Other specific methods can be implemented here
}
Enter fullscreen mode Exit fullscreen mode

With the implementation of the Repository Pattern, we have an abstract interface (the interface) and a concrete implementation (the repository class) to handle data access. The interface defines the methods for accessing the data independently of the specific implementation, while the repository class takes care of the actual interaction with the data source. This separation facilitates the maintenance and extension of the application.

Implementing the Cache Pattern
In the next step of our article, we will implement caching functionality to cache frequently accessed data and thereby improve the performance of our application.

Using Caching Libraries in C#
In the world of C#, various libraries are available to implement caching. These include, for example, _MemoryCache_ from the _System.Runtime.Caching_ namespace or _Microsoft.Extensions.Caching.Memory_, which are part of the .NET Core / .NET 5+ libraries.

For our tutorial, we choose to use MemoryCache as this solution is already integrated into .NET Core and .NET 5+.

Configuring the Cache

  1. First, add the appropriate namespaces:

using System.Runtime.Caching;

  1. Create a static class to manage the cache:
public static class CacheManager
{
  private static MemoryCache _cache = MemoryCache.Default;
  private static readonly object _lockObject = new object();
  public static T GetOrSet<T>(string key, Func<T> getItemCallback, DateTimeOffset absoluteExpiration)
  {
    if (!_cache.Contains(key))
    {
      lock (_lockObject)
      {
        if (!_cache.Contains(key))
        {
          T item = getItemCallback();
          _cache.Set(key, item, absoluteExpiration);
        }
      }
    }
    return (T)_cache.Get(key);
  }
  public static void Remove(string key)
  {
    _cache.Remove(key);
  }
}
Enter fullscreen mode Exit fullscreen mode

The **CacheManager** class contains a method called GetOrSet, which retrieves a value from the cache if it exists, or stores it there if it does not. This method expects three parameters: a key, a callback function for retrieving the value, and an expiration time for the cache entry.

With this class, we can cache data and retrieve it as needed to improve performance.

Integration of Repository and Cache
In this step, we extend the repository to retrieve data from the cache or store it there before retrieving it from the data source. This allows for improved application performance by caching frequently accessed data.

Extending the Repository for Cache Integration

  1. Modify the repository interface to add methods that utilize the cache:
public interface IRepository<T>
{
  T GetById(int id);
  IEnumerable<T> GetAll();
  void Add(T entity);
  void Update(T entity);
  void Delete(int id);
  // New methods for cache integration
  T GetByIdFromCache(int id);
  IEnumerable<T> GetAllFromCache();
}
Enter fullscreen mode Exit fullscreen mode
  1. Implement these new methods in the concrete repository:
public class MyEntityRepository : IRepository<MyEntity>
{
  private DbContext _dbContext;

  public MyEntityRepository(DbContext dbContext)
  {
    _dbContext = dbContext;
  }

  // Existing methods…
  public MyEntity GetByIdFromCache(int id)
  {
    return CacheManager.GetOrSet($"MyEntity_{id}", () => GetById(id), DateTimeOffset.UtcNow.AddMinutes(10));
  }

  public IEnumerable<MyEntity> GetAllFromCache()
  {
    return CacheManager.GetOrSet("AllMyEntities", GetAll, DateTimeOffset.UtcNow.AddMinutes(10));
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. With these changes, the repository first retrieves data from the cache and only accesses the data source when necessary.

Implementing Data Retrieval and Storage Logic
The cache integration can be further customized based on application logic and specific requirements. For example, methods can be added to update the cache after data changes in the data source.

With this integration, we have successfully connected the repository with the cache.

Testing and Validation

In this step, we will write unit tests to ensure that the Cached Repository Pattern functions as expected.

Unit Tests for the Cached Repository Pattern

  1. Open your test project in your development environment.

  2. Write unit tests for the repository methods to ensure that they retrieve data from the cache when available and correctly access the data source.

A simple example of a unit test could look like this:

[TestClass]
public class MyEntityRepositoryTests
{
  [TestMethod]
  public void GetByIdFromCache_Returns_Entity_From_Cache_If_Available()
  {
    // Arrange
    var mockCache = new Mock<MemoryCache>();
    mockCache.Setup(c => c.Contains(It.IsAny<string>())).Returns(true);
    var repository = new MyEntityRepository(mockCache.Object);
    // Act
    var result = repository.GetByIdFromCache(1);
    // Assert
    Assert.IsNotNull(result);
    // Additional assertions as needed
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Write similar tests for other methods of the repository as well as for the cache management methods.

Verifying Cache Functionality

  1. Run the unit tests to ensure that all tests pass successfully.

  2. Verify the behavior of the cache by testing various scenarios such as adding, updating, or deleting data, and verifying that the cache is appropriately updated or cleared.

By testing and validating, you ensure that the Cached Repository Pattern functions as expected and provides the performance benefits you are aiming for.

Conclusion and Summary

In this article, we have implemented the Cached Repository Pattern in C# to organize data access and improve performance. Here is a summary of the key steps:

  1. Introduction to the Cached Repository Pattern: We explained the fundamentals of the pattern, discussed its benefits, and explored its application areas.
  2. Implementation of the Repository Pattern: We created a repository interface and developed a concrete implementation for data access.

  3. Implementation of the Cache Pattern: Caching functionality was integrated to cache frequently accessed data in memory, thus improving performance.

  4. Integration of Repository and Cache: The repository was extended to first retrieve data from the cache or store it there before retrieving it from the data source.

  5. Testing and Validation: By writing unit tests, we ensured the functionality of the pattern and verified the behavior of the cache.

Suggestions for Further Improvement and Customization

  • Optimizing Cache Configuration: Adjusting cache expiration times and memory sizes for optimal performance.
  • Implementing Cache Invalidation: Adding mechanisms for automatic cache updates or deletions upon data changes.
  • Extending the Pattern for Distributed Systems: Adapting to distributed caching systems to improve scalability. By adapting and optimizing the Cached Repository Pattern, application performance can be enhanced, and maintainability can be increased.

Top comments (3)

Collapse
 
magom001 profile image
Arkady Magomedov

This is NOT how you add a caching layer to a repository. By adding *From cache methods to the interface, you violate SOLID principals. Repository should know nothing about the cache. You can use a decorator method on top of the interface implementation. See the decorator pattern for adding cache to repositories.

Collapse
 
crazeejeeves profile image
crazeejeeves • Edited

Agree with @magom001. You are also introducing a hard dependency of ur concrete class with the static cache manager, which is a shortcut that will bite u later.

Collapse
 
abrahamn profile image
Abraham

And most importantly is how you invalidate your cache, that should be fundamental in any cache implementation