2

I would like to wrap a repository inside another repository that will handle caching while internally using the passed in repository. This way my caching logic can be completely separate from the repository implementation. This pattern would also allow me to easily change from memory cache to distributed cache, that is I could have different caching repositories that use different cache types so I can plug them in depending on the environment. On Azure I could use distributed cache but on a single server I could use memory cache for example.

public sealed class CachingFooRepository : IFooRepository
{
    private IFooRepository repo;
    private IMemoryCache  cache;

    public CachingFooRepository(IFooRepository implementation, IMemoryCache  cache, IFooCachingRules)
    {
        if ((implementation == null)||(implementation is CachingFooRepository))
        {
            throw new ArgumentException("you must pass in an implementation of IFooRpository");
        }
        repo = implementation;
        this.cache = cache;
    }

    public async Task<bool> Save(IFooItem foo)
    {
        // TODO throw if foo is null
        bool result = await repo.Save(user);
        string key = "Key-" + foo.Id.ToString();
        cache.Remove(key);
        return result;
    }

    public async Task<IFooItem> Fetch(int fooId)
    {
        string key = "Key-" + fooId.ToString();
        object result = cache.Get(key);
        if(result != null) { return (foo)result; }
        foo = await repo.Fetch(fooId);
        if(foo != null)
        cache.Set(key, foo);

        return foo;
    }

}

Obviously while CachingFooRepository implements IFooRepository, I must make sure that a different implementation of IFooRepository is passed into the constructor for CachingFooRepository since it isn't a real implementation of IFooRepository but depends on a real implementation.

This example is simplified, pseudo-ish code, whether to cache and how long to cache can be passed in as IFooCachingRules or IOptions or something like that.

So my question is how can I reigster the services in such a way that things that depend on IFooRepository will get an instance of CachingFooRepository, but CachingFooRepository will get some other implementation of IFooRepository such as SqlFooRepository? I would like to keep other classes depending only on IFooRepository, I don't want to make anything depend specifically on CachingFooRepository.

Is this possible, feasible, a good idea, a bad idea?

Steven
  • 166,672
  • 24
  • 332
  • 435
Joe Audette
  • 35,330
  • 11
  • 106
  • 99

2 Answers2

4

I would like to wrap a repository inside another repository that will handle caching while internally using the passed in repository.

There's a name for the pattern you are using. It's called: The Decorator pattern.

a good idea, a bad idea?

Using the decorator pattern is an excellent idea, because it allows you to make add functionality, without having to make changes to any existing part of the system. In other words, you are able to adhere to the Open/closed principle.

Is this possible

No, there's no easy way do this with ASP.NET Core's built-in DI container. You should use one of the mature existing DI libraries for .NET to do this. The three libraries with the best support for applying the decorator pattern are Autofac, StructureMap and Simple Injector.

Steven
  • 166,672
  • 24
  • 332
  • 435
  • what if I just define public interface IFooRepositoryWrapper : IFooRepository, with no additional methods or properties vs IFooRepository. Then I make a DefaultFooRepositoryWrapper that takes IFooRepository and delegates all the methods to it. I can make most classes depend on IFooRepositoryWrapper instead of on IFooRepository, then I can implement CachingFooRepositoryWrapper. This I think should work with the default DI while nothing depends on a specific implementation. – Joe Audette Jul 18 '15 at 18:13
  • @JoeAudette: Sure that will work. Problem however is that you are now polluting your application with custom abstractions and you need to make sweeping changes throughout your application to get things working. Please don't do that. Please switch to a decent DI library; they're all free and way much better than the built-in library will ever be. – Steven Jul 18 '15 at 18:19
  • I'm building components that will become open source so I don't want to force any specific DI on the consumers of my components. I want people to be able to use the default DI or any of their choosing. My app is in early stages where changes are ok for me. Is it really that polluting to have 2 identical interfaces for nesting a wrapper? – Joe Audette Jul 18 '15 at 18:27
  • @JoeAudette: If you're building an open source tool, I would even take it a step further and prevent any dependency on any such thing as a DI container at all. So not even the built-in DI container from ASP.NET. And you should certainly not depend on the registration API of the built-in container, because this will sooner or later cause [hell to break loose](https://github.com/simpleinjector/SimpleInjector/issues/41) on you and your project. – Steven Jul 18 '15 at 18:36
  • my product will be in nugets people can consume and those will not depend on any DI. But it will also be packaged as a working app using the default DI in a startup integration file.Those who want to wire it up differently can write their own integration file using any DI or however they want but my working example will use the default DI – Joe Audette Jul 18 '15 at 18:42
  • @JoeAudette: nut do youself a favor. Never weaken your SOLID design because of your tools. – Steven Jul 18 '15 at 18:43
  • you convinced me. I will not introduce the IFooRepositoryWrapper concept and will switch to autofac for my example integration so I can wire up the decorator pattern correctly. Thanks! – Joe Audette Jul 18 '15 at 19:23
  • @JoeAudette: And if you need any help in creating integration examples for [Simple Injector](https://simpleinjector.org), just give me a call. – Steven Jul 19 '15 at 11:20
0

Contrary to popular belief, the decorator pattern is fairly easy to implement using the built-in container.

By using the extension methods in the linked answer, registering decorators becomes as simple as this:

public void ConfigureServices(IServiceCollection services)
{
    // First add the regular implementation
    services.AddSingleton<IDependency, OriginalImplementation>();

    // Wouldn't it be nice if we could do this...
    services.AddDecorator<IDependency>(
        (serviceProvider, decorated) => new DecoratorImplementation(decorated));
            
    // ...or even this?
    services.AddDecorator<IDependency, DecoratorImplementation>();
}
Timo
  • 7,992
  • 4
  • 49
  • 67