Resource Driven Messages in ASP.NET Core

Ever wanted to allow native country error messages from your ASP.NET Core api?

Published on 03 December 2018

I was asked at work the other day if I knew how to get localised error messages in ASP.NET Core. I was sure it was possible, but then they said that it wasn't a web front end but the values and error messages from a web api I was intregued. It was an interesting idea and I remember there wasn't a good localisation story in earlier version of ASP.NET Core however with the previews of 2.2 coming out I thought I would investigate and see if I could use it from an api perspective.

What is Localisation?

Localisation is the way in which you alter your website, application or api to be available and open for multiple areas of the world. You want to make your application open to a wider audience and to do that one of the items you can work with is language. This is sometimes done through a language dropdown on a website where you can pick your language. The other option is using the Accept-Language http header which is usually set by your browser from your operating systems preferences.

Can Accept-Language Header be used in an api?

There are a number of Request Culture Providers which are loaded into the pipeline automatically. The Request Culture Providers look at various different settings and then set the Culture and UICulture values.

  1. QueryStringRequestCultureProvider
  2. CookieRequestCultureProvider
  3. AcceptLanguageHeaderRequestCultureProvider

For Api developers using the querystring does not allow for the the business requirement of the data, it would also just get in the way. The cookie option is a big no no when it comes to api design. Apis should not be reliant on cookies, as it's a web technology, they could be called from web applications, mobile devices or desktop applications not all which understand or should deal with cookies.

This leaves the http header Accept-Language and the AcceptLanguageHeaderRequestCultureProvider.

How can set this up?

First we need to add in the resx files which include the different locale versions of the data which we want to use. For my example they are added under the Resources folder. The name they are given is the name of the resource class which drives the localisation and the culture which is being defined. For example SharedResources.en.resx for an English version.

I then wanted to wrap the IStringLocalizer<T> implementation. The type of T specifies the name of the resx files. To do this create a class called SharedResource which takes the IStringLocalizer<SharedResource> implementation in it's constructor.

public class SharedResource : ISharedResource
{
    private readonly IStringLocalizer<SharedResource> _localizer;

    public SharedResource(IStringLocalizer<SharedResource> localizer)   
    {
        _localizer = localizer;
    }

    public string MessageOne => _localizer["MessageOne"];
}

I then put this behind an interface so that it could be requested by controllers and services through the ASP.NET Core DI mechanism.

public interface ISharedResource
{
    string MessageOne { get; }
}

Now we've got a service we can inject we need to tell the framework how to hook it up. First the localisation middleware needs to be registered and the resources path, relative to the root of the project, needs to be specified.

public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization(config => { config.ResourcesPath = "Resources"; });
    services.AddTransient<ISharedResource, SharedResource>();

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

Once this is done the available culture info needs to be set on the IApplicationBuilder in the Configure method.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // snip for brevity

    var supportedCultures = new[]
    {
        new CultureInfo("en"),
        new CultureInfo("es"),
        new CultureInfo("fr"),
    };

    app.UseRequestLocalization(options =>
    {
        options.DefaultRequestCulture = new RequestCulture("en-GB");
        options.SupportedCultures = supportedCultures;
        options.SupportedUICultures = supportedCultures;
    });

    app.UseHttpsRedirection();
    app.UseMvc();
}

Above we are registering that we have English, Spanish and French resources available. We are also specifying the default culture.

How can we use it?

First we need to setup a controller which will use the ISharedResource implementation.

[Route("api/[controller]")]
[ApiController]
public class MessagesController : ControllerBase
{
    private readonly ISharedResource _sharedResource;

    public MessagesController(ISharedResource sharedResource)
    {
        _sharedResource = sharedResource;
    }

    [HttpGet]
    [ProducesResponseType(200)]
    public ActionResult<IEnumerable<string>> Get()
    {
        return Ok(_sharedResource.MessageOne);
    }
}

Nothing clever about this controller. It simply takes in the dependency and uses it. But calling it is where the clever happens.

Adding in the Accept-Language header to a request with a valid value eg. es-MX will look up the Spanish reource value.

Further reading

Conclusion

In this post we have briefly reviewed what localisation is, what it can be used for and how you can make your api messages return different configured messages.

Any questions/comments then please contact me on Twitter @WestDiscGolf