简体   繁体   English

ASP.NET 核心 IStringLocalizerFactory 并发

[英]ASP.NET Core IStringLocalizerFactory concurrency

In ASP.NET Core 3.1 project I have a custom IStringLocalizerFactory that works with database through entity framework:在 ASP.NET Core 3.1 项目中,我有一个自定义的IStringLocalizerFactory ,它通过实体框架与数据库一起使用:

public class EFStringLocalizerFactory : IStringLocalizerFactory
{
    private readonly LocalizationContext _context;
    private readonly IMemoryCache _memoryCache;
    private static readonly ConcurrentDictionary<string, IStringLocalizer> InternalLocalizersHolder = new ConcurrentDictionary<string, IStringLocalizer>();

    public EFStringLocalizerFactory(LocalizationContext context, IMemoryCache memoryCache)
    {
        _context = context;
        _memoryCache = memoryCache;
    }

    public IStringLocalizer Create(Type resourceSource)
    {
        return CreateStringLocalizer(_context, _memoryCache, resourceSource.FullName);
    }

    public IStringLocalizer Create(string baseName, string location)
    {
        return CreateStringLocalizer(_context, _memoryCache, baseName);
    }

    internal static IStringLocalizer CreateStringLocalizer(LocalizationContext context, IMemoryCache memoryCache, string resourceSection)
    {
        return InternalLocalizersHolder.GetOrAdd(resourceSection, s => new EFStringLocalizer(context, memoryCache, s));
    }
}

EFStringLocalizer class looks like this: EFStringLocalizer class 看起来像这样:

public class EFStringLocalizer : IStringLocalizer
{
    private readonly LocalizationContext _context;
    private readonly IMemoryCache _translationsCache;
    private readonly string _resourceSection;


    public EFStringLocalizer(LocalizationContext context, IMemoryCache memoryCache, string resourceSection)
    {
        _context = context;
        _translationsCache = memoryCache;
        _resourceSection = resourceSection;
    }

    public LocalizedString this[string name]
    {
        get
        {
            var value = GetString(name);
            return new LocalizedString(name, value ?? name, resourceNotFound: value == name);
        }
    }

    public LocalizedString this[string name, params object[] arguments]
    {
        get
        {
            var format = GetString(name);
            var value = string.Format(format ?? name, arguments);
            return new LocalizedString(name, value, resourceNotFound: format == null);
        }
    }

    public IStringLocalizer WithCulture(CultureInfo culture)
    {
        CultureInfo.DefaultThreadCurrentCulture = culture;
        return EFStringLocalizerFactory.CreateStringLocalizer(_context, _translationsCache, _resourceSection);
    }

    //TODO fix parameter usage?
    public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
    {
        try
        {
            return _translationsCache.GetOrCreate($"LocalizerGetAllStrings-{CultureInfo.CurrentCulture.Name}-{_resourceSection}", entry =>
            {
                var keysWithTranslations = _context.Resources
                    .Include(r => r.Culture)
                    .Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name && r.Section == _resourceSection)
                    .Select(r => new LocalizedString(r.Key, r.Value)).ToList();

                return keysWithTranslations;
            });
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }

    private string GetString(string name)
    {
        return GetAllStrings(false).FirstOrDefault(r => r.Name == name)?.Value;
    }
}

Resource/Culture classes are just POCOs stored in Database.资源/文化类只是存储在数据库中的 POCO。 I have the following code to register my dependencies at Startup.cs class:我有以下代码在 Startup.cs class 注册我的依赖项:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddDbContext<LocalizationContext>(options =>
                    options.UseSqlServer(
                        Configuration.GetConnectionString("DefaultLocalizationConnection")));//still concurrency error!
    ...
    services.AddSingleton<IStringLocalizerFactory, EFStringLocalizerFactory>();
    ...
}

The problem is whenever the application recycles/is restarted there is a certain chance that i get an exception related to EF concurrency.问题是每当应用程序回收/重新启动时,我都有一定的机会获得与 EF 并发相关的异常。 What makes it harder is that I was not able to reliably reproduce the issue.更难的是我无法可靠地重现该问题。 Here is the stack trace:这是堆栈跟踪:

