I'm working on an asp.net core 5 web api project and I want to localize (translate) my DTO's properties when serializing and converting them to json. For example assume we have this class:
public class PersonDto
{
public string Name { get; set; }
public int Grade { get; set; }
}
So when I try to serialize this object JsonConvert.Serialize(personDto) I got a json like:
{
"name": "any name",
"grade": 90
}
But when a user sends Accept-Language: fr-FR header I want to return a response like:
{
"nom": "any name",
"classe": 90
}
(I don't know french I just used google translate to demonstrate my purpose.)
What I have done until now:
First I added localization to my project. Here is the configuration:
services.AddLocalization();
services.Configure<RequestLocalizationOptions>(
options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR")
};
options.DefaultRequestCulture = new RequestCulture(culture: "fr-FR", uiCulture: "fr-FR");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[] { new AcceptLanguageHeaderRequestCultureProvider() };
});
And for Configure part of startup:
// ..
app.UseRouting();
app.UseRequestLocalization();
// ..
And localization works in a TestContoller I just wrote:
public class Test : BaseController // BaseController is inherited from ControllerBase (it's OK)
{
private readonly IStringLocalizer<Resource> _localizer;
public Test(IStringLocalizer<Resource> localizer)
{
_localizer = localizer;
}
[HttpGet]
public string Get()
{
return _localizer.GetString("Hello");
}
}
So when we send Accept-Language: fr-FR header we get Bonjour as response and it's OK.
But I don't know how to translate class properties. I tried to write a CustomContractResolver and it worked but it has a bug. Here is the ContractResolver:
public class CustomContractResolver : DefaultContractResolver
{
private readonly HashSet<Type> _localizeds;
private readonly IStringLocalizer<Resource> _localizer;
public CustomContractResolver(IStringLocalizer<Resource> localizer)
{
_localizer = localizer;
_localizeds = new HashSet<Type>();
}
public void Localize(Type type)
{
if (!_localizeds.Contains(type))
_localizeds.Add(type);
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (IsLocalized(property.DeclaringType, property.PropertyName))
{
property.PropertyName = _localizer.GetString(property.PropertyName);
}
return property;
}
private bool IsLocalized(Type type, string jsonPropertyName)
{
if (!_localizeds.Contains(type))
return false;
return type.GetProperties().Any(p => p.Name.Equals(jsonPropertyName));
}
}
And I added this to JsonDefaultSettings by calling this app.ChangePropertyNameForLocalizableClasses(); in Configure method of startup:
public static class NewtonsoftJsonOptionsExtensions
{
public static void ChangePropertyNameForLocalizableClasses(this IApplicationBuilder app)
{
// CustomLocalizeAttribute is a custom attribute that I add to classes that I want to localize them when serializng
// We are using reflection to get all the classes that should be localized when serializing
var types = Assembly.GetExecutingAssembly().GetTypes()
.Where(c => c.HasAttribute<CustomLocalizeAttribute>() && !c.IsAbstract && c.IsPublic && c.IsClass);
// Our problem starts here: I have to inject localizer manually and DI isn't working
var localizer = app.ApplicationServices.GetRequiredService<IStringLocalizer<Resource>>();
var jsonResolver = new PropertyRenameAndIgnoreSerializerContractResolver(localizer);
foreach (var type in types)
{
jsonResolver.Localize(type);
}
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings {ContractResolver = jsonResolver};
return settings;
};
}
}
So my problem is when the program starts, if the first request have Accept-Language: fr-FR header then all classes that have CustomLocalizeAttribute will be localized when serializing and they ignore what Accept-Language header actually is.
How can I get a new instance of _localizer (IStringLocalizer) with every request?
Maybe I'm going all the way wrong! Is this a common task to do? Is there any other solutions?
Dictionary) instead of fixed class should do.DefaultContractResolvercaches type information. As that is causing problems for you, you could manually serialize to JSON using a new contract resolver each time, and return the JSON literal as shown in e.g. this answer to Return a JSON string explicitly from Asp.net WEBAPI?.