Search code examples
entity-frameworkentity-framework-4lazy-loadingeager-loading

Configuring EF to throw if accessing navigation property not eager loaded (and lazy-load is disabled)


We have a few apps that are currently using an EF model that has lazy-loading enabled. When I turn off the lazy-loading (to avoid implicit loads and most of our N+1 selects), I'd much rather have accessing a should-have-been-eager-loaded (or manually Load() on the reference) throw an exception instead of returning null (since a specific exception for this would be nicer and easier to debug than a null ref).

I'm currently leaning towards just modifying the t4 template to do so (so, if reference.IsLoaded == false, throw), but wondered if this was already a solved problem, either in the box or via another project.

Bonus points for any references to plugins/extensions/etc that can do source analysis and detect such problems. :)


Solution

  • I wanted to do the same thing (throw on lazy loading) for several performance-related reasons - I wanted to avoid sync queries because they block the thread, and in some places I want to avoid loading a full entity and instead just load the properties that the code needs.

    Just disabling lazy loading isn't good enough because some entities have properties that can legitimately be null, and I don't want to confuse "null because it's null" with "null because we decided not to load it".

    I also wanted to only optionally throw on lazy loading in some specific code paths where I know lazy loading is problematic.

    Below is my solution.

    In my DbContext class, add this property:

    class AnimalContext : DbContext
    {
       public bool ThrowOnSyncQuery { get; set; }
    }
    

    Somewhere in my code's startup, run this:

    // Optionally don't let EF execute sync queries
    DbInterception.Add(new ThrowOnSyncQueryInterceptor());
    

    The code for ThrowOnSyncQueryInterceptor is as follows:

    public class ThrowOnSyncQueryInterceptor : IDbCommandInterceptor
    {
        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            OptionallyThrowOnSyncQuery(interceptionContext);
        }
    
        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }
    
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            OptionallyThrowOnSyncQuery(interceptionContext);
        }
    
        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
        }
    
        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            OptionallyThrowOnSyncQuery(interceptionContext);
        }
    
        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }
    
        private void OptionallyThrowOnSyncQuery<T>(DbCommandInterceptionContext<T> interceptionContext)
        {
            // Short-cut return on async queries.
            if (interceptionContext.IsAsync)
            {
                return;
            }
    
            // Throw if ThrowOnSyncQuery is enabled
            AnimalContext context = interceptionContext.DbContexts.OfType<AnimalContext>().SingleOrDefault();
            if (context != null && context.ThrowOnSyncQuery)
            {
                throw new InvalidOperationException("Sync query is disallowed in this context.");
            }
        }
    }
    

    Then in the code that uses AnimalContext

    using (AnimalContext context = new AnimalContext(_connectionString))
    {
        // Disable lazy loading and sync queries in this code path
        context.ThrowOnSyncQuery = true;
    
        // Async queries still work fine
        var dogs = await context.Dogs.Where(d => d.Breed == "Corgi").ToListAsync();
    
        // ... blah blah business logic ...
    }