An EF exception raised by ASP.NET Core ActionFilter

An EF exception raised by ASP.NET Core ActionFilter

A strange problem occurred recently when using ASP.NET Core. After using an ActionFilter on a Controller, EF errors often occur.

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()

This exception says that the second operation basis starts when the Context completes the previous operation. This error does not appear every time. It only appears when the concurrency is strong. It can basically be judged that it has something to do with multithreading. Take a look at the code:

   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();
        }
       
    }

The code is very simple, DbContext uses the Scope life cycle; there is only one Action in the Controller, and there is only one place to access the database. How can it cause errors in multi-threaded access to Context? So I moved my gaze to the Attribute of BasicAuthenticationAttribute.

 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))
            {
               //If the secret is not set, pass directly
                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;
        }
    }

The code of BasicAuthenticationAttribute is also very simple. Attribute injects a Service and rewrites the OnActionExecuting method, which performs Basic authentication on Http requests. There has also been a data query here, but await has been added. At first glance, there seems to be no problem. When an Http request comes in, it will first enter this Filter to perform Basic authentication on it. If it fails, it will return a 403 code. If it succeeds, it will enter the real Action method to continue execution. If this is the logic, it is impossible for two EF operations to be executed at the same time. Continue to find the problem and click on the metadata of the 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);
    }

This thing looks a bit different from before. In addition to the original 4 methods, there are 2 more Async ending methods. When I got here, I knew it in my mind. OnResultExecutionAsync should be rewritten here, because our Action method is an asynchronous method. Change the BasicAuthenticationAttribute and rewrite the OnResultExecutionAsync method:

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))
            {
               //If the secret is not set, pass directly
                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;
        }
    }

After the modification, the problem of EF error reporting has been resolved after concurrent testing. Let me explain how this problem is caused: At first, BasicAuthenticationAttribute was migrated from the framework version of ASP.NET MVC, and OnActionExecuting was rewritten as usual. The method in the injected service is asynchronous. Although await is marked, this is not useful, because the framework does not add await in front of it to wait for this method when calling OnActionExecuting. Therefore, a Filter that rewrites OnActionExecuting does not execute the action after OnActionExecuting is executed as preset when it is executed with an asynchronous Action. If there is an asynchronous method in OnActionExecuting, then this asynchronous method is likely to be executed at the same time as the asynchronous method in the Action, so that the abnormal problem of EF Context being multi-threaded operation occurs when high concurrency occurs. In fact, it is still a commonplace question here, that is, try not to call asynchronous methods in synchronous methods, so it is easy to cause multi-threading problems and even deadlocks. ASP.NET Core has fully embraced asynchrony, and there is a big difference from the framework version, but more attention is needed. It seems that this Core version of ActionFilter has to be studied carefully, so I checked on Microsoft's official website and found such a paragraph:

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.

That is to say, for the filter interface, either the synchronous version of the method or the asynchronous version of the method is implemented, not at the same time. The runtime will first see if the asynchronous version of the method is implemented, and if it is implemented, it will be called. If not, call the synchronous version. If both the synchronous version and the asynchronous version of the method are implemented at the same time, only the asynchronous version of the method will be called. When using abstract classes, such as ActionFilterAttribute, you only need to override one of the synchronous or asynchronous methods.

Reference: https://cloud.tencent.com/developer/article/1601779 ASP.NET Core ActionFilter raises an EF exception