最近在使用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[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异常相关推荐

  1. 升级到asp.net core 3.1遇到的json异常

    升级到asp.net core 3.1遇到的json异常 参考文章: (1)升级到asp.net core 3.1遇到的json异常 (2)https://www.cnblogs.com/Wadere ...

  2. ASP.NET Core 模型验证的一个小小坑

    今天在我们的一个项目中遇到一个 asp.net core 模型验证(model validation)的小问题.当模型属性的类型是 bool ,而提交上来的该属性值是 null ,asp.net co ...

  3. [Asp.net core 3.1] 通过一个小组件熟悉Blazor服务端组件开发

    通过一个小组件,熟悉 Blazor 服务端组件开发.github:https://github.com/git-net/NBlazors 一.环境搭建 vs2019 16.4, asp.net cor ...

  4. 解决 ASP.NET Core 自定义错误页面对 Middleware 异常无效的问题

    我们基于 Razor Class Library 实现了自定义错误页面的公用类库(详见之前的随笔),但是在实际使用时发现如果在 middleware 中发生了异常,则不能显示自定义错误页面,而是返回默 ...

  5. NET问答: 如何将 ASP.NET Core WebAPI 中抛出的异常封装成对象?

    咨询区 rianjs: 在 ASP.NET Core WebAPI 中,我的 Controller 代码如下: [Route("create-license/{licenseKey}&quo ...

  6. 利用asp.net core actionfilter实现简单的RBAC权限过滤

    参考这位大神的博客:https://www.cnblogs.com/fonour/p/5848933.html,实现了简单的RBAC权限管理系统,但文章没有提到对权限的过滤,直接输入url还是可以访问 ...

  7. ASP.NET Core - 在ActionFilter中使用依赖注入

    上次ActionFilter引发的一个EF异常,本质上是对Core版本的ActionFilter的知识掌握不够牢固造成的,所以花了点时间仔细阅读了微软的官方文档.发现除了IActionFilter.I ...

  8. ASP.NET Core MVC+EF Core从开发到部署

    笔记本电脑装了双系统(Windows 10和Ubuntu16.04)快半年了,平时有时间就喜欢切换到Ubuntu系统下耍耍Linux,熟悉熟悉Linux命令.Shell脚本以及Linux下的各种应用的 ...

  9. 一个迷你ASP.NET Core框架的实现(下)

    [框架内幕]| 作者 / Edison Zhou 这是恰童鞋骚年的第196篇原创文章 上一篇我们了解了AspNetCore.Mini这个项目的背景及项目结构和流程,这一篇我们继续解析几个核心对象.本文 ...

最新文章

  1. R语言使用skimr包的skim函数查看整个dataframe数据集的summary信息、统计汇总信息(Summarize a whole dataset)
  2. mingw msys 编译 libzip
  3. html js css倒计时,js+css3倒计时动画特效
  4. C++Miller Rabin算法的实现(附完整源码)
  5. 关于USB-AUDIO使用ALSA编程的一点问题
  6. Spark-shell 脚本批量执行命令,命令行批量执行命令
  7. netfilter que_QUE的完整形式是什么?
  8. dbms标识符无效_DBMS中的聚合运算符(分组依据和具有子句)
  9. application/x-www-form-urlencoded 的contentType,POST数据内容过大,导致tomcat的request取不到参数...
  10. 从远程服务器获取数据
  11. 禁用UpdateOrchestrator重新启动任务
  12. FileZilla软件下载使用简易教程
  13. Kafka 性能调优实战:同等资源配置性能提升 20 几倍的秘诀
  14. continue,return,break 在for循环中的作用
  15. 家居行业如何做好私域布局?
  16. Google / Baidu 黑客搜索引擎语法详细记录
  17. vue实现 可拖拽的div
  18. Iterator中的 FailFast FailSafe【学习笔记】
  19. sql分组排序语句顺序
  20. [渝粤教育] 广东-国家-开放大学 21秋期末考试土木工程施工10516k1

热门文章

  1. 201671030107胡文艳实验三作业互评与改进报告
  2. 洛谷——P1033 自由落体
  3. SQL JOIN连接分类[转]
  4. LeetCode - 3Sum Closest
  5. php 打印对象详细信息,php打印显示数组与对象的函数详解
  6. 后缀的形容词_构词法(18)构成形容词的常见后缀 3
  7. stop-hbase.sh一直处于等待状态
  8. PHP: 深入了解一致性哈希
  9. 【2】开发环境的搭建,Ubuntu14.04
  10. android键盘弹出,聊天背景不变形