专栏首页.NET开发那点事ASP.NET Core ActionFilter引发的一个EF异常

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还得仔细研究研究,于是上微软官网查了查有这么一段:

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,只需重写同步方法或者异步方法其中一个。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Azure认知服务之使用墨迹识别功能识别手写汉字

    前面我们使用Azure Face实现了人脸识别、使用Azure表格识别器提取了表格里的数据。这次我们试试使用Azure墨迹识别API来对笔迹进行识别。

    kklldog
  • ASP.NET Core中的Action的返回值类型

    在Asp.net Core之前所有的Action返回值都是ActionResult,Json(),File()等方法返回的都是ActionResult的子类。并...

    kklldog
  • .NET开源高性能Socket通信中间件Helios介绍及演示

      Helios是一套高性能的Socket通信中间件,使用C#编写。Helios的开发受到Netty的启发,使用非阻塞的事件驱动模型架构来实现高并发高吞吐量。H...

    kklldog
  • .Net Core在Middleware中解析RouteData

    在ASP.Net Core中,如果直接在Middleware中获取RouteData返回的是空值,这是因为RouterMiddleware还没执行。但有些情况下...

    Mr. Wei
  • .Net Core 权限验证与授权(AuthorizeFilter、ActionFilterAttribute)

    在.Net Core 中使用AuthorizeFilter或者ActionFilterAttribute来实现登录权限验证和授权

    小世界的野孩子
  • 你必须了解的反射——反射来实现实体验证

    日常开发,都是通过API进行前后端的系统对接,对API参数的验证是一个使用率非常高的功能,如果能非常简便的的进行参数验证,能降低代码量,提升工作效率。

    itmifen
  • Spring Cloud微服务-全栈技术与案例解析(文末送此书!)

    在Spring Cloud中我们用Hystrix来实现断路器,默认是用信号量来进行隔离的,我们可以通过配置使用线程方式隔离。

    纯洁的微笑
  • 你必须了解的反射——反射来实现实体验证

    开发工作中,都会需要针对传入的参数进行验证,特别是针对实体进行验证,确保传入的参数格式正确。这里做了一个非常简单的组件进行验证。抛砖引玉,让大家深入思考下反射的...

    itmifen
  • Unity NavMesh 动态烘焙绘制与随机取点

    最初的Unity导航系统很不完善,只能静态烘焙场景图的可行走区域,而且必须在本地保存场景的NavMesh数据,难以运行时动态计算;这使得鲜有开发者愿意再尝试Un...

    汐夜koshio
  • EntityFramework 外键值映射

    如果在 EF OnModelCreating 中配置了实体外键映射,也就是 SQL Server 中的 ForeignKey,那么我们在添加实体的时候,主实体的...

    逸鹏

扫码关注云+社区

领取腾讯云代金券