配置 EF 在访问导航属性未急切加载(且延迟加载已禁用)时抛出异常

发布于 2025-01-03 16:54:54 字数 292 浏览 2 评论 0原文

我们有一些应用程序当前正在使用启用了延迟加载的 EF 模型。当我关闭延迟加载(以避免隐式加载和我们的大多数 N+1 选择)时,我宁愿访问应该已经热切加载(或在引用上手动 Load() )抛出异常而不是返回 null(因为特定的异常比 null 引用更好、更容易调试)。

我目前倾向于仅修改 t4 模板来执行此操作(因此,如果 reference.IsLoaded == false,则抛出),但想知道这是否已经是一个已解决的问题,无论是在框中还是通过另一个项目。

任何对可以进行源分析并检测此类问题的插件/扩展/等的引用的奖励积分。 :)

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

囍孤女 2025-01-10 16:54:54

出于几个与性能相关的原因,我想做同样的事情(引发延迟加载) - 我想避免同步查询,因为它们会阻塞线程,并且在某些地方我想避免加载完整的实体,而只是加载属性代码需要的。

仅禁用延迟加载还不够好,因为某些实体具有可以合法为 null 的属性,并且我不想将“null 因为它是 null”与“null 因为我们决定不加载它”混淆。

我还只想选择性地在某些特定的代码路径中启用延迟加载,我知道延迟加载是有问题的。

以下是我的解决方案。

在我的 DbContext 类中,添加此属性:

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

在代码启动的某个位置,运行以下命令:

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

ThrowOnSyncQueryInterceptor 的代码如下:

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.");
        }
    }
}

然后在使用 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 ...
}

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 ...
}
浸婚纱 2025-01-10 16:54:54

jamesmanning,该项目的创建者 https://github.com/jamesmanning/EntityFramework.LazyLoadLoggingInterceptor ,通过读取堆栈跟踪设法拦截延迟加载的调用。

因此,您可以创建执行以下操作的 DbCommandInterceptor:

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

我知道读取堆栈跟踪帧可能会很昂贵,尽管我猜测在发生数据访问的正常情况下,与数据访问本身相比,成本可以忽略不计。但是,您需要亲自评估此方法的性能。

(顺便说一句,您所追求的是 NHibernate 多年来拥有的众多出色功能之一)。

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

转身以后 2025-01-10 16:54:54

您不必修改 T4。根据提及“T4”,我猜测您正在使用 EDMX。容器的属性窗口具有lazyloadingenabled属性。当您创建新模型时,它被设置为 true。您可以将其更改为 false。 T4 模板将看到这一点并将代码添加到 ctor 中。

此外,如果您使用 Microsoft 的 POCO 模板,他们会将 virtual 关键字添加到您的导航属性中。 virtual+lazyloadingenabled是实现延迟加载的必要组合。如果删除 virtual 关键字,则即使启用了延迟加载,该属性也永远不会延迟加载。


朱莉

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文