In ASP.NET Core 3.1 project I have a custom IStringLocalizerFactory
that works with database through entity framework:
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:
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. I have the following code to register my dependencies at 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. 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 WebInterfaceViewsHomeIndexNew.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. Is there any better/proper way of handling concurrency within IStringLocalizerFactory
aside from introducing global locks or other inefficient techniques?
question from:
https://stackoverflow.com/questions/65942300/asp-net-core-istringlocalizerfactory-concurrency