ASP.NET Core ActionFilter引發的一個EF異常

来源:https://www.cnblogs.com/kklldog/archive/2020/03/16/not-use-sync-in-actionfilter.html
-Advertisement-
Play Games

最近在使用ASP.NET Core的時候出現了一個奇怪的問題。在一個Controller上使用了一個ActionFilter之後經常出現EF報錯。 這個異常說Context在完成前一個操作的時候第二個操作依據開始。這個錯誤還不是每次都會出現,只有在併發強的時候出現,基本可以判斷跟多線程有關係。看一下 ...


最近在使用ASP.NET Core的時候出現了一個奇怪的問題。在一個Controller上使用了一個ActionFilter之後經常出現EF報錯。

InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.
Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()

這個異常說Context在完成前一個操作的時候第二個操作依據開始。這個錯誤還不是每次都會出現,只有在併發強的時候出現,基本可以判斷跟多線程有關係。看一下代碼:

   public static class ServiceCollectionExt
    {
        public static void AddAgileConfigDb(this IServiceCollection sc)
        {
            sc.AddScoped<ISqlContext, AgileConfigDbContext>();
        }
    }
  [TypeFilter(typeof(BasicAuthenticationAttribute))]
    [Route("api/[controller]")]
    public class ConfigController : Controller
    {
        private readonly IConfigService _configService;
        private readonly ILogger _logger;

        public ConfigController(IConfigService configService, ILoggerFactory loggerFactory)
        {
            _configService = configService;
            _logger = loggerFactory.CreateLogger<ConfigController>();
        }
        // GET: api/<controller>
        [HttpGet("app/{appId}")]
        public async Task<List<ConfigVM>> Get(string appId)
        {
            var configs = await _configService.GetByAppId(appId);

            var vms = configs.Select(c => {
                return new ConfigVM() {
                    Id = c.Id,
                    AppId = c.AppId,
                    Group = c.Group,
                    Key = c.Key,
                    Value = c.Value,
                    Status = c.Status
                };
            });

            _logger.LogTrace($"get app {appId} configs .");

            return vms.ToList();
        }
       
    }

代碼非常簡單,DbContext使用Scope生命周期;Controller里只有一個Action,裡面只有一個訪問資料庫的地方。怎麼會造成多線程訪問Context的錯誤的呢?於是把目光移到BasicAuthenticationAttribute這個Attribute。

 public class BasicAuthenticationAttribute : ActionFilterAttribute
    {
        private readonly IAppService _appService;
        public BasicAuthenticationAttribute(IAppService appService)
        {
            _appService = appService;
        }
        public async override void OnActionExecuting(ActionExecutingContext context)
        {
            if (!await Valid(context.HttpContext.Request))
            {
                context.HttpContext.Response.StatusCode = 403;
                context.Result = new ContentResult();
            }
        }

        public async Task<bool> Valid(HttpRequest httpRequest)
        {
            var appid = httpRequest.Headers["appid"];
            if (string.IsNullOrEmpty(appid))
            {
                return false;
            }
            var app = await _appService.GetAsync(appid);
            if (app == null)
            {
                return false;
            }

            if (string.IsNullOrEmpty(app.Secret))
            {
                //如果沒有設置secret則直接通過
                return true;
            }
            var authorization = httpRequest.Headers["Authorization"];
            if (string.IsNullOrEmpty(authorization))
            {
                return false;
            }

            if (!app.Enabled)
            {
                return false;
            }
            var sec = app.Secret;

            var txt = $"{appid}:{sec}";
            var data = Encoding.UTF8.GetBytes(txt);
            var auth = "Basic " + Convert.ToBase64String(data);

            return auth == authorization;
        }
    }

BasicAuthenticationAttribute的代碼也很簡單,Attribute註入了一個Service並且重寫了OnActionExecuting方法,在方法里對Http請求進行Basic認證。這裡也出現了一次數據查詢,但是已經都加上了await。咋一看好像沒什麼問題,一個Http請求進來的時候,首先會進入這個Filter對其進行Basic認證,如果失敗返回403碼,如果成功則進入真正的Action方法繼續執行。如果是這樣的邏輯,不可能出現兩次EF的操作同時執行。繼續查找問題,點開ActionFilterAttribute的元數據:

    public abstract class ActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata, IAsyncActionFilter, IAsyncResultFilter, IOrderedFilter, IResultFilter
    {
        protected ActionFilterAttribute();

        //
        public int Order { get; set; }

        //
        public virtual void OnActionExecuted(ActionExecutedContext context);
        //
        public virtual void OnActionExecuting(ActionExecutingContext context);
        //
        [DebuggerStepThrough]
        public virtual Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next);
        //
        public virtual void OnResultExecuted(ResultExecutedContext context);
        //
        public virtual void OnResultExecuting(ResultExecutingContext context);
        //
        [DebuggerStepThrough]
        public virtual Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next);
    }

這玩意這麼看著跟以前有點不一樣啊,除了原來的4個方法,多了2個Async結尾的方法。到了這裡其實心裡已經有數了。這裡應該重寫OnResultExecutionAsync,因為我們的Action方法是個非同步方法。改一下BasicAuthenticationAttribute,重寫OnResultExecutionAsync方法:

