对 ITVF 的引用会引发“在前一个操作完成之前在此上下文上启动的第二个操作”异常

Posted

技术标签:

【中文标题】对 ITVF 的引用会引发“在前一个操作完成之前在此上下文上启动的第二个操作”异常【英文标题】:Reference to an ITVF raises a "second operation started on this context before a previous operation completed" exception 【发布时间】:2018-10-25 16:58:54 【问题描述】:

我正在尝试在 Linq 查询中引用内联表值函数 (ITVF):

var results = await (
    from v in _context.Vehicles
    from r in _context.UnitRepairStatus(v.VehicleNumber) <-- ITVF reference
    orderby v.VehicleNumber
    select new FooViewModel  
            ID = v.ID, 
            VehicleNumber = v.VehicleNumber,
            InRepair = Convert.ToBoolean(r.InRepair) <-- ITFV field
        
    ).ToListAsync();

查询运行时会产生错误:

InvalidOperationException:在此上下文中启动了第二个操作 在之前的操作完成之前。任何实例成员都不是 保证是线程安全的。

提及代码:

System.Linq.AsyncEnumerable.Aggregate_(IAsyncEnumerable 源,TAccumulate 种子, Func 累加器、Func resultSelector、CancellationToken cancelToken) MyApplication.Controllers.VehiclesController.Foo() 在 VehiclesController.cs

                var results = await (

如果我删除 ITFV 引用,查询将按预期工作

var results = await (
    from v in _context.Vehicles
    orderby v.VehicleNumber
    select new FooViewModel  
            ID = v.ID, 
            VehicleNumber = v.VehicleNumber,
            InRepair = False <-- dummy value
        
    ).ToListAsync();

为什么在我添加 ITVF 参考时会发生这种情况?我该如何解决这个问题?

代码

UnitRepairStatusITVF:

CREATE FUNCTION dbo.UnitRepairStatus(
  @unit_number varchar(18)
)
RETURNS TABLE
AS

RETURN

  SELECT  h.InRepair
  -- connects to a second database on same server
  --  shouldn't be an issue, but mentioning it in case it might be
  FROM    Schema2..Unit u
  INNER JOIN Schema2..History h on u.ID = h.UnitID
  WHERE   u.UnitNumber = @unit_number

UnitRepairStatus模特:

public class UnitRepairStatus

    public string UnitNumber  get; set; 
    public int? InRepair  get; set; 

MyDatabaseDbContextDbContext:

public class MyDatabaseDbContext : DbContext


    public MyDatabaseDbContext(DbContextOptions<MyDatabaseDbContext> options) : base(options) 
...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    
        ...
        modelBuilder.Query<UnitRepairStatus>();
    

    public IQueryable<UnitRepairStatus> UnitRepairStatus(string unitNumber) =>
        Query<UnitRepairStatus>().FromSql($"SELECT * FROM UnitRepairStatus(unitNumber)");


FooViewModelViewModel:

public class FooViewModel

    public int ID  get; set; 
    public string VehicleNumber  get; set; 
    public bool InRepair  get; set; 

VehiclesController构造函数:

public VehiclesController(
    ILogger<VehiclesController> logger, 
    MyDatabaseDbContext context
)

    _logger = logger;
    _context = context;

VehiclesControllerFoo方法:

public async Task<IActionResult> Foo()


    List<FooViewModel> model = null;

    try
    
        var results = await (  <-- line referenced in error message
            from v in _context.Vehicles
            from r in _context.UnitRepairStatus(v.VehicleNumber)
            orderby v.VehicleNumber
            select new FooViewModel  
                    ID = v.ID, 
                    VehicleNumber = v.VehicleNumber,
                    InRepair = Convert.ToBoolean(r.InRepair)
                
            ).ToListAsync();

    
    catch (Exception e)
    
        _logger.LogError(e.Message);
        throw;
    

    return View(model);


参考:

Use a Inline Table-Valued Functions with Linq and Entity Framework

【问题讨论】:

【参考方案1】:

对不起,我的错。您之前问题的答案中的技术适用于使用常量/变量参数调用 ITVF,但不适用于您的情况(以及我的错误示​​例)中的相关子查询。

解决方案是删除 ITVF 参数并扩展结果以包括该列(有效地将其转换为无参数视图):

CREATE FUNCTION dbo.UnitRepairStatus()
RETURNS TABLE
AS
RETURN
  SELECT u.UnitNumber, h.InRepair
  FROM    Schema2.Unit u
  INNER JOIN Schema2.History h on u.ID = h.UnitID

同时从上下文方法中移除参数

EF Core 2.x:

public IQueryable<UnitRepairStatus> UnitRepairStatus() =>
    Query<UnitRepairStatus>().FromSql("SELECT * FROM UnitRepairStatus()");

EF Core 3.x:

public IQueryable<UnitRepairStatus> UnitRepairStatus() =>
    Set<UnitRepairStatus>().FromSqlRaw("SELECT * FROM UnitRepairStatus()");

并更改 LINQ 查询以使用连接:

var results = await (
    from v in _context.Vehicles
    join r in _context.UnitRepairStatus() on v.VehicleNumber equals r.UnitNumber // <---
    orderby v.VehicleNumber
    select new FooViewModel  
        ID = v.ID, 
        VehicleNumber = v.VehicleNumber,
        InRepair = Convert.ToBoolean(r.InRepair)
    
).ToListAsync();

现在它应该在服务器端翻译和执行,并在客户端成功实现。

原始方法的问题在于 EF Core 默默地将查询执行切换到客户端评估(讨厌这样),然后在同一个上下文中执行多个异步操作时受到保护。

【讨论】:

使用没有参数的 TVF 有什么价值,而不仅仅是一个视图?如果没有,我可以消除IQueryable&lt;&gt; 方法,并将其定义为modelBuilder.Query&lt;UnitRepairStatus&gt;("vUnitRepairStatus")。似乎您的代码的其余部分将按原样工作。 “原始方法的问题在于 EF Core 默默地将查询执行切换到客户端评估(讨厌那个),然后在同一个上下文中执行多个异步操作时受到保护。”当我使用第二个上下文(与另一个数据库的连接)时,我没有收到错误——可能是因为它没有达到操作限制。这也解释了为什么输出显示重复调用该函数的数据库。 查询已按预期翻译:SELECT [v].[ID], [v].[VehicleNumber], [r].[InRepair] FROM [Vehicles] AS [v] INNER JOIN ( SELECT * FROM UnitRepairStatus() ) AS [r] ON [v].[VehicleNumber] = [r].[UnitNumber] ORDER BY [VehicleNumber] @craig 确实,没有参数它可以映射到视图。仍然使用函数更灵活 - 它允许您定义和传递一些其他(非相关)参数,并且在将来的某个时候,我希望将添加对 TVF(设置)返回函数的支持(类似于对 [数据库标量函数映射](docs.microsoft.com/en-us/ef/core/what-is-new/… 已在 v2.0 中添加)。上述技术只是当前 EF Core 限制的解决方法。

以上是关于对 ITVF 的引用会引发“在前一个操作完成之前在此上下文上启动的第二个操作”异常的主要内容,如果未能解决你的问题,请参考以下文章

Blazor:在前一个操作完成之前在此上下文上启动了第二个操作

UserManager 错误“在前一个操作完成之前在此上下文上启动了第二个操作”

Entity Framework Core:在前一个操作完成之前在此上下文上启动了第二个操作

asp.net core 在前一个操作完成之前在此上下文上启动了第二个操作

JPA 级联持续存在并且对分离实体的引用会引发 PersistentObjectException。为啥?

SQL 中内联表值函数的性能影响