Search code examples
c#asp.net-corescrutor

Implementing Scrutor but extend cache on only few methods


I am planning to implement Scrutor

public interface ICustomersRepository
{
   Task<CustomerDto> GetCustomerAsync(Guid customerId);
   Task<bool> SaveCustomer(CustomerDto customer);
}

public class CustomersRepository : ICustomersRepository
{
   private readonly List<CustomerDto> _customers = new List<CustomerDto>
   {
      new CustomerDto{Id = Guid.Parse("64fa643f-2d35-46e7-b3f8-31fa673d719b"), Name = "Nick Chapsas"},
      new CustomerDto{Id = Guid.Parse("fc7cdfc4-f407-4955-acbe-98c666ee51a2"), Name = "John Doe"},
      new CustomerDto{Id = Guid.Parse("a46ac8f4-2ecd-43bf-a9e6-e557b9af1d6e"), Name = "Sam McRandom"}
   };
        
   public Task<CustomerDto> GetCustomerAsync(Guid customerId)
   {
      return Task.FromResult(_customers.SingleOrDefault(x => x.Id == customerId));
   }

   public Task<bool> SaveCustomer(CustomerDto customer)
   {
       return "Save Customer here and return bool";
   }
}

Implementing cached repository:

public class CachedCustomersRepository : ICustomersRepository
    {
        private readonly ICustomersRepository _customersRepository; //CustomersRepository
        private readonly ConcurrentDictionary<Guid, CustomerDto> _cache = new ConcurrentDictionary<Guid, CustomerDto>();
        
        public CachedCustomersRepository(ICustomersRepository customersRepository)
        {
            _customersRepository = customersRepository;
        }
        
        public async Task<CustomerDto> GetCustomerAsync(Guid customerId)
        {
            if (_cache.ContainsKey(customerId))
            {
                return _cache[customerId];
            }

            var customer = await _customersRepository.GetCustomerAsync(customerId);
            _cache.TryAdd(customerId, customer);
            return customer;
        }
    }

And here is how i do DI:

public void ConfigureServices(IServiceCollection services)
{
   services.AddControllers();

   services.AddSingleton<ICustomersRepository, CustomersRepository>();
   services.Decorate<ICustomersRepository, CachedCustomersRepository>();
}

The issue i see is ICustomerRepository have both GetCustomerAsync & SaveCustomer methods, I could implement ICustomerRepository in CacheCustomerRepository and define GetCustomerAsync method but how could I ignore SaveCustomer as that cannot go as cache method in CacheCustomerRepository class. If I don't define that method in CacheCustomerRepository i will be getting errors as missing implementation for interface, how to implement scrutor in clean way and is there a way implement only few methods that are required to be cached and rest can be in the main repository class ?

I can think of only including skeleton in the CacheCustomersRepository for save method but is it a clean way to do it, and also if my CustomerRepository have 50 methods of which only 5 methods needs cached implementation then it's very redundant and not a good way to put all the skeleton methods in the cache repository.

How can we implement Scrutor in a clean way ? Any suggestions ?


Solution

  • To address your specific question, the method that does not need to deal with caching, just delegate it to the decorated object, i.e.

    public class CachedCustomersRepository : ICustomersRepository
    {
        private readonly ICustomersRepository _customersRepository; //CustomersRepository
        private readonly ConcurrentDictionary<Guid, CustomerDto> _cache = new ConcurrentDictionary<Guid, CustomerDto>();
            
        public CachedCustomersRepository(ICustomersRepository customersRepository)
        {
            _customersRepository = customersRepository;
        }
            
        public async Task<CustomerDto> GetCustomerAsync(Guid customerId)
        {
            if (_cache.ContainsKey(customerId))
            {
                return _cache[customerId];
            }
    
            var customer = await _customersRepository.GetCustomerAsync(customerId);
            _cache.TryAdd(customerId, customer);
            return customer;
        }
    
        // No need to cache anything, just delegate it, although you might need to
        // invalidate the cache if a cached customer is saved
        public Task<bool> SaveCustomer(CustomerDto customer) => 
            _customersRepository.SaveCustomer(customer);
    }
    

    As for the broader question on how to approach this problem if your repository has 50 methods, without knowing anything about the rest of your project, my first instinct is that there might be some room for improvement in the existing design. Likely this hypothetical situation would be a good chance to practice the interface seggregation principle.