Generic Cache-Aware Data Adapter












1














I've written a generic data adapter that can make use of a cache, for use in a data access layer. It presents an interface allowing CRUD operations, as well as retrieving all objects of a certain type.



IEntity.cs:



using System;

namespace GenericDataAdapter
{
public interface IEntity
{
Guid Id { get; set; }
}
}


IDataAdapter.cs:



using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace GenericDataAdapter
{
public interface IDataAdapter<T> where T : IEntity
{
Task<IEnumerable<T>> ReadAllAsync();
Task<(bool, T)> ReadAsync(Guid id); // bool indicates whether entity was present in data source
Task SaveAsync(T newData); // update if present, create if not present
Task DeleteAsync(Guid id);
}
}


CacheDataAdapter.cs:



using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace GenericDataAdapter
{
public class CacheDataAdapter<T> : IDataAdapter<T> where T : IEntity
{
private IDataAdapter<T> PrimaryDataSource;
private IDataAdapter<T> Cache;

public CacheDataAdapter(IDataAdapter<T> primaryDataSource, IDataAdapter<T> cache)
{
PrimaryDataSource = primaryDataSource;
Cache = cache;
}

public Task<IEnumerable<T>> ReadAllAsync()
{
return PrimaryDataSource.ReadAllAsync();
}

// can potentially return stale data, due to SaveAsync()/DeleteAsync() not being atomic
public async Task<(bool, T)> ReadAsync(Guid id)
{
var (presentInCache, cacheData) = await Cache.ReadAsync(id);
if (presentInCache)
{
return (true, cacheData);
}

var (presentInPrimary, primaryData) = await PrimaryDataSource.ReadAsync(id);
if (presentInPrimary)
{
await Cache.SaveAsync(primaryData);
return (true, primaryData);
}
else
{
return (false, default(T));
}
}

public async Task SaveAsync(T newData)
{
await Cache.SaveAsync(newData);
await PrimaryDataSource.SaveAsync(newData);
}

public async Task DeleteAsync(Guid id)
{
await Cache.DeleteAsync(id);
await PrimaryDataSource.DeleteAsync(id);
}
}
}


There are two points I'd like feedback on, though all comments are welcome:




  • In CacheDataAdapter.ReadAllAsync(), should I use await? It doesn't seem like I should, since I'm just passing through the return value of PrimaryDataSource.ReadAllAsync(), but I'm not sure.

  • I'm not sure whether unifying the create and update operations into SaveAsync() is a good idea; it puts the responsibility for fully initializing objects on the application. That's why I'm using GUIDs as the identifier, so the application can generate unique IDs without having to consult the database first.










