ASP.NET Core ActionFilter EF anomaly caused by a

Recently there has been a strange problem when using ASP.NET Core of. EF often used after being given a ActionFilter on a Controller.

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

The exception says Context beginning of time prior to the completion of the operation of a second operation basis. This error will not occur every time, only occur when strong concurrency, which can be judged with multi-threaded relationship. 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 use Scope life cycle; Controller only one Action, there is only one place to access the database. How wrong can cause multiple threads access the Context of it? Then turned his gaze to BasicAuthenticationAttribute this 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 code is also very simple, Attribute injected a Service and rewrite OnActionExecuting method, Basic authentication method in Http request. There is also the emergence of a data query, but have been added await. At first glance it seems no problem, a Http request comes in, the first thing would be to enter the Filter Basic authentication, if it fails to return 403 yards, if successful, to enter the real Action method continues. If this is the logic operation of the two EF impossible performed simultaneously. Continue to find problems, opening the metadata of 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 stuff does not look a bit the same as before ah, in addition to the original four methods, ending more than two Async method. In fact, my heart has been coming here a few of. It should rewrite OnResultExecutionAsync, because our Action method is asynchronous method. The change BasicAuthenticationAttribute, rewrite 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))
            {
                //如果没有设置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;
        }
    }

After modification through concurrent test, EF error issue has been resolved.
Explain again how this issue is caused by: a start BasicAuthenticationAttribute version of the ASP.NET MVC framework is migrated, as is customary rewrite OnActionExecuting. Which is injected inside the service method is asynchronous, despite marked await, but this is no egg, because the time frame in the call OnActionExecuting not preceded await to wait for this method. So a rewrite of Filter OnActionExecuting with an asynchronous time Action performed as the default does not like to wait OnActionExecuting then execute action after the execution. If the asynchronous method OnActionExecuting appeared that this approach is likely to simultaneously perform asynchronous with Action in the asynchronous method, so there is abnormal operation of the EF Context is multi-threaded when high concurrency. In fact, here it is a commonplace problem, is to try not to call in the synchronous method asynchronous method, so it is prone to multithreading problems and even deadlock.
ASP.NET Core has been fully embraced asynchronous, and have a very different framework version still needs a lot of attention. Research seems to have carefully studied the Core version of ActionFilter, so Microsoft's official website check the check has this to say:

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 filter interface for the synchronous version of the method or implement, or achieve the asynchronous version of the method, do not realize at the same time. We will first see the asynchronous version of the method has not achieved runtime, if the implementation is called. If there is no synchronization version is called. If the synchronous version with asynchronous version of the method are achieved at the same time, the method is only called asynchronous version. When using an abstract class, such as the ActionFilterAttribute, or simply override synchronization method wherein an asynchronous method.

Reference: Filters in ASP.NET Core

Guess you like

Origin www.cnblogs.com/kklldog/p/not-use-sync-in-actionfilter.html