Open Closed

Custom AbpStringLocalizerFactory #10023


User avatar
0

I would like to use a custom implementation of AbpStringLocalizerFactory, so that I can add metrics and watch what's going on. I suspect that localization isn't being cached correctly in my instance (for example, host admin is super fast but all tenant pages that contain lots of localization are slow), but to know what's going on, I need a custom implementation.

I have the following, but it's just not getting called.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using JetBrains.Annotations;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Localization;
using Volo.Abp.Localization.External;

namespace CabMD.Localization
{
    /// <summary>
    /// A factory for creating <see cref="IStringLocalizer"/> instances that tracks performance metrics  for
    /// localization operations. This factory extends the functionality of <see cref="AbpStringLocalizerFactory"/>  by
    /// monitoring and logging the performance of string lookups and enumerations.
    /// </summary>
    /// <remarks>This factory wraps the created <see cref="IStringLocalizer"/> instances with a
    /// performance-tracking  implementation, enabling the measurement of slow localization operations. It is designed
    /// to help  identify and diagnose performance bottlenecks in localization processes.</remarks>
    [UsedImplicitly]
    [Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(IStringLocalizerFactory), typeof(IAbpStringLocalizerFactory))]
    public class PerformanceTrackingStringLocalizerFactory : AbpStringLocalizerFactory
    {
        private readonly ILoggerFactory _loggerFactory;
        private readonly ILocalizationPerformanceMonitor _performanceMonitor;
        private const int SlowLookupThresholdMs =50;
        private const int SlowEnumerationThresholdMs =100;

        public PerformanceTrackingStringLocalizerFactory(
            ResourceManagerStringLocalizerFactory innerFactory,
            IOptions<AbpLocalizationOptions> abpLocalizationOptions,
            IServiceProvider serviceProvider,
            IExternalLocalizationStore externalLocalizationStore,
            ILocalizationPerformanceMonitor performanceMonitor,
            ILoggerFactory loggerFactory)
            : base(innerFactory, abpLocalizationOptions, serviceProvider, externalLocalizationStore)
        {
            _performanceMonitor = performanceMonitor;
            _loggerFactory = loggerFactory;
        }

        public override IStringLocalizer Create(Type resourceType)
        {
            var inner = base.Create(resourceType);
            return Wrap(inner, !string.IsNullOrEmpty(resourceType.Name) ? resourceType.Name : "Unknown");
        }

        public override IStringLocalizer Create(string baseName, string location)
        {
            var inner = base.Create(baseName, location);
            return Wrap(inner, $"{baseName}@{location}");
        }

        private PerformanceTrackingStringLocalizer Wrap(IStringLocalizer inner, string resourceName)
        {
            return new PerformanceTrackingStringLocalizer(
                inner,
                resourceName,
                _performanceMonitor,
                _loggerFactory.CreateLogger<PerformanceTrackingStringLocalizer>(),
                SlowLookupThresholdMs,
                SlowEnumerationThresholdMs);
        }
    }

    internal class PerformanceTrackingStringLocalizer : IStringLocalizer
    {
        private readonly IStringLocalizer _inner;
        private readonly string _resourceName;
        private readonly ILocalizationPerformanceMonitor _monitor;
        private readonly ILogger<PerformanceTrackingStringLocalizer> _logger;
        private readonly int _lookupThresholdMs;
        private readonly int _enumerationThresholdMs;

        public PerformanceTrackingStringLocalizer(
            IStringLocalizer inner,
            string resourceName,
            ILocalizationPerformanceMonitor monitor,
            ILogger<PerformanceTrackingStringLocalizer> logger,
            int lookupThresholdMs,
            int enumerationThresholdMs)
        {
            _inner = inner;
            _resourceName = resourceName;
            _monitor = monitor;
            _logger = logger;
            _lookupThresholdMs = lookupThresholdMs;
            _enumerationThresholdMs = enumerationThresholdMs;
        }

        public LocalizedString this[string name]
        {
            get
            {
                var sw = Stopwatch.StartNew();
                var culture = Thread.CurrentThread.CurrentUICulture.Name;
                try
                {
                    var result = _inner[name];
                    sw.Stop();
                    Record(name, sw.ElapsedMilliseconds, culture);
                    WarnSlow(sw.ElapsedMilliseconds, name, culture, false,0);
                    return result;
                }
                catch (Exception ex)
                {
                    sw.Stop();
                    _logger.LogError(ex, "Error localizing key '{Key}' in resource '{Resource}' for culture '{Culture}' after {ElapsedMs}ms", name, _resourceName, culture, sw.ElapsedMilliseconds);
                    throw;
                }
            }
        }

        public LocalizedString this[string name, params object[] arguments]
        {
            get
            {
                var sw = Stopwatch.StartNew();
                var culture = Thread.CurrentThread.CurrentUICulture.Name;
                try
                {
                    var result = _inner[name, arguments];
                    sw.Stop();
                    Record(name, sw.ElapsedMilliseconds, culture);
                    WarnSlow(sw.ElapsedMilliseconds, name, culture, true, arguments.Length);
                    return result;
                }
                catch (Exception ex)
                {
                    sw.Stop();
                    _logger.LogError(ex, "Error localizing key '{Key}' with arguments in resource '{Resource}' for culture '{Culture}' after {ElapsedMs}ms", name, _resourceName, culture, sw.ElapsedMilliseconds);
                    throw;
                }
            }
        }

        public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
        {
            var sw = Stopwatch.StartNew();
            var culture = Thread.CurrentThread.CurrentUICulture.Name;
            try
            {
                var result = _inner.GetAllStrings(includeParentCultures);
                sw.Stop();
                if (sw.ElapsedMilliseconds > _enumerationThresholdMs)
                {
                    _logger.LogWarning("GetAllStrings for resource '{Resource}' and culture '{Culture}' took {ElapsedMs}ms", _resourceName, culture, sw.ElapsedMilliseconds);
                }
                return result;
            }
            catch (Exception ex)
            {
                sw.Stop();
                _logger.LogError(ex, "Error in GetAllStrings for resource '{Resource}' and culture '{Culture}' after {ElapsedMs}ms", _resourceName, culture, sw.ElapsedMilliseconds);
                throw;
            }
        }

        private void Record(string name, long elapsedMs, string culture)
        {
            var key = string.IsNullOrEmpty(_resourceName) ? name : $"{_resourceName}:{name}";
            _monitor.RecordMetric(key, elapsedMs, culture);
        }

        private void WarnSlow(long elapsedMs, string name, string culture, bool hasArgs, int argCount)
        {
            if (elapsedMs <= _lookupThresholdMs) return;
            if (hasArgs)
            {
                _logger.LogWarning("Slow localization with args: Resource='{Resource}', Key='{Key}', Culture='{Culture}', ArgCount={ArgCount}, Time={ElapsedMs}ms", _resourceName, name, culture, argCount, elapsedMs);
            }
            else
            {
                _logger.LogWarning("Slow localization: Resource='{Resource}', Key='{Key}', Culture='{Culture}', Time={ElapsedMs}ms", _resourceName, name, culture, elapsedMs);
            }
        }
    }
}

Do you have any suggestions?


4 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent

    To use a custom implementation of AbpStringLocalizerFactory in ABP, you must ensure that your replacement is registered in the correct module and project, and that it is picked up before any localization services are resolved. Here are the steps and checks to ensure your custom factory is used:

    1. Correct Service Replacement

      • Use [Dependency(ReplaceServices = true)] and [ExposeServices(typeof(IStringLocalizerFactory), typeof(IAbpStringLocalizerFactory))] as you did.
      • Make sure your custom factory is in a module that is loaded before any localization is used (typically in your .Domain.Shared or .Web module, depending on your architecture).
    2. Module Registration

      • If you are using a tiered architecture, ensure the custom factory is registered in the correct layer (e.g., in the .HttpApi or .Web project, not just in .Application or .Domain).
    3. Service Lifetime

      • ABP uses Transient lifetime for IStringLocalizerFactory. Ensure your custom implementation is also registered as transient.
    4. Virtual File System and Embedded Resources

      • If you override localization files, ensure they are marked as EmbeddedResource in your .csproj file. Otherwise, ABP may not find your custom resources, especially in production.
    5. Check for Multiple Registrations

      • If another module or library registers its own IStringLocalizerFactory, it may override yours. Use ABP's context.Services.Replace in your module's ConfigureServices method to ensure your implementation is the last one registered:
    context.Services.Replace(
             ServiceDescriptor.Transient<IStringLocalizerFactory, PerformanceTrackingStringLocalizerFactory>()
         );
         context.Services.Replace(
             ServiceDescriptor.Transient<IAbpStringLocalizerFactory, PerformanceTrackingStringLocalizerFactory>()
         );
    
    1. Debugging Registration

      • Add a breakpoint or log in your factory's constructor to verify it is being instantiated.
      • If not, check the order of module initialization and service registration.
    2. Reference Official Docs

    3. Production vs Development

      • If it works in development but not in production, double-check that your custom DLL is deployed and that no other startup logic is interfering.

    Summary of what to check:

    • Correct module and project for registration.
    • Use context.Services.Replace in ConfigureServices.
    • Ensure no other factory is registered after yours.
    • Mark localization files as EmbeddedResource if you override them.

    If you follow these steps and your custom factory is still not called, review the module dependency order and ensure your module is loaded last or after all ABP modules that might register their own factory.

    Sources:


    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

  • User Avatar
    0

    Not quite clear, I think, because of the way that AbpStringLocalizerFactory itself gets "registered".

  • User Avatar
    0

    Is there an example of this anywhere? This seems to be the only detail that might drastically improve our production performance.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I suspect that localization isn't being cached correctly in my instance (for example, host admin is super fast but all tenant pages that contain lots of localization are slow), but to know what's going on, I need a custom implementation.

    How can I reproduce the issue in a new template project? I can help you troubleshoot the cause.

    Thanks

Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.1.0-preview. Updated on October 21, 2025, 11:36