So, I'm refactoring my clusterfuck of an application. The company mandates having the business logic separated from the data logic, which I agree with (it would also be nice if they held to it themselves, but hey...). So my DAL lives in a MyApp.DAL
project, my BL lives in a MyApp.BL
project, my UI... you get the drill.
Now, I'm using Entity Framework for my data layer. And because the database schema is very much not constant, can change under me at a moment's notice, I have very little control over it and occasionally I need to call out to other databases which are totally off-limits, I'm using a database-first .edmx model which gets an Update From Database once in a while. (By the way, it's EF4.0 if anybody wonders - yep, welcome to 5 years ago...)
Until now, I've kept all the more or less complex queries in the data layer hidden behind repositories - in DAL, I'd have a BusinessThingiesDAORepository
with several GetInterestingThingies()
-like methods, returning lists of entities. Then, in business layer, I'd have BusinessThingiesRepository
with the same set of methods basically proxying the calls and transforming the entities into business objects I need further in the application.
Now that seemed a) a little redundant (for every logical object, I need to create a DA repository, a method in said repository doing the actual work, a BL repository, and a proxy method), and b) a little leaky, since I had to shove all my queries (which are pretty much business logic) into the data layer.
So I decided to change that - made a light generic repository with some helper methods in the DAL, with code similar to:
public class GenericRepositoryDAO : IDisposable
{
protected ObjectContext _ctx;
protected bool _ownsContext;
public GenericRepositoryDAO(GenericRepositoryDAO repository)
{
_ctx = repository._ctx;
_ownsContext = false;
}
public GenericRepositoryDAO(DatabaseKey key = /*default*/)
{
_ctx = DataManager.GetContext(key); // gets an ObjectContext for a specified database
_ctx.DefaultContainerName = key.GetDBValue();
_ownsContext = true;
}
public void Dispose()
{
if (_ownsContext) _ctx.Dispose();
}
}
public class GenericRepositoryDAO<T> : GenericRepositoryDAO where T : EntityObject, new()
{
public GenericRepositoryDAO(GenericRepositoryDAO repository) : base(repository) { }
public GenericRepositoryDAO(DatabaseKey key = DatabaseKey.BusinessBook) : base(key) { }
private ObjectSet<T> _objectSet = null;
public ObjectSet<T> Entities
{
get
{
if (_objectSet == null) _objectSet = _ctx.CreateObjectSet<T>(typeof(T).Name);
return _objectSet;
}
}
public List<TResult> Query<TQueryResult, TResult> (
Func<ObjectSet<T>, IQueryable<TQueryResult>> query,
Func<TQueryResult, TResult> transform,
bool transformNulls = false
)
{
return query(Entities).ToList().Select(x => !transformNulls && x == null ? default(TResult) : transform(x)).ToList();
}
public List<TResult> GetAll<TResult>(Func<T, TResult> transform)
{
return Entities.ToList().Select(x => transform(x)).ToList();
}
public List<TResult> GetBy<TResult>(
Expression<Func<T, bool>> condition,
Func<T, TResult> transform,
bool transformNulls = false
)
{
return Entities.Where(condition).ToList().Select(x => (!transformNulls && x == null) ? default(TResult) : transform(x)).ToList();
}
//and similar for adding, updating, etc, etc...
}
Now, my business logic layer, instead of just proxying the calls, looks like this:
public List<BusinessObjectThingy> GetInterestingBusinessObjects()
{
using ( var rep = new GenericRepositoryDAO<Business_object_entities>() )
{
return rep.GetBy(x => x.is_interesting, x => BusinessObjectThingy.CreateFromDAO(x));
}
}
It's as close to elegant as I can get - the entities themselves don't leave the data layer, the EF context are bound to business-layer units of work via the repository, and the logic of how the query should look like (which is a simple "filter-by-property" case here, but due to the lack of control over the DB can get way out of hand in other places - thing 15-line GroupJoin()
mess) lives in the business layer.
The problem is, that now it leaks the other way - business layer needs to know about the entities, and how to query for them. Is there any smart way to keep the entities isolated in the data layer, when you can't just go Generic Repository all the way, because a generic GetAll()
not returning a IQueryable
will murder your database server?
TL;DR: how do I do a generic repository while still being able to write complex LINQ queries on the Entity Framework. I guess.