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?
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) – Matias Cicero Dec 11 '15 at 12:00