7
\$\begingroup\$

I was thinking of building a really flexible fluent API for my persistence layer. One of my goals is to achieve somehow the following result:

IEnumerable<User> users = _userRepo.Use(users => users.Create(new User("MattDamon"), 
                                                              new User("GeorgeClooney"), 
                                                              new User("BradPitt"))
                                                      .Where(u => u.Username.Length > 8)
                                                      .SortAscending(u => u.Username));

For this, I got a few ideas, the one I like the most is the use of Command objects to defer each action on the data source until a Resolve() method is called.

This my implementation of a command object:

 /**
  * Represents an action to be executed
 **/
 public interface ICommand<TResult>
 {
      // The action's result (so anyone can inspect the output without executing it again)
      TResult Result { get; }

      // Executes the action
      TResult Execute();
 }

 public class SimpleCommand<TResult> : ICommand<TResult>
 {
      // The action to execute
      private Func<TResult> _execution;

      private TResult _result;

      public TResult Result { get { return _result; } }

      // Creates a new command that will execute the specified function
      public SimpleCommand(Func<TResult> executeFunction)
      {
           _execution = executeFunction;
      }

      public TResult Execute()
      {
          return (_result = _execution());
      }
 }

I would then have the following base persistence strategy:

/**
 * Represents a persistence strategy that has deferred behaviour
**/
public abstract class BaseDeferredPersistenceStrategy<TSource> : IDeferredPersistenceStrategy<TSource>
   where TSource : IPersistable
{
     protected abstract ICommand<IEnumerable<TSource>> DeferredGetAll();

     // DeferredWhere and DeferredSort methods receive the previous command 
     // because the implementation may reuse it. 
     // (For example, a filter condition may be sent along the same request 
     // that gets all the entities, so this should reuse the previous 
     // command and not create a new one);
     protected abstract ICommand<IEnumerable<TSource>> DeferredWhere(ICommand<IEnumerable<TSource>> previous, Expression<Func<TSource, bool>> expression);
     protected abstract ICommand<IEnumerable<TSource>> DeferredSort(ICommand<IEnumerable<TSource>> previous, Expression<Func<TSource, object>> expression, bool ascending);

     protected abstract ICommand<IEnumerable<TSource>> DeferredGet(IEnumerable<object> keys);
     protected abstract ICommand<IEnumerable<TSource>> DeferredAdd(TSource persistable);
     protected abstract ICommand<IEnumerable<TSource>> DeferredUpdate(TSource persistable);
     protected abstract ICommand<IEnumerable<TSource>> DeferredDelete(IEnumerable<object> keys);

     private ICollection<ICommand<IEnumerable<TSource>>> _commands;

     public BaseDeferredPersistenceStrategy()
     {
          _commands = new HashSet<ICommand<IEnumerable<TSource>>>();
     }

     public BaseDeferredPersistenceStrategy<TSource> GetAll()
     {
          _commands.Add(DeferredGetAll());
          return this;
     }

     public BaseDeferredPersistenceStrategy<TSource> Where(Expression<Func<TSource, bool>> expression)
     {
          _commands.Add(DeferredWhere(_commands.LastOrDefault(), expression));
          return this;
     }

     public BaseDeferredPersistenceStrategy<TSource> Sort(Expression<Func<TSource, object>> expression, bool ascending = true)
     {
          _commands.Add(DeferredSort(_commands.LastOrDefault(), expression, ascending));
          return this;
     }

     public BaseDeferredPersistenceStrategy<TSource> Get(params object[] keys)
     {
          _commands.Add(DeferredGet(keys));
          return this;
     }

     public BaseDeferredPersistenceStrategy<TSource> Add(TSource persistable)
     {
          _commands.Add(DeferredAdd(persistable));
          return this;
     }

     public BaseDeferredPersistenceStrategy<TSource> Update(TSource persistable)
     {
          _commands.Add(DeferredUpdate(persistable));
          return this;
     }

     public BaseDeferredPersistenceStrategy<TSource> Delete(params object[] keys)
     {
          _commands.Add(DeferredDelete(keys));
          return this;
     }

     /**
      * Executes all the deferred commands in the order they were created
     **/
     public IEnumerable<TSource> Resolve()
    {
        IEnumerable<TSource> result = Enumerable.Empty<TSource>();

        // If the result of the command's execution is null, it means it was not a query, so leave the result as it is.
        foreach (var command in _commands) result = command.Execute() ?? result;

        return result;
    }
}

