Problem
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?
Solution
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.
-
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.
-
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.