I have two simple POCO classes; I'm trying to get the MyY
property below hydrated with the an instance of Y
. I've tried a number of ways to do this, and think I might be missing something obvious or simple.
public class X
{
public int Id { get; set;}
public virtual Y MyY { get; set; }
}
public class Y
{
public int Id { get; set; }
// ...
}
I've turned lazy loading off via this call in my subclass of DbContext
's constructor:
Configuration.LazyLoadingEnabled = false;
When retrieving an X
I have tried
context.Set<X>.Include("MyY").FirstOrDefault(x => ....);
which did not work. I tried
var result = context.Set<X>.FirstOrDefault(x => ....);
context.Entry(result).Reference("MyY").Load();
which works, but requires two round-trips to the database. I tried
context.Set<X>.Select(x => new { X = x, Y = x.MyY }).FirstOrDefault(x => ...);
which also works, but "weakens" my model (ordinarily projecting to a new type is not so bad, but the "shape" of these EF POCOs works perfectly for the DTOs I'll be sending through WCF later).
I finally tried removing virtual
from the MyY
property as suggested in an answer to another question, but that had no effect at all.
Finally, I want to use the generic repository pattern. What I have ended up with is the following design, shown in part, which supports explicit-load (not preferred) and eager-load when modified to work properly. How do I modify it to get the single db round-trip eager-load?
public class EFRepository : IRepository
{
public T Get<T>(Specification<T> specification) where T : class, IEntity
{
var result = ApplyEagerLoading(context.Set<T>()).FirstOrDefault(specification.IsMatch);
ApplyPostQueryLoading(new List<T> { result });
return result;
}
// doesn't really seem to work yet...
private DbSet<T> ApplyEagerLoading<T>(DbSet<T> set) where T : class, IEntity
{
var ls = loadSpecs.GetOrAdd(typeof(T), () => new List<LoadSpec>());
foreach (var spec in ls.Where(s => !s.ExplicitLoad))
set.Include(spec.PropertyName);
return set;
}
// works, but wrong on so many levels...
private void ApplyPostQueryLoading<T>(IEnumerable<T> entities) where T : class, IEntity
{
var ls = loadSpecs.GetOrAdd(typeof(T), () => new List<LoadSpec>());
foreach (var e in entities)
foreach (var spec in ls.Where(s => s.ExplicitLoad))
if (spec.IsCollection)
context.Entry(e).Collection(spec.PropertyName).Load();
else
context.Entry(e).Reference(spec.PropertyName).Load();
}
private readonly IDictionary<Type, IList<LoadSpec>> loadSpecs = new Dictionary<Type, IList<LoadSpec>>();
private class LoadSpec
{
internal string PropertyName;
internal bool ExplicitLoad;
internal bool IsCollection;
}
}
Example uses:
// add a rule to load MyY explicitly
repository.AddLoadRule<X>(x => x.MyY, explicit:true, isCollection:false)
...
var x = repository.Get<X>(new Specification<X>(x => x.Id == 5));
// add a rule to load MyY with X
repository.AddLoadRule<X>(x => x.MyY, explicit:false)
...
// x.MyY will be null! Doesn't work!
var x = repository.Get<X>(new Specification<X>(x => x.Id == 5));
It turns out my temp code examples lied (those one-liners above). I had actually cached the result of .Include
in a local variable but applied the .FirstOrDefault
against the .Set<X>
not the result of .Include
. Here is the fix to ApplyEagerLoading
, which mirrors what others have suggested in related questions:
private IQueryable<T> ApplyEagerLoading<T>(IEnumerable<T> set) where T : class, IEntity
{
var ls = loadSpecs.GetOrAdd(typeof(T), () => new List<LoadSpec>());
var query = set.AsQueryable();
return ls.Where(s => !s.ExplicitLoad).Aggregate(query, (current, spec) => current.Include(spec.PropertyName));
}
This should work:
X entity = context.Set<X>().Include(x => x.MyY).FirstOrDefault();
IF it doesn't the problem must be elsewhere.
If you need some eager loading strategy check this answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With