2021-01-28 15:00:07.2356|ERROR|Microsoft.EntityFrameworkCore.Query|System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()|An exception occurred while iterating over the results of a query for context type 'DataAccess.LocalizationContext'.
System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
2021-01-28 15:00:07.2615|ERROR|Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware|System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
   at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at Localization.EFStringLocalizer.<GetAllStrings>b__9_0(ICacheEntry entry) in EFStringLocalizer.cs:line 58
   at Microsoft.Extensions.Caching.Memory.CacheExtensions.GetOrCreate[TItem](IMemoryCache cache, Object key, Func`2 factory)
   at Localization.EFStringLocalizer.GetAllStrings(Boolean includeAncestorCultures) in EFStringLocalizer.cs:line 56
   at Localization.EFStringLocalizer.GetString(String name) in EFStringLocalizer.cs:line 80
   at Localization.EFStringLocalizer.get_Item(String name) in EFStringLocalizer.cs:line 30
   at Microsoft.AspNetCore.Mvc.Localization.HtmlLocalizer.get_Item(String name)
   at Microsoft.AspNetCore.Mvc.Localization.HtmlLocalizer`1.get_Item(String name)
   at AspNetCore.Views_Home_IndexNew.ExecuteAsync() in WebInterface\Views\Home\IndexNew.cshtml:line 15
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ActionContext actionContext, IView view, ViewDataDictionary viewData, ITempDataDictionary tempData, String contentType, Nullable`1 statusCode)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor.ExecuteAsync(ActionContext context, ViewResult result)
   at Microsoft.AspNetCore.Mvc.ViewResult.ExecuteResultAsync(ActionContext context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.<Invoke>g__AwaitMatcher|8_0(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task`1 matcherTask)
   at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
   at WebInterface.Startup.<>c.<<AddSecurityMiddlewares>b__12_4>d.MoveNext() in Startup.cs:line 284
--- End of stack trace from previous location where exception was thrown ---
   at NWebsec.AspNetCore.Middleware.Middleware.CspMiddleware.Invoke(HttpContext context)
   at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
   at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
   at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
   at NWebsec.AspNetCore.Middleware.Middleware.MiddlewareBase.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)|An unhandled exception has occurred while executing the request.

I believe that registering LocalizationContext with transient scope will not help as EFStringLocalizerFactory is registered as singleton anyway.我相信使用瞬态 scope 注册LocalizationContext将无济于事,因为EFStringLocalizerFactory无论如何都注册为 singleton 。 Is there any better/proper way of handling concurrency within IStringLocalizerFactory aside from introducing global locks or other inefficient techniques?除了引入全局锁或其他低效技术之外,在IStringLocalizerFactory中是否有更好/正确的方法来处理并发?

I believe that registering LocalizationContext with transient scope will not help as EFStringLocalizerFactory is registered as singleton anyway.我相信使用瞬态 scope 注册 LocalizationContext 将无济于事,因为 EFStringLocalizerFactory 无论如何都注册为 singleton 。

Correct.正确的。

Is there any better/proper way of handling concurrency within IStringLocalizerFactory aside from introducing global locks or other inefficient techniques?除了引入全局锁或其他低效技术之外,在 IStringLocalizerFactory 中是否有更好/正确的方法来处理并发?

Not as far as I know.据我所知不是。

EF Core DbContexts only support one operation at a time, which I think the error message is clear about. EF Core DbContexts 一次只支持一个操作,我认为错误消息很清楚。 The other factor is that the in-memory cache implementation doesn't do any sort of locking, so the lambda expression used to create the cache entry can be executed concurrently by several consumers wanting to read from the cache.另一个因素是内存中的缓存实现不进行任何类型的锁定,因此用于创建缓存条目的 lambda 表达式可以由多个想要从缓存中读取的消费者同时执行。

Explicitly locking is the way to go IMO, with two options:显式锁定是 go IMO 的方式,有两种选择:

  • Around the GetOrCreate method, meaning you can guarantee the EF Core query will only be run once, but no 2 consumers will be able to read from the cache concurrently;围绕 GetOrCreate 方法,意味着您可以保证 EF Core 查询只会运行一次,但不会有 2 个消费者能够同时从缓存中读取; or或者
  • Around the EF Core query, meaning you can potentially override an existing cache entry, but consumers can then read from the cache concurrently.围绕 EF Core 查询,这意味着您可以潜在地覆盖现有的缓存条目,但消费者可以同时从缓存中读取。

I'd personally go with option 2 and use a SemaphoreSlim instance.我个人 go 与选项 2 并使用 SemaphoreSlim 实例。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM