1

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 ?

VR1256
  • 1,266
  • 4
  • 28
  • 56

1 Answers1

1

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.

Ivan Vargas
  • 703
  • 3
  • 9
  • thank you, I am using CustomerRepository to explain the problem in general with scrutor implementation of optional defining of the methods from interface, I thought of making interface as abstract classes so that implementation of methods becomes optional, but I was thinking it is a good/clean way to do it ? even if I follow ISP there will be always few methods that don't really need cached implementation. what do you suggest after implementing ISP delegating to actual repo method from cache method is good or change the interface to abstract class is clean way to do this ? thanks for your help – VR1256 Aug 27 '20 at 15:30
  • 1
    Interfaces are unit test friendly, abstract classes, in general, are not. Regarding what approach to follow, I'd just say the typical answer, "it depends". I'd look into the design and determine how that repository is being used, it if it doing too much, I'd break it down into more concise classes. If it is just a handful of methods, I think I wouldn't bother unless it is causing bigger issues somewhere else. – Ivan Vargas Aug 28 '20 at 22:15