Dapper:关闭 reader 时尝试调用 NextResult 无效

Dapper: Invalid attempt to call NextResult when reader is closed

我在尝试使用 .NET 5.0、Dapper 2.0.78 异步和 MSSQL 服务器执行多个语句时出现以下错误 - 有时 -

System.InvalidOperationException: Invalid attempt to call NextResult when reader is closed. at Microsoft.Data.SqlClient.SqlDataReader.TryNextResult(Boolean& more)
at Microsoft.Data.SqlClient.SqlDataReader.NextResult() at Dapper.SqlMapper.GridReader.NextResult() in //Dapper/SqlMapper.GridReader.cs:line 414 at Dapper.SqlMapper.GridReader.ReadDeferred[T](Int32 index, Func 2 deserializer, Type effectiveType)+System.IDisposable.Dispose() at Dapper.SqlMapper.GridReader.ReadDeferred[T](Int32 index, Func 2 deserializer, Type effectiveType)+MoveNext() in //Dapper/SqlMapper.GridReader.cs:line 384 at System.Collections.Generic.List 1..ctor(IEnumerable 1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)

它不会一直发生。我怀疑 SQL 连接被无意关闭。有什么问题吗?

为了调试,我测试了 SQL 语句并查看了 SSMS 中的执行计划 - 这里没有任何标记,即正确的索引、配置的主键,它在很短的时间内执行。 SQL 服务器有

这是我的代码,比较简单。

private async Task<RecipeListModel> GetRecipesByIngredientAsync(int id)
    {

        string sql = @"SELECT Id,Title FROM dbo.[Ingredients] WHERE Id = @id;
                       SELECT ID,Title FROM dbo.[Recipes] WHERE IngredientId = @Id;" // simplified for the example

        RecipeListModel model = new() { };

        using (SqlConnection conn = new("my connection here"))
        {
            var data = await conn.QueryMultipleAsync(sql, new { id }).ConfigureAwait(false);

            model = new RecipeListModel
            {
                Ingredient = data.ReadAsync<Ingredient>().Result.SingleOrDefault(),
                Recipes = data.ReadAsync<Recipe>().Result.ToList()
            };
        }
        return model; // exception happens here.
    }


public class Ingredient
{
    public int Id { get; set; }
    public string Title { get; set; }
    // ...
}

public class Recipe
{
    public int Id { get; set; }
    public string Title { get; set; }
    // ...
}

public class RecipeListModel
{
    public IEnumerable<Recipe> Recipes { get; set; }
    public Ingredient Ingredient { get; set; }
    // ...
}

