Dear Support,
I have an Azure App with a per connection Redis Premium. I'm facing a slowness in anexport process. This export function take around 1000 items and during the calculation makes a extension usage of the translation system (calling the L(...) method). The function is working properly when used in a normal context but becomes super slow (minutes) when called from a background job. I guess there are some dependencies from some other environment variable but I wasn't able to identify to root cause of the issue.
5 Answer(s)
-
0
Hi @niengineering
Thank you for the detailed explanation. This behavior is expected and is related to how the Abp.AspNetCore.PerRequestRedisCache works internally.
In ASP.NET Zero / ABP Framework, localization results are cached using
PerRequestRedisCache. This cache relies onHttpContextto store localization values in an in memory layer (HttpContext.Items) during a web request.However, in a background job, there is no active HTTP request, so
HttpContextis always null. As a result, the in memory per request cache layer is bypassed, and every call toL(...)directly queries Redis.Each localization call typically results in 1-2 Redis round trips (tenant + host fallback). When processing large datasets (for example, 1000 items with multiple localization calls per item), this can lead to thousands of Redis calls, causing significant latency, especially when using Azure Redis over network.
This is why the export runs fast during normal HTTP requests but becomes slow in background jobs.
Recommended Solutions
Preload localization values once
Instead of calling
L(...)repeatedly inside loops, preload the required localization strings once and reuse them:var localizationSource = LocalizationManager.GetSource(AbpZeroTemplateConsts.LocalizationSourceName); var localizedStrings = new Dictionary<string, string>(); foreach (var key in neededKeys) { localizedStrings[key] = localizationSource.GetString(key); } // Use localizedStrings[key] inside loops instead of calling L(...)This ensures Redis is accessed only once per key, not once per iteration.
Use batch localization methods
ABP provides batch methods that load multiple localization values efficiently:
var source = LocalizationManager.GetSource(AbpZeroTemplateConsts.LocalizationSourceName); var strings = source.GetStrings(keyList, CultureInfo.CurrentUICulture);This reduces Redis round trips significantly by loading values in bulk.
This is not related to Azure Redis configuration or environment variables. It is caused by the absence of HttpContext in background jobs, which disables the per request in memory cache layer. Optimizing localization access by preloading or batching localization values will resolve the performance issue.
Please let us know if you need assistance implementing one of these approaches.
We will review this scenario and evaluate framework level enhancements to provide better on premise support and improve localization performance in backend business environments.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Clear, but this means a large review of a lot of code.
Do you think we can bypass this issue creating in some way an HttpContext simulating the needed day for the translation system?
I forgot to mention that the exact scenario is that the background job initiate an app service proxy call and that the translations are called inside the service proxy method.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi @niengineering
Yes, you can work around this without reviewing all your code. Since your background job calls an app service proxy and the translations happen inside that method, you can simulate an
HttpContextat the beginning of your background job. This will enable the per request in memory cache layer for localization, so repeatedL(...)calls within the same job execution will be served from memory instead of hitting Redis every time.In your background job's
Executemethod, add the following before calling the app service:var httpContextAccessor = IocManager.Resolve<IHttpContextAccessor>(); httpContextAccessor.HttpContext = new DefaultHttpContext();This creates a minimal
HttpContextwith an emptyItemsdictionary, which is all thePerRequestRedisCacheneeds to function. The first call to each localization key will still go to Redis, but all subsequent calls within the same job execution will be served from the in-memoryHttpContext.Itemscache exactly the same behavior as during a normal HTTP request.Make sure to clean up at the end:
try { httpContextAccessor.HttpContext = new DefaultHttpContext(); // ... your existing job logic (app service call, etc.) } finally { httpContextAccessor.HttpContext = null; }This is a single point change in your background job class no need to modify your app service or any localization calls.
Additionally, we have created a framework improvement issue to introduce an AsyncLocal-based fallback cache when HttpContext is not available. This will allow background jobs and other non-HTTP execution contexts to benefit from the in-memory per-scope caching layer automatically without requiring manual HttpContext simulation.
Please feel free to contact us if you have any questions. Thank you.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
It is working faster but looks it's not loading the correct localization environment. Is it possible? Can we force it? I see that even forcing session tenant id and user id is not enough to have the correct translation
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image) -
0
Hi @niengineering
Yes, this is expected. Creating an empty
DefaultHttpContext()fixes the Redis caching issue (making it faster), but it does not set up the culture/language correctly. Here's why and how to fix it.Why the wrong language is loaded
In a normal HTTP request, the
UseAbpRequestLocalization()middleware runs and resolves the correct language by checking (in order):- user language setting
- tenant default language
- application default
- cookie
Accept-Languageheader
It then sets
Thread.CurrentThread.CurrentUICulture, which is whatL(...)reads.With
new DefaultHttpContext(), this middleware never executes. The thread culture remains the server's OS default (e.g.,en-US), so allL(...)calls return translations for that default culture regardless of the tenant/user's actual language preference.The Fix
In the background job, after creating the
DefaultHttpContextand setting the tenant/user session, you need to explicitly resolve and set the culture.The correct pattern already exists in the codebase at
UserCollectedDataPrepareJob.cs:// Inside your background job's Execute method: try { var httpContextAccessor = IocManager.Resolve<IHttpContextAccessor>(); httpContextAccessor.HttpContext = new DefaultHttpContext(); // Resolve the user's (or tenant's) preferred language from settings var userLanguage = await SettingManager.GetSettingValueForUserAsync( LocalizationSettingNames.DefaultLanguage, new UserIdentifier(tenantId, userId) ); // Or if you only know the tenant (not a specific user): // var tenantLanguage = await SettingManager.GetSettingValueForTenantAsync( // LocalizationSettingNames.DefaultLanguage, tenantId); var culture = CultureHelper.GetCultureInfoByChecking(userLanguage); using (CultureInfoHelper.Use(culture)) { // All L(...) calls inside this block will use the correct language // ... your existing export logic here ... } } finally { httpContextAccessor.HttpContext = null; }CultureInfoHelper.Use(culture)sets bothThread.CurrentThread.CurrentCultureandThread.CurrentThread.CurrentUICulturefor the duration of theusingblock, ensuring that allL(...)calls resolve to the correct language.This is the same pattern used in
UserCollectedDataPrepareJob, which is the only background job in the codebase that correctly handles culture resolution.If this explanation is insufficient, or if you need further assistance on a different matter, please feel free to contact us.
Thank you
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image)