最近在使用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

ASP.NET Core ActionFilter引发的一个EF异常的更多相关文章

  1. 008.Adding a model to an ASP.NET Core MVC app --【在 asp.net core mvc 中添加一个model (模型)】

    Adding a model to an ASP.NET Core MVC app在 asp.net core mvc 中添加一个model (模型)2017-3-30 8 分钟阅读时长 本文内容1. ...

  2. 006.Adding a controller to a ASP.NET Core MVC app with Visual Studio -- 【在asp.net core mvc 中添加一个控制器】

    Adding a controller to a ASP.NET Core MVC app with Visual Studio 在asp.net core mvc 中添加一个控制器 2017-2-2 ...

  3. ASP.NET Core错误处理中间件[2]: 开发者异常页面

    <呈现错误信息>通过几个简单的实例演示了如何呈现一个错误页面,该过程由3个对应的中间件来完成.下面先介绍用来呈现开发者异常页面的DeveloperExceptionPageMiddlewa ...

  4. ASP.NET Core 使用 SQLite 教程,EF SQLite教程,修改模型更新数据库,适合初学者看懂详细、简单教程

    SQLIte 操作方便,简单小巧,这里笔者就不再过多介绍,感兴趣可以到以下博文 https://blog.csdn.net/qq_31930499/article/details/80420246 文 ...

  5. 项目开发中的一些注意事项以及技巧总结 基于Repository模式设计项目架构—你可以参考的项目架构设计 Asp.Net Core中使用RSA加密 EF Core中的多对多映射如何实现? asp.net core下的如何给网站做安全设置 获取服务端https证书 Js异常捕获

    项目开发中的一些注意事项以及技巧总结   1.jquery采用ajax向后端请求时,MVC框架并不能返回View的数据,也就是一般我们使用View().PartialView()等,只能返回json以 ...

  6. (18)ASP.NET Core 基于现有数据库创建EF模型(反向工程)

    1.简介 Entity Framework Core可通过数据库提供给应用程序的插件访问许多不同的数据库.我们可以通过使用Entity Framework Core构建执行基本数据访问的ASP.NET ...

  7. ASP.NET Core中如何针对一个使用HttpClient对象的类编写单元测试

    原文地址: How to unit test a class that consumes an HttpClient with IHttpClientFactory in ASP.NET Core? ...

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

    通过一个小组件,熟悉 Blazor 服务端组件开发.github 一.环境搭建 vs2019 16.4, asp.net core 3.1 新建 Blazor 应用,选择 asp.net core 3 ...

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

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

随机推荐

  1. PowerDesigner的文章

    来自iteye的PowerDesigner的相关文章,写的不错,这里引用一下 PD12 建模工具的使用 博客分类: PowerDesigner .net http://jpkc.zjbti.net.c ...

  2. cesium入门示例-HelloWorld

    示例准备: 在Cesium ion官网(https://cesium.com/)上注册用户,获取AccessToken,在js代码入口设置Cesium.Ion.defaultAccessToken,即 ...

  3. 吴裕雄--天生自然 R语言开发学习:回归

    #------------------------------------------------------------# # R in Action (2nd ed): Chapter 8 # # ...

  4. iOS自动化登录测试demo

    <软件自动化测试开发>出版了 测试开发公开课培训大讲堂 微信公众号:测试开发社区 测试开发QQ群:173172133 咨询QQ:7980068 咨询微信:zouhui1003it

  5. Hexo和github搭建个人博客 - 朱晨

    GitHub账号 mac/pc 环境 12 node.jsgit 创建GitHub仓库 登陆GitHub,创建一个新的Respository Repository name叫做{username}.g ...

  6. 金矿还是大坑 VR创业真有那么美好?

    VR创业真有那么美好?"> 近段时间,一个段子在疯狂流传:彩票中奖的1000万,其实是存放在银行里,而彩民每次花两块钱买彩票,其实就是去输一次密码,只要够坚持,总会取出那1000万-- ...

  7. 1,Hadoop知识储备

    Hadoop初学思维导图 1,Hadoop ··· Hadoop:     Hadoop的核心由HDFS和MapReduce组成.HDFS是分布式文件系统,是Hadoop生态圈的分布式数据存储基石:M ...

  8. 在Linux上显示正在运行的进程的线程ID

    在Linux上显示正在运行的进程的线程ID 在上Linux," ps -T"可以显示正在运行的进程的线程信息: # ps -T 2739 PID SPID TTY STAT TIM ...

  9. spring——AOP原理及源码(一)

    教程共分为五篇,从AOP实例的构建及其重要组件.基本运行流程.容器创建流程.关键方法调用.原理总结归纳等几个方面一步步走进AOP的世界. 本篇主要为读者演示构建AOP实例及AOP核心组件分析. 一.项 ...

  10. 第二章 表与指针Pro SQL Server Internal (Dmitri Korotkev)

    聚集索引 聚集索引就是表中数据的物理顺序,它是按照聚集索引分类的.表只能定义一个聚集索引. 如果你要在一个有数据的堆表中创建一个聚集索引,如2-5所示,第一步要做的就是SQL服务器创建另一个根据聚集索 ...