根据评论更新:

    [Route("~/{lang}/ingredient/{title}-{id}", Name = "ingredient-view")]
    public async Task<IActionResult> Ingredient(string title, int id, string q, int page = 1)
    {

        if (ModelState.IsValid)
        {
            var model = await GetRecipesByIngredientAsync(page, id, title, q).ConfigureAwait(false);
            if (model.Ingredient == null || Common.ClearUrl(model.Ingredient.Title) != title) // for SEO purposes, make sure that we do not have a tampered URL
            {
                return NotFound();
            }
            if (model.Recipes.Any())
            {
                var total = model.Recipes.First().TotalRows;
                model.TotalRows = total;
                model.Pager = new Pager(total, page, 8);
                model.q = string.IsNullOrEmpty(q) ? "" : q.ToString();
                return View(model);
            }
            else
            {
                RecipeListModel empty = new()
                {
                    Recipes = new List<Recipe>() { new Recipe() { Title = "" } },
                    Pager = new Pager(0, 0, 1),
                    q = q
                };
                return View(empty);
            }
        }
        return NotFound();
    }

    private async Task<RecipeListModel> GetRecipesByIngredientAsync(int p, int id, string title, string q)
    {

        string searchSql = @" -- optional (search phrase can be empty)    
                                    INNER JOIN FREETEXTTABLE([dbo].[FT_Recipes_v], *, @SearchPhrase, LANGUAGE 'English') AS recipesFullSearch
                                    ON(r.Id = recipesFullSearch.[Key])";

        string sql = string.Format(@"SELECT Id,Title FROM dbo.[Ingredients] WHERE Id = @id;

                                ;WITH RowCounter AS (
                                SELECT COUNT(r.Id) as TotalRows
                                FROM
                                    [dbo].[Recipes] r
                                    INNER JOIN [dbo].[RecipeIngredients] RI ON RI.Recipe_Id = r.Id
                                    INNER JOIN [dbo].[Ingredients] I ON RI.Ingredient_Id = I.Id
                                            {0} -- inject search phrase here if not empty

                                WHERE 
                                    [Active] = 1 AND [Approved] = 1 
                                    AND I.Id = @Id
                                ),
                               
                                DataRows AS (
                                SELECT 
                                    r.Id
                                    ,STUFF( -- combine all tags into a 'csv list' like pepper[i:123]salt[i:124]...
                                            (
                                                SELECT TOP 3
                                                    ']' + Title + '[i:' + CAST(Ingredient_Id AS nvarchar(11)) [text()]
                                                FROM 
                                                    (
                                                        SELECT 
                                                            RI.Recipe_Id
                                                            ,RI.Ingredient_Id
                                                            ,I.Title
                                                        FROM dbo.RecipeIngredients AS RI
                                                        INNER JOIN dbo.Ingredients AS I 
                                                            ON RI.Ingredient_Id = I.Id
                                                        -- here's the relation to the main query
                                                        WHERE RI.Recipe_Id = r.Id
                                                    ) TempTable
                                                FOR XML PATH(''), TYPE
                                            ).value('.','nvarchar(max)'),1,1,''
                                        ) IngredientsCSV
                                ,r.Title
                                ,LEFT(r.Description,260) as Description
                                ,d.Title AS DishTypeTitle
                                ,I.Title AS IngredientTitle
                                ,RF.[file]
                            FROM
                                    [dbo].[Recipes] r
                                            INNER JOIN [dbo].[DishTypes] d ON r.DishType_Id = d.Id
                                            INNER JOIN [dbo].[RecipeIngredients] RI ON RI.Recipe_Id = r.Id
                                            INNER JOIN [dbo].[Ingredients] I ON RI.Ingredient_Id = I.Id
                                        {0} -- inject search phrase here if not empty
                            OUTER APPLY 
                            (SELECT TOP 1 recipe_id,[file] FROM dbo.RecipeFiles WHERE recipe_id = r.id ) RF
                            WHERE
                                    [Active] = 1 AND [Approved] = 1 AND I.[Id] = @Id
                            ORDER BY 
                                r.Id DESC
                            OFFSET (@PageNumber) ROWS 
                            FETCH FIRST (@RowsPerPage) ROWS ONLY
                        )
                            SELECT 
                                    dr.*,
                                    (select TotalRows from rowcounter) as TotalRows
                                FROM 
                                    DataRows dr;"
                            , !string.IsNullOrEmpty(q) ? searchSql : ""
                            );

        using (SqlConnection conn = new("data source=someip;initial catalog=mydb;persist security info=True;user id=u;password=p"))
        {
            using (var data = await conn.QueryMultipleAsync(sql, new
            {
                SearchPhrase = q,
                id,
                PageNumber = (p - 1) * 8,
                RowsPerPage = 8

            }).ConfigureAwait(false))
            {
                return new RecipeListModel
                {
                    Ingredient = await data.ReadSingleOrDefaultAsync<Ingredient>().ConfigureAwait(false),
                    Recipes = await data.ReadAsync<Recipe>().ConfigureAwait(false)
                };
            }          
        }
    }

这是完整的事件日志(注意 NextResultAsync 而不是之前的 NextResult

RequestPath: /ingredient/pepper-123

An unhandled exception has occurred while executing the request.

Exception: System.InvalidOperationException: Invalid attempt to call NextResultAsync when reader is closed. at Microsoft.Data.Common.ADP.ExceptionWithStackTrace(Exception e) --- End of stack trace from previous location --- at Dapper.SqlMapper.GridReader.NextResultAsync() in /_/Dapper/SqlMapper.GridReader.Async.cs:line 157 at Dapper.SqlMapper.GridReader.ReadBufferedAsync[T](Int32 index, Func2 deserializer) in /_/Dapper/SqlMapper.GridReader.Async.cs:line 241 at myproject.Controllers.HomeController.GetRecipesByIngredientAsync(Int32 p, Int32 id, String title, String q) in C:\Web\myproject\myproject-net\Controllers\HomeController.cs:line 502 at myproject.Controllers.HomeController.Ingredient(String title, Int32 id, String q, Int32 page) in C:\Web\myproject\myproject-net\Controllers\HomeController.cs:line 125 at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask1 actionResultValueTask) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Filters.MiddlewareFilterBuilder.<>c.<b__8_0>d.MoveNext() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at SixLabors.ImageSharp.Web.Middleware.ImageSharpMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context) at WebMarkupMin.AspNetCore2.WebMarkupMinMiddleware.ProcessAsync(HttpContext context, Boolean useMinification, Boolean useCompression) at WebMarkupMin.AspNetCore2.WebMarkupMinMiddlewareBase.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)

我不能肯定地说,但它似乎是 ReadSingleOrDefaultAsync 中的一个错误,导致它关闭连接。

我们的想法是在关闭连接之前完全读取所有结果集,因此您需要使用能够做到这一点的东西。这可能是 foreachToListToDictionarySingleOrDefault

所以改用 ReadAsync 并将其输入标准 SingleOrDefault

model = new RecipeListModel
{
    Ingredient = (await data.ReadAsync<Ingredient>().ConfigureAwait(false)).SingleOrDefault(),
    Recipes = (await data.ReadAsync<Recipe>().ConfigureAwait(false)).ToList(),
}

我希望有机会调试 Dapper 源代码,并更新此答案以准确说明问题所在。