Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Using Localization with an EF Context - resource string caching issue #7636

Closed
J0nKn1ght opened this issue Apr 12, 2018 · 10 comments
Closed
Assignees
Labels

Comments

@J0nKn1ght
Copy link

J0nKn1ght commented Apr 12, 2018

Hi,

I've successfully implemented an IStringLocalizer service as a singleton in my project, but I have an issue which I'm not sure how to address.

To give you some context, the site we're developing will have base English content which will be translated by people who are located around the world. The number of languages supported will be added to over time, as an when translators are found. One of their tasks, in addition to translating the dynamic site content, will be to translate the static content which is stored in the SQL resource table.

The problem I have is that the singleton data context used for the IStringLocalizer seem to cache the values. This means that if you update a translation in the admin site, and then go to the live site to view the result, the cached (old) value will be used. We don't really want to resort to having to manually go onto Azure and restart the site whenever someone indicates that they have updated the resources.

I tried changing the scope of the DBContext to Scoped, but this causes an exception saying that it can't be consumed from a Singleton (Cannot consume scoped service
'Microsoft.Extensions.Localization.IStringLocalizerFactory' from singleton 'Microsoft.Extensions.Options.IOptions'1[Microsoft.AspNetCore.Mvc.MvcOptions]')

We therefore need to find a way of either re-initialising the DBContext, or re-initialising the Localizer service (or, we could just abandon trying to use the Localizer functionality, and roll our own solution). We could then call this whenever the resources were updated on the admin site. I guess that if there was a way to prevent the context from caching, this would also be an option, but it seems like it would not be the optimal solution.

Does anyone know how, from a controller action, we can force the reloading of the Localizer service, or get it to re-initialise its database context?

@mkArtakMSFT
Copy link
Member

Hi. It looks like this is a question about how to use ASP.NET Core. While we do our best to look through all the issues filed here, to get a faster response we suggest posting your questions to StackOverflow using the asp.net-core-mvc tag.

@mkArtakMSFT
Copy link
Member

Hi @J0nKn1ght.
Can you share a repro project?
Would be interesting to see how you've configured the db-backed localization.

@J0nKn1ght
Copy link
Author

Hi @mkArtakMSFT,

Thanks for your interest. Unfortunately I can't share the repo, as it's for a customer project. The main parts are below though. I struggled to work out how to do this, to be honest, as it was right after .Net Core v2 was released, and there was scant information on how to implement it (and actually, there still is with regard to using string resources in a database).

There is also another issue, which I suspect is down to how I've implemented it, in that we occasionally get EF exceptions: 'The connection was not closed. The connection's current state is open.'. I'm not sure if this is related to the fact that we have to declare the database context as Singleton?

As the localizer is required to be a Singleton, I also had to create a second EF context specifically for it.

Anyway, here's the code:

The Startup.cs file contains the following:

services.AddDbContext<LocalizationContext>(options => options
.UseSqlServer(Configuration.GetConnectionString("LocalizationContext")),
                ServiceLifetime.Singleton);
services.AddEFStringLocalizer();

Here's the definition for AddEfStringLocalization:

    public static class EFStringLocalizerExtensions
    {

        public static IServiceCollection AddEFStringLocalizer(this IServiceCollection services)
        {

            if (services == null)
                throw new ArgumentNullException(nameof(services));

            services.TryAddSingleton<IStringLocalizerFactory, EFStringLocalizerFactory>();
            services.TryAddSingleton<IStringLocalizer, EFStringLocalizer>();

            return services;
        }

    }

Here are the EFStringLocalizerFactory and EFStringLocalizer classes:

    public class EFStringLocalizerFactory : IStringLocalizerFactory
    {
        private readonly LocalizationContext _context;

        public EFStringLocalizerFactory(LocalizationContext context)
        {
            _context = context;
        }
        public IStringLocalizer Create(Type resourceSource)
        {
            return new EFStringLocalizer(_context);
        }
        public IStringLocalizer Create(string baseName, string location)
        {
            return new EFStringLocalizer(_context);
        }
    }
    public class EFStringLocalizer : IStringLocalizer
    {
        private readonly LocalizationContext _context;

        public EFStringLocalizer(LocalizationContext context)
        {
            _context = context;
        }

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

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

        public IStringLocalizer WithCulture(CultureInfo culture)
        {
            CultureInfo.DefaultThreadCurrentCulture = culture;
            return new EFStringLocalizer(_context);
        }

        public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
        {
            return _context.Resources.Where(r => r.CultureCode == CultureInfo.CurrentUICulture.TwoLetterISOLanguageName).Select(r => new LocalizedString(r.Key, r.Value, false));
        }

        private string GetString(string name)
        {
            string language = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
            var result = _context.Resources.Where(r => r.CultureCode == language && r.Key == name).FirstOrDefault();
            if (result == null && language != CultureConstants.DefaultLanguage)
                result = _context.Resources.Where(r => r.CultureCode == CultureConstants.DefaultLanguage && r.Key == name).FirstOrDefault();

            return result?.Value;
        }

    }

N.B. We're supporting language variations, rather than full culture variations (i.e. both language and region), hence the use of CurrentUICulture.TwoLetterISOLanguageName above.

I'd be interested in any observations or recommendations that you have.

@J0nKn1ght
Copy link
Author

Actually, we've just got round this by creating a scoped repository service for the resources stored in our database, rather than use the built-in IStringLocalizer support. This means we now only need a single EF DBContext, which is injected into the service constructor (whereas before we needed one scoped and one singleton context).

This will obviously lead to more database hits, but we don't have the EF problems we were finding with the Singleton, and it also means that we can update the resources from an admin page, and they are instantly reflected on the site.

Can you see any issues with localizing using this approach?

@mkArtakMSFT
Copy link
Member

Thanks for the details @J0nKn1ght. Glad that you've found a solution with the EF context.
As for the localization, I'll delegate that to our localization expert for this.
@ryanbrandenburg, do you see any issues regarding above db-based localization implementation?

@ryanbrandenburg
Copy link
Contributor

It sounds like you've got this figured out @J0nKn1ght. I don't see any problems with your approach from a localization perspective, but you seem to have EF problems/questions. I believe that you've hit on the correct approach to using DbContext items (which to my knowledge are not thread safe, and therefor should be recreated per-request), but I'm not an EF expert so we might want to have one of them look it over.

@J0nKn1ght
Copy link
Author

Thanks for your input. I get the impression that the Localization support in MVC Core is still focused on using traditional resource files, where a singleton makes more sense. Maybe for future versions the scenario of using different back-end stores for resources could be considered.

@ryanbrandenburg
Copy link
Contributor

Do you have any suggestions for things we could do to make the scoped scenario easier? Maybe you left some out but it seems like the boilerplate you posted to get a custom store going was relatively minimal, maybe just some more documentation in this area?

@J0nKn1ght
Copy link
Author

More documentation (as always), would definitely be useful - especially more example code. I think that the problem that I had was that I started to use the built-in support for IStringLocalizer thinking this was the "right" way to do localisation in MVC Core, but our requirements didn't really match the use case for the built-in support (i.e. fixed resource files that would require a site publish to update).

I think that it's a credit to the MVC Core architecture that it's so easy to implement your own services and middleware - you just need to recognise when it's time to implement your own code, rather than trying to work around any limitations of the built in support.

@mkArtakMSFT
Copy link
Member

Just created a separate issue to come up with a sample showcasing our advised approach for using localization with EF. dotnet/aspnetcore#3080

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

3 participants