I already did this two years ago, have a look https://github.com/aspnet/Entropy/tree/dev/samples/Localization.EntityFramework
@mkArtakMSFT Is there any extra things we can add to it?
Yeah, I think that sample handles this scenario just fine. Closing this, @mkArtakMSFT feel free to re-open if there's a scenario we want covered that this doesn't.
Thanks for this example, but I think that a 'real world' example would be more helpful. This doesn't have any controller actions, or consume the localizer from a view - it only uses an instance created in the startup method which is then immediately used to write out a response, which I don't think is a typical scenario.
Also, it instantiates the EF context in the factory class, whereas I thought that adding the EF contexts to the services collection in Startup and then using DI was the preferred approach. Is that not the case? This is where I ran into problems with Scoped vs Singleton.
I think that my wider point really is that when examples are too simplistic, or demonstrate the usage in atypical scenarios, it makes it more difficult for developers trying to learn the technology to apply them to their projects.
@J0nKn1ght did you ever nail down a solution to this?
@benm-eras Yes - I've come up with a working solution. Sorry - I've not got time to document it at the moment, but I'll try and put a post on here with the details next week.
So what's the wrong with the above sample?
The link you posted 5 months ago @hishamco? Saying you'd done it 2 years before that? That's links dead (404) and a lot has changed in the last 2.5 years
Sorry no one remind before it was in a master branch, here the active link https://github.com/aspnet/Entropy/tree/master/samples/Localization.EntityFramework
a lot has changed in the last 2.5 years
I knew but still work as expected, AFAIK there's no much changes in the underlying APIs expect adding some features such as Builder APIs, ResourceLocation attribute .. etc
@hishamco you are creating a singleton instance of your DbContext in your factory which I was under the impression is something that should be avoided, especially if your context is also responsible for reading/writing data in other areas of your application.
@J0nKn1ght the best solution I have found so far is to inject an IServiceScopeFactory into the IStringLocalizer implementation and use that to create a scope on each call for a resource that is used to get an instance of the DbContext:
public class StringResourceLocalizer : IStringLocalizer
{
private readonly IServiceScopeFactory scopes;
public StringResourceLocalizer(IServiceScopeFactory scopes) => this.scopes = scopes;
public LocalizedString this[stringkey]
{
get
{
using (IServiceScope scope = this.scopes.CreateScope())
{
DataContext context = scope.ServiceProvider.GetRequiredService<DataContext>();
return new LocalizedString(key, context.Resources.Single(r => r.key == key).Value);
}
}
}
}
But I don't think this is ideal because it's creating a new scope and context for every single resource request, which will obviously have a performance impact.
@benm-eras Looking back at our code, I realise that in the end I just abandoned using the built in localization support, and just created our own interface and implementation:
Interface:
public interface ILocaliserService
{
string this[string name] { get; }
string this[string name, string languageCode] { get; }
}
Implementation (sorry - UK spellings!):
public class LocaliserService : ILocaliserService
{
private readonly DatabaseContext _context;
public LocaliserService(DatabaseContext context)
{
_context = context;
}
public string this[string name]
{
get { return this[name, CultureInfo.CurrentUICulture.TwoLetterISOLanguageName]; }
}
public string this[string name, string languageCode]
{
get
{
var result = _context.Resources.Where(r => r.LanguageCode == languageCode && r.Key == name).FirstOrDefault();
if (result == null && languageCode != LanguageConstants.DefaultLanguage)
result = _context.Resources.Where(r => r.LanguageCode == LanguageConstants.DefaultLanguage && r.Key == name).FirstOrDefault();
return result?.Value;
}
}
}
Then in Startup.cs ConfigureServices, we just add the scoped service:
services.AddScoped<ILocaliserService, LocaliserService>();
In the views, we then just inject the service and use constants for the resource keys, for example:
@model List<WebPage>
@inject ILocaliserService localiser
@if (Model != null)
{
@foreach (var webPage in Model)
{
<li>
<div class="search-result-item">
<a href="@webPage.Url">@webPage.Name</a>
<p>@webPage.Snippet</p>
</div>
</li>
}
}
else
{
<li class="mt-3">@localiser[ResourceConstants.NoResultsKey]</li>
}
This suited our requirements, of having resource strings stored in the database, and also got around the problem that I had with caching, which I raised here: https://github.com/aspnet/Mvc/issues/7636
@J0nKn1ght UK spellings are correct spellings!
Are you then able to use your service with the in built data annotation attributes and validation?
@benm-eras Unfortunately, I don't think that the attributes will work any more, as we're not implementing IStringLocalizer.
Our requirements are outside of the normal intended usage of the resource localisation implementation in .Net Core, as the site we've developed is basically a CMS, so everything that we want to localise is data, rather than being the names of properties, and their validation messages.
Maybe @hishamco could update the example code to address your concerns about the singleton database context, and perhaps also add some actual MVC artefacts, such as Controllers, Views and Models that make use of resource-aware data annotations, to show how they all fit together?
@benm-eras that was a show can, please have a look to what we did in SimplCommerce which is similar little bit to what you mentioned using ServiceProvider.CreateScope(). @J0nKn1ght you we use culture codes similar to what you pointed above but with the same localization APIs infrastructure
@J0nKn1ght I thought that might be the case. Nevermind, thank you for your time anyway. It's a real shame that they don't seem to have designed the localization features to enable easy use of a database as the provider!
@hishamco That is actually pretty much identical to my current solution, the code I posted above being a simplified version to illustrate my point.
It's a real shame that they don't seem to have designed the localization features to enable easy use of a database as the provider!
The current localization APIs have a lot of extensibility points that doesn't let you stick with what its available now, for further information please have a look to https://github.com/aspnet/Docs/issues/8870
I knew there 're something may the community dislike such as the one that I personally hate in the localization APIs which is IStringLocalizerFactory because the definition that shown here is more specific for resx files
I also created an EF Core localization lib using the extension points.
https://github.com/damienbod/AspNetCoreLocalization/tree/master/src/Localization.SqlLocalizer
And the docs
https://localizationsqllocalizer.readthedocs.io/en/latest/
maybe this can help you
Greetings Damien
I forgot to mentioned your repo @damienbod, but I'm sure that I mentioned it before in another issue 馃槃
Edit: @benm-eras - Just posted this and only then realised that it is the exact same solution that you suggested in October. D'oh! It's been a long few months! Anyway, hopefully it will still help someone.
@benm-eras I realise that this is a bit of a late follow up, but I've just had to update our site to support localisation of dataannotations. It was difficult to get everything working together, because of the issue trying to call Scoped services from Singleton services, but after a lot of StackOverflow browsing, I've reached a solution that seems to work.
The key (for me) was the realisation that I needed to use IServiceScopeFactory in the IStringLocalizer and IStringLocalizerFactory, so that I could request an instance of my DB Context when I needed one.
So, here's my IStringLocalizerFactory implementation:
public class DataAnnotationLocaliserFactory : IStringLocalizerFactory
{
private readonly IServiceScopeFactory _scopeFactory;
public DataAnnotationLocaliserFactory(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public IStringLocalizer Create(Type resourceSource)
{
return new DataAnnotationLocaliser(_scopeFactory);
}
public IStringLocalizer Create(string baseName, string location)
{
return new DataAnnotationLocaliser(_scopeFactory);
}
}
Then in the IStringLocalizer implementation, IServiceScopeFactory scopeFactory is DI'd into the constructor in the same way as for the factory, which then allows a DB context instance to be accessed. I'm doing this in a private method in my IStringLocalizer implementation:
private string GetString(string name, string languageCode)
{
using (IServiceScope scope = _scopeFactory.CreateScope())
{
using (MyContext context = scope.ServiceProvider.GetRequiredService<MyContext>())
{
var result = context.Resources.Where(r => r.LanguageCode == languageCode && r.Key == name).FirstOrDefault();
if (result == null && languageCode != LanguageConstants.DefaultLanguage)
result = context.Resources.Where(r => r.LanguageCode == LanguageConstants.DefaultLanguage && r.Key == name).FirstOrDefault();
return result?.Value ?? name;
}
}
}
My Startup then has the following:
services.AddSingleton<IStringLocalizerFactory, DataAnnotationLocaliserFactory>();
...
services.AddMvc(options =>
{
//(filters and modelbinders go here)
})
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(DataAnnotationLocaliserFactory));
});
The DataAnnotations can then reference the keys that I have in my database Resources table (the ones prefixed with 'Contact_' here):
[Display(Name = "Contact_LinkDetails", Description = "Contact_LinkDetailsHint")]
[Required(ErrorMessage = "Contact_LinkDetailsRequired")]
[StringLength(4000)]
public string LinkDetails { get; set; }
I've also hooked up custom ValidationAttributes using IClientModelValidator, which work fine. (This example helped me with the localisation specifics.)
I hope that this helps others trying to go down the database localisation resource route.