share|improve this question



























    1














    I've written a generic data adapter that can make use of a cache, for use in a data access layer. It presents an interface allowing CRUD operations, as well as retrieving all objects of a certain type.



    IEntity.cs:



    using System;

    namespace GenericDataAdapter
    {
    public interface IEntity
    {
    Guid Id { get; set; }
    }
    }


    IDataAdapter.cs:



    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;

    namespace GenericDataAdapter
    {
    public interface IDataAdapter<T> where T : IEntity
    {
    Task<IEnumerable<T>> ReadAllAsync();
    Task<(bool, T)> ReadAsync(Guid id); // bool indicates whether entity was present in data source
    Task SaveAsync(T newData); // update if present, create if not present
    Task DeleteAsync(Guid id);
    }
    }


    CacheDataAdapter.cs:



    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;

    namespace GenericDataAdapter
    {
    public class CacheDataAdapter<T> : IDataAdapter<T> where T : IEntity
    {
    private IDataAdapter<T> PrimaryDataSource;
    private IDataAdapter<T> Cache;

    public CacheDataAdapter(IDataAdapter<T> primaryDataSource, IDataAdapter<T> cache)
    {
    PrimaryDataSource = primaryDataSource;
    Cache = cache;
    }

    public Task<IEnumerable<T>> ReadAllAsync()
    {
    return PrimaryDataSource.ReadAllAsync();
    }

    // can potentially return stale data, due to SaveAsync()/DeleteAsync() not being atomic
    public async Task<(bool, T)> ReadAsync(Guid id)
    {
    var (presentInCache, cacheData) = await Cache.ReadAsync(id);
    if (presentInCache)
    {
    return (true, cacheData);
    }

    var (presentInPrimary, primaryData) = await PrimaryDataSource.ReadAsync(id);
    if (presentInPrimary)
    {
    await Cache.SaveAsync(primaryData);
    return (true, primaryData);
    }
    else
    {
    return (false, default(T));
    }
    }

    public async Task SaveAsync(T newData)
    {
    await Cache.SaveAsync(newData);
    await PrimaryDataSource.SaveAsync(newData);
    }

    public async Task DeleteAsync(Guid id)
    {
    await Cache.DeleteAsync(id);
    await PrimaryDataSource.DeleteAsync(id);
    }
    }
    }


    There are two points I'd like feedback on, though all comments are welcome:




    • In CacheDataAdapter.ReadAllAsync(), should I use await? It doesn't seem like I should, since I'm just passing through the return value of PrimaryDataSource.ReadAllAsync(), but I'm not sure.

    • I'm not sure whether unifying the create and update operations into SaveAsync() is a good idea; it puts the responsibility for fully initializing objects on the application. That's why I'm using GUIDs as the identifier, so the application can generate unique IDs without having to consult the database first.










    share|improve this question

























      1












      1








      1







      I've written a generic data adapter that can make use of a cache, for use in a data access layer. It presents an interface allowing CRUD operations, as well as retrieving all objects of a certain type.



      IEntity.cs:



      using System;

      namespace GenericDataAdapter
      {
      public interface IEntity
      {
      Guid Id { get; set; }
      }
      }


      IDataAdapter.cs:



      using System;
      using System.Collections.Generic;
      using System.Threading.Tasks;

      namespace GenericDataAdapter
      {
      public interface IDataAdapter<T> where T : IEntity
      {
      Task<IEnumerable<T>> ReadAllAsync();
      Task<(bool, T)> ReadAsync(Guid id); // bool indicates whether entity was present in data source
      Task SaveAsync(T newData); // update if present, create if not present
      Task DeleteAsync(Guid id);
      }
      }


      CacheDataAdapter.cs:



      using System;
      using System.Collections.Generic;
      using System.Threading.Tasks;

      namespace GenericDataAdapter
      {
      public class CacheDataAdapter<T> : IDataAdapter<T> where T : IEntity
      {
      private IDataAdapter<T> PrimaryDataSource;
      private IDataAdapter<T> Cache;

      public CacheDataAdapter(IDataAdapter<T> primaryDataSource, IDataAdapter<T> cache)
      {
      PrimaryDataSource = primaryDataSource;
      Cache = cache;
      }

      public Task<IEnumerable<T>> ReadAllAsync()
      {
      return PrimaryDataSource.ReadAllAsync();
      }

      // can potentially return stale data, due to SaveAsync()/DeleteAsync() not being atomic
      public async Task<(bool, T)> ReadAsync(Guid id)
      {
      var (presentInCache, cacheData) = await Cache.ReadAsync(id);
      if (presentInCache)
      {
      return (true, cacheData);
      }

      var (presentInPrimary, primaryData) = await PrimaryDataSource.ReadAsync(id);
      if (presentInPrimary)
      {
      await Cache.SaveAsync(primaryData);
      return (true, primaryData);
      }
      else
      {
      return (false, default(T));
      }
      }

      public async Task SaveAsync(T newData)
      {
      await Cache.SaveAsync(newData);
      await PrimaryDataSource.SaveAsync(newData);
      }

      public async Task DeleteAsync(Guid id)
      {
      await Cache.DeleteAsync(id);
      await PrimaryDataSource.DeleteAsync(id);
      }
      }
      }


      There are two points I'd like feedback on, though all comments are welcome:




      • In CacheDataAdapter.ReadAllAsync(), should I use await? It doesn't seem like I should, since I'm just passing through the return value of PrimaryDataSource.ReadAllAsync(), but I'm not sure.

      • I'm not sure whether unifying the create and update operations into SaveAsync() is a good idea; it puts the responsibility for fully initializing objects on the application. That's why I'm using GUIDs as the identifier, so the application can generate unique IDs without having to consult the database first.










      share|improve this question













      I've written a generic data adapter that can make use of a cache, for use in a data access layer. It presents an interface allowing CRUD operations, as well as retrieving all objects of a certain type.



      IEntity.cs:



      using System;

      namespace GenericDataAdapter
      {
      public interface IEntity
      {
      Guid Id { get; set; }
      }
      }


      IDataAdapter.cs:



      using System;
      using System.Collections.Generic;
      using System.Threading.Tasks;

      namespace GenericDataAdapter
      {
      public interface IDataAdapter<T> where T : IEntity
      {
      Task<IEnumerable<T>> ReadAllAsync();
      Task<(bool, T)> ReadAsync(Guid id); // bool indicates whether entity was present in data source
      Task SaveAsync(T newData); // update if present, create if not present
      Task DeleteAsync(Guid id);
      }
      }


      CacheDataAdapter.cs:



      using System;
      using System.Collections.Generic;
      using System.Threading.Tasks;

      namespace GenericDataAdapter
      {
      public class CacheDataAdapter<T> : IDataAdapter<T> where T : IEntity
      {
      private IDataAdapter<T> PrimaryDataSource;
      private IDataAdapter<T> Cache;

      public CacheDataAdapter(IDataAdapter<T> primaryDataSource, IDataAdapter<T> cache)
      {
      PrimaryDataSource = primaryDataSource;
      Cache = cache;
      }

      public Task<IEnumerable<T>> ReadAllAsync()
      {
      return PrimaryDataSource.ReadAllAsync();
      }

      // can potentially return stale data, due to SaveAsync()/DeleteAsync() not being atomic
      public async Task<(bool, T)> ReadAsync(Guid id)
      {
      var (presentInCache, cacheData) = await Cache.ReadAsync(id);
      if (presentInCache)
      {
      return (true, cacheData);
      }

      var (presentInPrimary, primaryData) = await PrimaryDataSource.ReadAsync(id);
      if (presentInPrimary)
      {
      await Cache.SaveAsync(primaryData);
      return (true, primaryData);
      }
      else
      {
      return (false, default(T));
      }
      }

      public async Task SaveAsync(T newData)
      {
      await Cache.SaveAsync(newData);
      await PrimaryDataSource.SaveAsync(newData);
      }

      public async Task DeleteAsync(Guid id)
      {
      await Cache.DeleteAsync(id);
      await PrimaryDataSource.DeleteAsync(id);
      }
      }
      }


      There are two points I'd like feedback on, though all comments are welcome:




      • In CacheDataAdapter.ReadAllAsync(), should I use await? It doesn't seem like I should, since I'm just passing through the return value of PrimaryDataSource.ReadAllAsync(), but I'm not sure.

      • I'm not sure whether unifying the create and update operations into SaveAsync() is a good idea; it puts the responsibility for fully initializing objects on the application. That's why I'm using GUIDs as the identifier, so the application can generate unique IDs without having to consult the database first.







      c# cache






      share|improve this question













      share|improve this question











      share|improve this question




      share|improve this question










      asked 2 days ago









      DylanSpDylanSp

      1087




      1087






















          1 Answer
          1






          active

          oldest

          votes


















          0














          Typically this scenario is done with a decorator pattern where you would wrap just the one and have the cache functionality in the one class. You would put the code in the CacheDataAdapter that I assume is currently in the code of the IDataAdapter cache. Most of the time this will simplify your code as you can call base method to get the values if not in the backing store for the cache.



          For example if you are using the ConcurrentDictionary or MemoryCache they will have methods to GetOrCreate or AddOrUpdate that will take a lamba expression that you can call into your base method to get the value if missing. Since you are doing async work while caching you should check out the AsyncLazy



          If you don't want to do the decorator pattern I would swap Save and Delete to do it in the primary store before removing them from the cache. You could have a situation where the save fails in the primary but your cache is updated to say it's good.



          As far if you should await the ReadAllAsync or not it doesn't matter a lot. I personally wouldn't await it but either way is fine. See answer on SO






          share|improve this answer





















          • I figured I'd separate the implementation of caching logic into CacheDataAdapter, while another implementation of IDataAdapter would handle the lower-level logic of connecting to a specific cache solution like Redis.
            – DylanSp
            yesterday











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f211120%2fgeneric-cache-aware-data-adapter%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          1 Answer
          1






          active

          oldest

          votes








          1 Answer
          1






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes









          0














          Typically this scenario is done with a decorator pattern where you would wrap just the one and have the cache functionality in the one class. You would put the code in the CacheDataAdapter that I assume is currently in the code of the IDataAdapter cache. Most of the time this will simplify your code as you can call base method to get the values if not in the backing store for the cache.



          For example if you are using the ConcurrentDictionary or MemoryCache they will have methods to GetOrCreate or AddOrUpdate that will take a lamba expression that you can call into your base method to get the value if missing. Since you are doing async work while caching you should check out the AsyncLazy



          If you don't want to do the decorator pattern I would swap Save and Delete to do it in the primary store before removing them from the cache. You could have a situation where the save fails in the primary but your cache is updated to say it's good.



          As far if you should await the ReadAllAsync or not it doesn't matter a lot. I personally wouldn't await it but either way is fine. See answer on SO






          share|improve this answer





















          • I figured I'd separate the implementation of caching logic into CacheDataAdapter, while another implementation of IDataAdapter would handle the lower-level logic of connecting to a specific cache solution like Redis.
            – DylanSp
            yesterday
















          0














          Typically this scenario is done with a decorator pattern where you would wrap just the one and have the cache functionality in the one class. You would put the code in the CacheDataAdapter that I assume is currently in the code of the IDataAdapter cache. Most of the time this will simplify your code as you can call base method to get the values if not in the backing store for the cache.



          For example if you are using the ConcurrentDictionary or MemoryCache they will have methods to GetOrCreate or AddOrUpdate that will take a lamba expression that you can call into your base method to get the value if missing. Since you are doing async work while caching you should check out the AsyncLazy



          If you don't want to do the decorator pattern I would swap Save and Delete to do it in the primary store before removing them from the cache. You could have a situation where the save fails in the primary but your cache is updated to say it's good.



          As far if you should await the ReadAllAsync or not it doesn't matter a lot. I personally wouldn't await it but either way is fine. See answer on SO






          share|improve this answer





















          • I figured I'd separate the implementation of caching logic into CacheDataAdapter, while another implementation of IDataAdapter would handle the lower-level logic of connecting to a specific cache solution like Redis.
            – DylanSp
            yesterday














          0












          0








          0






          Typically this scenario is done with a decorator pattern where you would wrap just the one and have the cache functionality in the one class. You would put the code in the CacheDataAdapter that I assume is currently in the code of the IDataAdapter cache. Most of the time this will simplify your code as you can call base method to get the values if not in the backing store for the cache.



          For example if you are using the ConcurrentDictionary or MemoryCache they will have methods to GetOrCreate or AddOrUpdate that will take a lamba expression that you can call into your base method to get the value if missing. Since you are doing async work while caching you should check out the AsyncLazy



          If you don't want to do the decorator pattern I would swap Save and Delete to do it in the primary store before removing them from the cache. You could have a situation where the save fails in the primary but your cache is updated to say it's good.



          As far if you should await the ReadAllAsync or not it doesn't matter a lot. I personally wouldn't await it but either way is fine. See answer on SO






          share|improve this answer












          Typically this scenario is done with a decorator pattern where you would wrap just the one and have the cache functionality in the one class. You would put the code in the CacheDataAdapter that I assume is currently in the code of the IDataAdapter cache. Most of the time this will simplify your code as you can call base method to get the values if not in the backing store for the cache.



          For example if you are using the ConcurrentDictionary or MemoryCache they will have methods to GetOrCreate or AddOrUpdate that will take a lamba expression that you can call into your base method to get the value if missing. Since you are doing async work while caching you should check out the AsyncLazy



          If you don't want to do the decorator pattern I would swap Save and Delete to do it in the primary store before removing them from the cache. You could have a situation where the save fails in the primary but your cache is updated to say it's good.



          As far if you should await the ReadAllAsync or not it doesn't matter a lot. I personally wouldn't await it but either way is fine. See answer on SO







          share|improve this answer












          share|improve this answer



          share|improve this answer










          answered 2 days ago









          CharlesNRiceCharlesNRice

          1,872512




          1,872512












          • I figured I'd separate the implementation of caching logic into CacheDataAdapter, while another implementation of IDataAdapter would handle the lower-level logic of connecting to a specific cache solution like Redis.
            – DylanSp
            yesterday


















          • I figured I'd separate the implementation of caching logic into CacheDataAdapter, while another implementation of IDataAdapter would handle the lower-level logic of connecting to a specific cache solution like Redis.
            – DylanSp
            yesterday
















          I figured I'd separate the implementation of caching logic into CacheDataAdapter, while another implementation of IDataAdapter would handle the lower-level logic of connecting to a specific cache solution like Redis.
          – DylanSp
          yesterday




          I figured I'd separate the implementation of caching logic into CacheDataAdapter, while another implementation of IDataAdapter would handle the lower-level logic of connecting to a specific cache solution like Redis.
          – DylanSp
          yesterday


















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f211120%2fgeneric-cache-aware-data-adapter%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          Terni

          A new problem with tex4ht and tikz

          Sun Ra