ASP.NET Core ActionFilter引发的一个EF异常
最近在使用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還得仔細研究研究,于是上微軟官網查了查有這么一段:
就是說對于filter interface要么實現同步版本的方法,要么實現異步版本的方法,不要同時實現。運行時會首先看異步版本的方法有沒有實現,如果實現則調用。如果沒有則調用同步版本。如果同步版本跟異步版本的方法都同時實現了,則只會調用異步版本的方法。當使用抽象類,比如ActionFilterAttribute,只需重寫同步方法或者異步方法其中一個。
參考:filters in asp.net core[1]
關注我的公眾號一起玩轉技術
References
[1]?filters in asp.net core:?https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的ASP.NET Core ActionFilter引发的一个EF异常的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用C#+Selenium+ChromeD
- 下一篇: asp.net ajax控件工具集 Au