And this is one possible childs of the base class:

/**
 * Represents a Cache Persistence Strategy
**/
public class CachedDeferredPersistenceStrategy<TSource> : BaseDeferredPersistenceStrategy<TSource>
   where TSource : IPersistable
{
     private ICache<TSource> _cache;

     // ICache<TSource> is injected
     public CachedDeferredPersistenceStrategy(ICache<TSource> cache)
     {
         _cache = cache;
     }

     protected override ICommand<IEnumerable<TSource>> DeferredGetAll()
     {
         return new SimpleCommand<IEnumerable<TSource>>(() => _cache.FetchAll());
     }

     protected override ICommand<IEnumerable<TSource>> DeferredWhere(ICommand<IEnumerable<TSource>> previous, Expression<Func<TSource, bool>> expression)
     {
         Func<TSource, bool> compiledExpression = expression.Compile();
         return new SimpleCommand<IEnumerable<TSource>>(() =>
         {
            // If the previous command was a query, then use the previous command's result. If not, then operate on all stored entities
            IEnumerable<TSource> persistables = previous != null && previous.Result != null ? previous.Result : _cache.FetchAll();

            return persistables.Where(compiledExpression);
         });
     }

     protected override ICommand<IEnumerable<TSource>> DeferredSort(ICommand<IEnumerable<TSource>> previous, Expression<Func<TSource, object>> expression, bool ascending)
     {
         Func<TSource, bool> compiledExpression = expression.Compile();
         return new SimpleCommand<IEnumerable<TSource>>(() =>
         {
            // If the previous command was a query, then use the previous command's result. If not, then operate on all stored entities
            IEnumerable<TSource> persistables = previous != null && previous.Result != null ? previous.Result : _cache.FetchAll();

            return ascending ? persistables.OrderBy(compiledExpression) : persistables.OrderByDescending(compiledExpression);
         });
     }

     protected override ICommand<IEnumerable<TSource>> DeferredGet(IEnumerable<object> keys)
    {
        return new SimpleCommand<IEnumerable<TSource>>(() =>
        {
            string key = BuildCacheKey(keys);
            TSource value = _cache.Fetch(key);
            return value != null ? new [] { value } : Enumerable.Empty<TSource>();
        });
    }

    protected override ICommand<IEnumerable<TSource>> DeferredAdd(TSource persistable)
    {
        return new SimpleCommand<IEnumerable<TSource>>(() =>
        {
            string key = BuildCacheKey(persistable);
            if(!_cache.Store(key, persistable, TimeSpan.FromMinutes(10)))
               throw new ArgumentException("Entity is already persisted", "persistable");
            return null;
        });
    }

    // ... Remaining methods ...
}

This would allow me to have a persistence strategy that can be queried like this:

var result = _strategy.Where(u => u.IsValid).Sort(u => u.Id).Resolve();

Of course, having an even higher layer (a repository) with the following method:

// IDataAccessObject<TSource> is a collection of IDeferredPersistenceStrategy<TSource>.
// It delegates each action to each registered strategies.   
// For instance, _dao.Create(user) will execute _cacheStrategy.Add(user), _sqlStrategy.Add(user), etc...
public IEnumerable<TSource> Use(Func<IDataAccessObject<TSource>, IDataAccessObject<TSource>> operation)
{
    return operation(_dao).Resolve();
}

would allow me to:

var result = _userRepo.Use(users => users.Where(u => u.Username.Length > 8)
                                         .SortAscending(u => u.Username));

What are the advantages and disadvantages of my approach? More importantly, is it scalable? Would it take too much effort to add a new persistence strategy? Is it too generic? Is it not generic enough?

