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

3

6

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. :)

Tot answered 9/2, 2012 at 16:53 Comment(0)
F
3

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 ...
}
Fingerbreadth answered 28/4, 2016 at 0:25 Comment(3)
Very nice! Looks great!Tot
I don't understand how this helps with detecting lazy loading and throwing an exception. Isn't this just detecting sync queries?Servo
Lazy loading always happens sync, so if you always use async in your own queries, this method will find all lazy loading.Hearne
L
1

jamesmanning, the creator of the project https://github.com/jamesmanning/EntityFramework.LazyLoadLoggingInterceptor, managed to intercept lazy-loaded calls by reading the stack trace.

So in you could create DbCommandInterceptor that does something like:

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        // unfortunately not a better way to detect whether the load is lazy or explicit via interceptor
        var stackFrames = new StackTrace(true).GetFrames();
        var stackMethods = stackFrames?.Select(x => x.GetMethod()).ToList();

        var dynamicProxyPropertyGetterMethod = stackMethods?
            .FirstOrDefault(x =>
                x.DeclaringType?.FullName.StartsWith("System.Data.Entity.DynamicProxies") == true &&
                x.Name.StartsWith("get_"));

        if (dynamicProxyPropertyGetterMethod != null)
        {
              throw new LazyLoadingDisallowedException();
        }

I know that reading the stack trace frames can be expensive, although my guess would be that in normal circumstances where data access is occurring, the cost is negligible compared to the data access itself. However you will want to assess the performance of this method for yourself.

(As a side note, what you are after is one of the many nice features that NHibernate has had for many many years).

Litho answered 9/4, 2020 at 6:52 Comment(0)
A
0

You shouldn't have to modify the T4. Based on mention of "T4" I'm guessing you are using EDMX. The properties window of the container has the lazyloadingenabled property. It's set to true when you create a new model. You can change it to false. T4 template will see that and add the code into the ctor.

Also if you're using Microsoft's POCO templates, they'll add the virtual keyword to your nav properties. Virtual + lazyloadingenabled is the necessary combination to get lazy loading. If you remove the virtual keyword then the property will never be lazy loaded, eve if lazyloading is enabled.

hth julie

Anzac answered 27/2, 2012 at 16:32 Comment(2)
The problem is that if I disable lazy-loading, accessing the properties that weren't loaded (EntityCollection's, for instance) don't throw, they just return null/empty. That means various consuming code that assumes they were already loaded takes it as no child rows (for instance) don't exist. I'd much rather change the template (or however to get this to work) so that if accessing such collections or navigation properties throw when accessed if IsLoaded == false. THanks!Tot
I'll start by saying that probably modifying the T4 is probably the way to go. I would definitely rethink the goal of throwing when isloaded is false. There must be a friendlier way of checking that value and responding to the situation. :)Anzac

© 2022 - 2024 — McMap. All rights reserved.