8

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));

An Update Based on The Answer:

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));
    }
Community
  • 1
  • 1
Kit
  • 20,354
  • 4
  • 60
  • 103
  • Do you have an relation (foreign key relation) between X and Y in modeldesigner and/or database? – Marcus Jul 06 '11 at 18:51
  • Properties on entities need to be virtual when lazy loading is enabled, otherwise it doesnt matter. – Marcus Jul 06 '11 at 18:51
  • @Kit: The previous answer you link to was mine, the statement about eager loading was wrong and I removed it - at this point I am not aware of any mechanism to always force eager loading of a particular navigation property by default in Linq to Entities, so you will have to use `Include()` or `Load()` in some form. – BrokenGlass Jul 06 '11 at 19:19
  • @BrokenGlass - ok no problem. It would be a great way to do it if real! – Kit Jul 06 '11 at 20:34
  • @Marcus - there is an FK relationship in the database. FWIW, when I save the aggregate root, the related entity also saves as expected. – Kit Jul 06 '11 at 20:49
  • You should start with removing all the repository and especially specification stuff and make the bare query to work. – Ladislav Mrnka Jul 06 '11 at 21:44

1 Answers1

1

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.

Community
  • 1
  • 1
Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • I tried the expression form of DbSet.Include() and it had the same result. I'm somewhat surprised. – Kit Jul 06 '11 at 20:32
  • Take the whole linq query without FirstOrDefault and convert it to DbQuery. Check its ToString method, it should contain generated SQL. Execute the query manually and check results. – Ladislav Mrnka Jul 06 '11 at 21:40
  • I thought I had the bare queries working. You made me go recheck, and for that, thanks! What I was doing was applying .FirstOrDefault to the Set, not the Set.Include(). In other words, I did do the .Include(), but I basically threw it away (some temp variables and what not as I was playing with the code). – Kit Jul 06 '11 at 22:22