public class BasicAuthenticationAttribute : ActionFilterAttribute
    {
        private readonly IAppService _appService;
        public BasicAuthenticationAttribute(IAppService appService)
        {
            _appService = appService;
        }

        public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            if (!await Valid(context.HttpContext.Request))
            {
                context.HttpContext.Response.StatusCode = 403;
                context.Result = new ContentResult();
            }
            await base.OnActionExecutionAsync(context, next);
        }

        public async Task<bool> Valid(HttpRequest httpRequest)
        {
            var appid = httpRequest.Headers["appid"];
            if (string.IsNullOrEmpty(appid))
            {
                return false;
            }
            var app = await _appService.GetAsync(appid);
            if (app == null)
            {
                return false;
            }

            if (string.IsNullOrEmpty(app.Secret))
            {
                //如果沒有設置secret則直接通過
                return true;
            }
            var authorization = httpRequest.Headers["Authorization"];
            if (string.IsNullOrEmpty(authorization))
            {
                return false;
            }

            if (!app.Enabled)
            {
                return false;
            }
            var sec = app.Secret;

            var txt = $"{appid}:{sec}";
            var data = Encoding.UTF8.GetBytes(txt);
            var auth = "Basic " + Convert.ToBase64String(data);

            return auth == authorization;
        }
    }

修改完後經過併發測試,EF報錯的問題得到瞭解決。
再來解釋下這個問題是如何造成的:一開始BasicAuthenticationAttribute是framework版本的ASP.NET MVC遷移過來的,按照慣例重寫了OnActionExecuting。其中註入的service裡面的方法是非同步的,儘管標記了await,但是這並沒有什麼卵用,因為框架在調用OnActionExecuting的時候並不會在前面加上await來等待這個方法。於是一個重寫了OnActionExecuting的Filter配合一個非同步的Action執行的時候並不會如預設的一樣先等待OnActionExecuting執行完之後再執行action。如果OnActionExecuting里出現非同步方法,那這個非同步方法很可能跟Action里的非同步方法同時執行,這樣在高併發的時候就出現EF的Context被多線程操作的異常問題。這裡其實還是一個老生常談的問題,就是儘量不要在同步方法內調用非同步方法,這樣很容易出現多線程的問題,甚至出現死鎖。
ASP.NET Core已經全面擁抱非同步,與framework版本有了很大的差異還是需要多多註意。看來這個Core版本的ActionFilter還得仔細研究研究,於是上微軟官網查了查有這麼一段:

Implement either the synchronous or the async version of a filter interface, not both. The runtime checks first to see if the filter implements the async interface, and if so, it calls that. If not, it calls the synchronous interface's method(s). If both asynchronous and synchronous interfaces are implemented in one class, only the async method is called. When using abstract classes like ActionFilterAttribute, override only the synchronous methods or the asynchronous method for each filter type.

就是說對於filter interface要麼實現同步版本的方法,要麼實現非同步版本的方法,不要同時實現。運行時會首先看非同步版本的方法有沒有實現,如果實現則調用。如果沒有則調用同步版本。如果同步版本跟非同步版本的方法都同時實現了,則只會調用非同步版本的方法。當使用抽象類,比如ActionFilterAttribute,只需重寫同步方法或者非同步方法其中一個。

參考:filters in asp.net core


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 題目描述: 給定一個長度為 N 的數列,求它數值單調遞增的子序列長度最大為多少。即已知有數列 A , A=A1,A2....An ,求 A的任意子序列 B ( B=Ak1,Ak2....Akp ),使 B 滿足 k1<k2<....<kp且 Ak1<Ak2<....<Akp 。 現求 p 的最大值。 ...
  • 【Spring Data 系列學習】Spring Data JPA @Query 註解查詢 前面的章節講述了 Spring Data Jpa 通過聲明式對資料庫進行操作,上手速度快簡單易操作。但同時 JPA 還提供通過註解的方式實現,通過將 註解在繼承 repository 的介面類方法上 。 Qu ...
  • 一、分析網站內容 本次爬取網站為opgg,網址為:” http://www.op.gg/champion/statistics” ​ 由網站界面可以看出,右側有英雄的詳細信息,以Garen為例,勝率為53.84%,選取率為16.99%,常用位置為上單 現對網頁源代碼進行分析(右鍵滑鼠在菜單中即可找到 ...
  • 安裝:pip install BeautifulSoup4 下表列出了主要的解析器,以及它們的優缺點:看個人習慣選取自己喜歡的解析方式 1 # 獲取html代碼 2 import requests 3 r = requests.get('http://www.python123.io/ws/demo ...
  • 通過一個例子說明瞭C++中自定義結構體或類作為關聯容器的鍵時的問題:需要定義排序規則。 ...
  • 1、逢7跳過小游戲:從1-100之間,遇到帶7的數字或者7的倍數跳過。 1 for i in range(1,101): 2 if i == 7 or i % 10 == 7 or i // 10 == 7: 3 continue 4 else: 5 print(i,end = ",") 2、七段數 ...
  • In [1]: import os import matplotlib.image as mpimg from PIL import Image import matplotlib.pyplot as plt import numpy as np import matplotlib as mpl m ...
  • Redis 在當前的技術社區里是非常熱門的。從來自 Antirez 一個小小的個人項目到成為記憶體數據存儲行業的標準,Redis已經走過了很長的一段路。 隨之而來的一系列最佳實踐,使得大多數人可以正確地使用 Redis。 下麵我們將探索正確使用 Redis 的10個技巧。 1、停止使用 KEYS Ok ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...