\$\endgroup\$
  • 1
    \$\begingroup\$ I find the code above complex and confusing. But as I read it (3 times), I kept thinking about 2 things 1) IQueryable things are deferred by default and 2) A messaging system would most likely be much easier. When we look at what a messaging protocol (such as any protocol that runs on TCP) we know that very difficult issues are solved in a very easy work-flow type manner. \$\endgroup\$ – John Peters Dec 11 '15 at 4:19
  • 2
    \$\begingroup\$ It is not clear, why is your origanl code, which uses LINQ query is not good enough. LINQ already uses deferred execution, so whats the point in adding an extra level of abstraction? \$\endgroup\$ – Nikita B Dec 11 '15 at 11:43
  • \$\begingroup\$ @NikitaBrizhak I'd love if it was that easy. There are 3 things troubling me though: 1. If I use plain LINQ, then I would be working and filtering in memory, which I don't want. 2. Let's say I use IQueryable as @JohnPeters recommend, how can I make it work exactly as I want with databases that doesn't have a "IQueryable API? Also, I would want users.GetAll().Where(u => u.Age == 21).OrderBy(u => u.Username) to be executed in one single query request when possible. Some databases can receive multiple filtering parameters in the same request. (Continues) \$\endgroup\$ – Matias Cicero Dec 11 '15 at 12:00
  • 3
    \$\begingroup\$ @MatiasCicero Is it possible that what you want is Entity Framework?? \$\endgroup\$ – IEatBagels Dec 11 '15 at 15:41
  • 2
    \$\begingroup\$ As fun as it can be to implement as persistence layer like this, I would recommend it as an exercise only. I have tried to do something similar in the past, and it gets tricky pretty fast. If I where to take over your code, I would prefer to see very specific abstractions which tells me what I can do, but not how. Then I would like to see a very specific implementation for your database. This way, the abstractions will guide me to where I need to change. The specific implementation will then be easier to understand and debug. What I don't want, is to debug a generic persistence layer. \$\endgroup\$ – Thomas Eyde Dec 24 '15 at 11:07
2
\$\begingroup\$

What are your functional and non functional requirements here?

It is really uncommon to do not know your storage in advance for sure. Developing new universal data access technology as a part of something else is a way to big troubles.

Generally speaking, it is common to use some data access technology directly for simple CRUD tasks like reporting, or isolate data access technology by repositories.

  • CRUD with direct access - it is flexible and cheap, but we have dependency on data access technology.

  • Repositories - there are two kind of them: generic and custom.

    1. Generic is basically an exposure of IQueryable or your own query language. It almost always a very bad thing to have. You can not really isolate yourself from data technology specifics, it is a constant obstacle on using advanced features of data access technology and it is a really, really big leak of abstraction. It is almost useless to say the least in many situations.

    2. Custom repositories look good even for some CRUD, you definitely need them for DDD scenarios. It is the only way to isolate dependencies on concrete data access technology. They look like a concrete and simple contract for data access layer with potentially replaceable implementation.

Custom repository might be something like this:

interface IUserReader
{
   User Read(int id);
   IPage<User> Read(UserQuery query);
}   

class UserQuery : PagingQuery
{
   public int? CompanyId { get; set; }
}  

class PagingQuery
{
   public int Page { get; set; } = 1;
   public int PageSize { get; set; } = 20;
} 

interface IPage<T> : IEnumerable<T>
{
   int Page { get; }
   int PageCount { get; } 
}

You have some flexibility with these Query objects, but be aware of over-complicating them as it makes testing and implementation much more complex. It is better to define more queries if necessary to be absolutely explicit about their contract. It usually gives a lot if you operate in terms of Models or DTO instead of data relational mapper objects, especially for Entity Framework.

You might find it attractive to have some asymmetry - use reader/writers to change db state and direct access to ORM for UI data fetching/reporting where flexibility is welcomed.

Hope this is relevant somehow. BTW, there are a lot of good books on this.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.