9

Leveraging Globalization and Localization

When developing an application, it is important to think about multi-language support; a multilingual application allows for a wider audience reach. This is also true for web APIs: messages returned by endpoints (for example, validation errors) should be localized, and the service should be able to handle different cultures and deal with time zones. In this chapter of the book, we will talk about globalization and localization, and we will explain what features are available in minimal APIs to work with these concepts. The information and samples that will be provided will guide us when adding multi-language support to our services and correctly handling all the related behaviors so that we will be able to develop global applications.

In this chapter, we will be covering the following topics:

  • Introducing globalization and localization
  • Localizing a minimal API application
  • Using resource files
  • Integrating localization in validation frameworks
  • Adding UTC support to a globalized minimal API

Technical requirements

To follow the descriptions in this chapter, you will need to create an ASP.NET Core 6.0 Web API application. Refer to the Technical requirements section in Chapter 1, Introduction to Minimal APIs, for instructions on how to do so.

If you’re using your console, shell, or Bash terminal to create the API, remember to change your working directory to the current chapter number (Chapter09).

All the code samples in this chapter can be found in the GitHub repository for this book at https://github.com/PacktPublishing/Minimal-APIs-in-ASP.NET-Core-6/tree/main/Chapter09.

Introducing globalization and localization

When thinking about internationalization, we must deal with globalization and localization, two terms that seem to refer to the same concepts but actually involve different areas. Globalization is the task of designing applications that can manage and support different cultures. Localization is the process of adapting an application to a particular culture, for example, by providing translated resources for each culture that will be supported.

Note

The terms internationalization, globalization, and localization are often abbreviated to I18N, G11N, and L10N, respectively.

As with all the other features that we have already introduced in the previous chapters, globalization and localization can be handled by the corresponding middleware and services that ASP.NET Core provides and work in the same way in minimal APIs and controller-based projects.

You can find a great introduction to globalization and localization in the official documentation available at https://docs.microsoft.com/dotnet/core/extensions/globalization and https://docs.microsoft.com/dotnet/core/extensions/localization, respectively. In the rest of the chapter, we will focus on how to add support for these features in a minimal API project; in this way, we’ll introduce some important concepts and explain how to leverage globalization and localization in ASP.NET Core.

Localizing a minimal API application

To enable localization within a minimal API application, let us go through the following steps:

  1. The first step to making an application localizable is to specify the supported cultures by setting the corresponding options, as follows:

    var builder = WebApplication.CreateBuilder(args);

    //...

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

    builder.Services.Configure<RequestLocalizationOptions>(options =>

    {

       options.SupportedCultures = supportedCultures;

       options.SupportedUICultures = supportedCultures;

       options.DefaultRequestCulture = new

       RequestCulture(supportedCultures.First());

    });

In our example, we want to support three cultures – English, Italian, and French – so, we create an array of CultureInfo objects.

We’re defining neutral cultures, that is, cultures that have a language but are not associated with a country or region. We could also use specific cultures, such as en-US or en-GB, to represent the cultures of a particular region: for example, en-US would refer to the English culture prevalent in the United States, while en-GB would refer to the English culture prevalent in the United Kingdom. This difference is important because, depending on the scenario, we may need to use country-specific information to correctly implement localization. For example, if we want to show a date, we have to know that the date format in the United States is M/d/yyyy, while in the United Kingdom, it is dd/MM/yyyy. So, in this case, it becomes fundamental to work with specific cultures. We also use specific cultures if we need to support language differences across cultures. For example, a particular word may have different spellings depending on the country (e.g., color in the US versus colour in the UK). That said, for our scenario of minimal APIs, working with neutral cultures is just fine.

  1. Next, we configure RequestLocalizationOptions, setting the cultures and specifying the default one to use if no information about the culture is provided. We specify both the supported cultures and the supported UI cultures:
    • The supported cultures control the output of culture-dependent functions, such as date, time, and number format.
    • The supported UI cultures are used to choose which translated strings (from .resx files) are searched for. We will talk about .resx files later in this chapter.

In a typical application, cultures and UI cultures are set to the same values, but of course, we can use different options if needed.

  1. Now that we have configured our service to support globalization, we need to add the localization middleware to the ASP.NET Core pipeline so it will be able to automatically set the culture of the request. Let us do so using the following code:

    var app = builder.Build();

    //...

    app.UseRequestLocalization();

    //...

    app.Run();

In the preceding code, with UseRequestLocalization(), we’re adding RequestLocalizationMiddleware to the ASP.NET Core pipeline to set the current culture of each request. This task is performed using a list of RequestCultureProvider that can read information about the culture from various sources. Default providers comprise the following:  

  • QueryStringRequestCultureProvider: Searches for the culture and ui-culture query string parameters
  • CookieRequestCultureProvider: Uses the ASP.NET Core cookie
  • AcceptLanguageHeaderRequestProvider: Reads the requested culture from the Accept-Language HTTP header

For each request, the system will try to use these providers in this exact order, until it finds the first one that can determine the culture. If the culture cannot be set, the one specified in the DefaultRequestCulture property of RequestLocalizationOptions will be used.

If necessary, it is also possible to change the order of the request culture providers or even define a custom provider to implement our own logic to determine the culture. More information on this topic is available at https://docs.microsoft.com/aspnet/core/fundamentals/localization#use-a-custom-provider.

Important note

The localization middleware must be inserted before any other middleware that might use the request culture.

In the case of web APIs, whether using controller-based or minimal APIs, we usually set the request culture through the Accept-Language HTTP header. In the following section, we will see how to extend Swagger with the ability to add this header when trying to invoke methods.

Adding globalization support to Swagger

We want Swagger to provide us with a way to specify the Accept-Language HTTP header for each request so that we can test our globalized endpoints. Technically speaking, this means adding an operation filter to Swagger that will be able to automatically insert the language header, using the following code:

public class AcceptLanguageHeaderOperationFilter : IOperationFilter
{
     private readonly List<IOpenApiAny>? 
     supportedLanguages;
     public AcceptLanguageHeaderOperationFilter 
     (IOptions<RequestLocalizationOptions> 
     requestLocalizationOptions)
     {
           supportedLanguages = 
           requestLocalizationOptions.Value.
           SupportedCultures?.Select(c => 
           newOpenApiString(c.TwoLetterISOLanguageName)).
           Cast<IOpenApiAny>().           ToList();
     }
     public void Apply(OpenApiOperation operation, 
     OperationFilterContext context)
     {
           if (supportedLanguages?.Any() ?? false)
           {
                 operation.Parameters ??= new 
                 List<OpenApiParameter>();
                 operation.Parameters.Add(new 
                 OpenApiParameter
                 {
                       Name = HeaderNames.AcceptLanguage,
                       In = ParameterLocation.Header,
                       Required = false,
                       Schema = new OpenApiSchema
                       {
                             Type = "string",
                             Enum = supportedLanguages,
                             Default = supportedLanguages.
                             First()
                       }
                 });
           }
     }
}

In the preceding code, AcceptLanguageHeaderOperationFilter takes the RequestLocalizationOptions object via dependency injection that we have defined at startup and extracts the supported languages in the format that Swagger expects from it. Then, in the Apply() method, we add a new OpenApiParameter that corresponds to the Accept-Language header. In particular, with the Schema.Enum property, we provide the list of supported languages using the values we have extracted in the constructor. This method is invoked for every operation (that is, every endpoint), meaning that the parameter will be automatically added to each of them.

Now, we need to add the new filter to Swagger:

var builder = WebApplication.CreateBuilder(args);
//...
builder.Services.AddSwaggerGen(options =>
{
     options.OperationFilter<AcceptLanguageHeaderOperation
     Filter>();
});

As we did with the preceding code, for every operation, Swagger will execute the filter, which in turn will add a parameter to specify the language of the request.

So, let’s suppose we have the following endpoint:

app.MapGet("/culture", () => Thread.CurrentThread.CurrentCulture.DisplayName);

In the preceding handler, we just return the culture of the thread. This method takes no parameter; however, after adding the preceding filter, the Swagger UI will show the following:

Figure 9.1 – The Accept-Language header added to Swagger

Figure 9.1 – The Accept-Language header added to Swagger

The operation filter has added a new parameter to the endpoint, allowing us to select the language from a dropdown. We can click the Try it out button to choose a value from the list and then click Execute to invoke the endpoint:

Figure 9.2 – The result of the execution with the Accept-Language HTTP header

Figure 9.2 – The result of the execution with the Accept-Language HTTP header

This is the result of selecting it as a language request: Swagger has added the Accept-Language HTTP header, which, in turn, has been used by ASP.NET Core to set the current culture. Then, in the end, we get and return the culture display name in the route handler.

This example shows us that we have correctly added globalization support to our minimal API. In the next section, we’ll go further and work with localization, starting by providing translated resources to callers based on the corresponding languages.

Using resource files

Our minimal API now supports globalization, so it can switch cultures based on the request. This means that we can provide localized messages to callers, for example, when communicating validation errors. This feature is based on the so-called resource files (.resx), a particular kind of XML file that contains key-value string pairs representing messages that must be localized.

Note

These resource files are exactly the same as they have been since the early versions of .NET.

Creating and working with resource files

With resource files, we can easily separate strings from code and group them by culture. Typically, resource files are put in a folder called Resources. To create a file of this kind using Visual Studio, let us go through the following steps:

Important note

Unfortunately, Visual Studio Code does not provide support for handling .resx files. More information about this topic is available at https://github.com/dotnet/AspNetCore.Docs/issues/2501.

  1. Right-click on the folder in Solution Explorer and then choose Add | New Item.
  2. In the Add New Item dialog window, search for Resources, select the corresponding template, and name the file, for example, Messages.resx:
Figure 9.3 – Adding a resource file to the project

Figure 9.3 – Adding a resource file to the project

The new file will immediately open in the Visual Studio editor.

  1. The first thing to do in the new file is to select Internal or Public (based on the code visibility we want to achieve) from the Access Modifier option so that Visual Studio will create a C# file that exposes the properties to access the resources:
Figure 9.4 – Changing the Access Modifier of the resource file

Figure 9.4 – Changing the Access Modifier of the resource file

As soon as we change this value, Visual Studio will add a Messages.Designer.cs file to the project and automatically create properties that correspond to the strings we insert in the resource file.

Resource files must follow a precise naming convention. The file that contains default culture messages can have any name (such as Messages.resx, as in our example), but the other .resx files that provide the corresponding translations must have the same name, with the specification of the culture (neutral or specific) to which they refer. So, we have Messages.resx, which will store default (English) messages.

  1. Since we also want to localize our messages in Italian, we need to create another file with the name Messages.it.resx.

Note

We don’t create a resource file for French culture on purpose because this way, we’ll see how APS.NET Core looks up the localized messages in practice.

  1. Now, we can start experimenting with resource files. Let’s open the Messages.resx file and set Name to HelloWorld and Value to Hello World!.

In this way, Visual Studio will add a static HelloWorld property in the Messages autogenerated class that allows us to access values based on the current culture.

  1. To demonstrate this behavior, also open the Messages.it.resx file and add an item with the same Name, HelloWorld, but now set Value to the translation Ciao mondo!.
  2. Finally, we can add a new endpoint to showcase the usage of the resource files:

    // using Chapter09.Resources;

    app.MapGet("/helloworld", () => Messages.HelloWorld);

In the preceding route handler, we simply access the static Mesasges.HelloWorld property that, as discussed before, has been automatically created while editing the Messages.resx file.

If we now run the minimal API and try to execute this endpoint, we’ll get the following responses based on the request language that we select in Swagger:

Table 9.1 – Responses based on the request language

Table 9.1 – Responses based on the request language

When accessing a property such as HelloWorld, the autogenerated Messages class internally uses ResourceManager to look up the corresponding localized string. First of all, it looks for a resource file whose name contains the requested culture. If it is not found, it reverts to the parent culture of that culture. This means that, if the requested culture is specific, ResourceManager searches for the neutral culture. If no resource file is still found, then the default one is used.

In our case, using Swagger, we can select only English, Italian, or French as a neutral culture. But what happens if a client sends other values? We can have situations such as the following:

  • The request culture is it-IT: the system searches for Messages.it-IT.resx and then finds and uses Messages.it.resx.
  • The request culture is fr-FR: the system searches for Messages.fr-FR.resx, then Messages.fr.resx, and (because neither are available) finally uses the default, Messages.resx.
  • The request culture is de (German): because this isn’t a supported culture at all, the default request culture will be automatically selected, so strings will be searched for in the Messages.resx file.

Note

If a localized resource file exists, but it doesn’t contain the specified key, then the value of the default file will be used.

Formatting localized messages using resource files

We can also use resource files to format localized messages. For example, we can add the following strings to the resource files of the project:

Table 9.2 – A custom localized message

Table 9.2 – A custom localized message

Now, let’s define this endpoint:

// using Chapter09.Resources;
app.MapGet("/hello", (string name) =>
{
     var message = string.Format(Messages.GreetingMessage, 
     name);
     return message;
});

As in the preceding code example, we get a string from a resource file according to the culture of the request. But, in this case, the message contains a placeholder, so we can use it to create a custom localized message using the name that is passed to the route handler. If we try to execute the endpoint, we will get results such as these:

Table 9.3 – Responses with custom localized messages based on the request language

Table 9.3 – Responses with custom localized messages based on the request language

The possibility to create localized messages with placeholders that are replaced at runtime using different values is a key point for creating truly localizable services.

In the beginning, we said that a typical use case of localization in web APIs is when we need to provide localized error messages upon validation. In the next section, we’ll see how to add this feature to our minimal API.

Integrating localization in validation frameworks

In Chapter 6, Exploring Validation and Mapping, we talked about how to integrate validation into a minimal API project. We learned how to use the MiniValidation library, rather than FluentValidation, to validate our models and provide validation messages to the callers. We also said that FluentValidation already provides translations for standard error messages.

However, with both libraries, we can leverage the localization support we have just added to our project to support localized and custom validation messages.

Localizing validation messages with MiniValidation

Using the MiniValidation library, we can use validation based on Data Annotations with minimal APIs. Refer to Chapter 6, Exploring Validation and Mapping, for instructions on how to add this library to the project.

Then, recreate the same Person class:

public class Person
{
     [Required]
     [MaxLength(30)]
     public string FirstName { get; set; }
     [Required]
     [MaxLength(30)]
     public string LastName { get; set; }
     [EmailAddress]
     [StringLength(100, MinimumLength = 6)]
     public string Email { get; set; }
}

Every validation attribute allows us to specify an error message, which can be a static string or a reference to a resource file. Let’s see how to correctly handle the localization for the Required attribute. Add the following values in resource files:

Table 9.4 – Localized validation error messages used by Data Annotations

Table 9.4 – Localized validation error messages used by Data Annotations

We want it so that when a required validation rule fails, the localized message that corresponds to FieldRequiredAnnotation is returned. Moreover, this message contains a placeholder, because we want to use it for every required field, so we also need the translation of property names.

With these resources, we can update the Person class with the following declarations:

public class Person
{
     [Display(Name = "FirstName", ResourceType = 
      typeof(Messages))]
     [Required(ErrorMessageResourceName = 
     "FieldRequiredAnnotation",
      ErrorMessageResourceType = typeof(Messages))]
     public string FirstName { get; set; }
     //...
}

Each validation attribute, such as Required (as used in this example), exposes properties that allow us to specify the name of the resource to use and the type of class that contains the corresponding definition. Keep in mind that the name is a simple string, with no check at compile time, so if we write an incorrect value, we’ll only get an error at runtime.

Next, we can use the Display attribute to also specify the name of the field that must be inserted in the validation message.

Note

You can find the complete declaration of the Person class with localized data annotations on the GitHub repository at https://github.com/PacktPublishing/Minimal-APIs-in-ASP.NET-Core-6/blob/main/Chapter09/Program.cs#L97.

Now we can re-add the validation code shown in Chapter 6, Exploring Validation and Mapping. The difference is that now the validation messages will be localized:

app.MapPost("/people", (Person person) =>
{
     var isValid = MiniValidator.TryValidate(person, out 
     var errors);
     if (!isValid)
     {
           return Results.ValidationProblem(errors, title: 
           Messages.ValidationErrors);
     }
     return Results.NoContent();
});

In the preceding code, the messages contained in the errors dictionary that is returned by the MiniValidator.TryValidate() method will be localized according to the request culture, as described in the previous sections. We also specify the title parameter in the Results.ValidationProblem() invocation because we want to localize this value too (otherwise, it will always be the default One or more validation errors occurred).

If instead of data annotations, we prefer using FluentValidation, we know that it supports localization of standard error messages by default from Chapter 6, Exploring Validation and Mapping. However, with this library, we can also provide our translations. In the next section, we’ll talk about implementing this solution.

Localizing validation messages with FluentValidation

With FluentValidation, we can totally decouple the validation rules from our models. As said before, refer to Chapter 6, Exploring Validation and Mapping, for instructions on how to add this library to the project and how to configure it.

Next, let us recreate the PersonValidator class:

public class PersonValidator : AbstractValidator<Person>
{
     public PersonValidator()
     {
           RuleFor(p => p.FirstName).NotEmpty().
           MaximumLength(30);
           RuleFor(p => p.LastName).NotEmpty().
           MaximumLength(30);
           RuleFor(p => p.Email).EmailAddress().Length(6, 
           100);
     }
}

In the case that we haven’t specified any messages, the default ones will be used. Let’s add the following resource to customize the NotEmpty validation rule:

Table 9.5 – The localized validation error messages used by FluentValidation

Table 9.5 – The localized validation error messages used by FluentValidation

Note that, in this case, we also have a placeholder that will be replaced by the property name. However, different from data annotations, FluentValidation uses a placeholder with a name to better identify its meaning.

Now, we can add this message in the validator, for example, for the FirstName property:

RuleFor(p => p.FirstName).NotEmpty().
     WithMessage(Messages.NotEmptyMessage).
     WithName(Messages.FirstName);

We use WithMessage() to specify the message that must be used when the preceding rule fails, following which we add the WithName() invocation to overwrite the default property name used for the {PropertyName} placeholder of the message.

Note

You can find the complete implementation of the PersonValidator class with localized messages in the GitHub repository at https://github.com/PacktPublishing/Minimal-APIs-in-ASP.NET-Core-6/blob/main/Chapter09/Program.cs#L129.

Finally, we can leverage the localized validator in our endpoint, as we did in Chapter 6, Exploring Validation and Mapping:

app.MapPost("/people", async (Person person, IValidator<Person> validator) =>
{
     var validationResult = await validator.
     ValidateAsync(person);
     if (!validationResult.IsValid)
     {
           var errors = validationResult.ToDictionary();
           return Results.ValidationProblem(errors, title: 
           Messages.ValidationErrors);
     }
     return Results.NoContent();
});

As in the case of data annotations, the validationResult variable will contain localized error messages that we return to the caller using the Results.ValidationProblem() method (again, with the definition of the title property).

Tip

In our example, we have seen how to explicitly assign translations for each property using the WithMessage() method. FluentValidation also provides a way to replace all (or some) of its default messages. You can find more information in the official documentation at https://docs.fluentvalidation.net/en/latest/localization.html#default-messages.

This ends our overview of localization using resource files. Next, we’ll talk about an important topic when dealing with services that are meant to be used worldwide: the correct handling of different time zones.

Adding UTC support to a globalized minimal API

So far, we have added globalization and localization support to our minimal API because we want it to be used by the widest audience possible, irrespective of culture. But, if we think about being accessible to a worldwide audience, we should consider several aspects related to globalization. Globalization does not only pertain to language support; there are important factors we need to consider, for example, geographic locations, as well as time zones.

So, for example, we can have our minimal API running in Italy, which follows Central European Time (CET) (GMT+1), while our clients can use browsers that execute a single-page application, rather than mobile apps, all over the world. We could also have a database server that contains our data, and this could be in another time zone. Moreover, at a certain point, it may be necessary to provide better support for worldwide users, so we’ll have to move our service to another location, which could have a new time zone. In conclusion, our system could deal with data in different time zones, and, potentially, the same services could switch time zones during their lives.

In these situations, the ideal solution is working with DateTimeOffset, a data type that includes time zones and that JsonSerializer fully supports, preserving time zone information during serialization and deserialization. If we could always use it, we’d automatically solve any problem related to globalization, because converting a DateTimeOffset value to a different time zone is straightforward. However, there are cases in which we can’t handle the DateTimeOffset type, for example:

  • When we’re working on a legacy system that relies on DateTime everywhere, updating the code to use DateTimeOffset isn’t an option because it requires too many changes and breaks the compatibility with the old data.
  • We have a database server such as MySQL that doesn’t have a column type for storing DateTimeOffset directly, so handling it requires extra effort, for example, using two separate columns, increasing the complexity of the domain.
  • In some cases, we simply aren’t interested in sending, receiving, and saving time zones – we just want to handle time in a “universal” way.

So, in all the scenarios where we can’t or don’t want to use the DateTimeOffset data type, one of the best and simplest ways to deal with different time zones is to handle all dates using Coordinated Universal Time (UTC): the service must assume that the dates it receives are in the UTC format and, on the other hand, all the dates returned by the API must be in UTC.

Of course, we must handle this behavior in a centralized way; we don’t want to have to remember to apply the conversion to and from the UTC format every time we receive or send a date. The well-known JSON.NET library provides an option to specify how to treat the time value when working with a DateTime property, allowing it to automatically handle all dates as UTC and convert them to that format if they represent a local time. However, the current version of Microsoft JsonSerializer used in minimal APIs doesn’t include such a feature. From Chapter 2, Exploring Minimal APIs and Their Advantages, we know that we cannot change the default JSON serializer in minimal APIs, but we can overcome this lack of UTC support by creating a simple JsonConverter:

public class UtcDateTimeConverter : JsonConverter<DateTime>
{
     public override DateTime Read(ref Utf8JsonReader 
     reader, Type typeToConvert, JsonSerializerOptions  
     options)
     => reader.GetDateTime().ToUniversalTime();
     public override void Write(Utf8JsonWriter writer, 
     DateTime value, JsonSerializerOptions options)
     => writer.WriteStringValue((value.Kind == 
     DateTimeKind.Local ? value.ToUniversalTime() : value)
     .ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'
     fffffff'Z'"));
}

With this converter, we tell JsonSerializer how to treat DateTime properties:

  • When DateTime is read from JSON, the value is converted to UTC using the ToUniversalTime() method.
  • When DateTime must be written to JSON, if it represents a local time (DateTimeKind.Local), it is converted to UTC before serialization – then, it is serialized using the Z suffix, which indicates that the time is UTC.

Now, before using this converter, let’s add the following endpoint definition:

app.MapPost("/date", (DateInput date) =>
{
     return Results.Ok(new
     {
           Input = date.Value,
           DateKind = date.Value.Kind.ToString(),
           ServerDate = DateTime.Now
     });
});
public record DateInput(DateTime Value);

Let’s try to call it, for example, with a date formatted as 2022-03-06T16:42:37-05:00. We’ll obtain something similar to the following:

{
  "input": "2022-03-06T22:42:37+01:00",
  "dateKind": "Local",
  "serverDate": "2022-03-07T18:33:17.0288535+01:00"
}

The input date, containing a time zone, has automatically been converted to the local time of the server (in this case, the server is running in Italy, as stated at the beginning), as also demonstrated by the dateKind field. Moreover, serverDate contains a date that is relative to the server time zone.

Now, let’s add UtcDateTimeConverter to JsonSerializer:

var builder = WebApplication.CreateBuilder(args);
//...
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.
JsonOptions>(options =>
{
     options.SerializerOptions.Converters.Add(new 
     UtcDateTimeConverter());
});

With this configuration, every DateTime property will be processed using our custom converters. Now, execute the endpoint again, using the same input as before. This time, the result will be as follows:

{
  "input": "2022-03-06T21:42:37.0000000Z",
  "dateKind": "Utc",
  "serverDate": "2022-03-06T17:40:08.1472051Z"
}

The input is the same, but our UtcDateTimeConverter has now converted the date to UTC and, on the other hand, has serialized the server date as UTC; now, our API, in a centralized way, can automatically handle all dates as UTC, no matter its time zone or the time zones of the callers.

Finally, there are two other points to make all the systems correctly work with UTC:

  • When we need to retrieve the current date in the code, we always have to use DateTime.UtcNow instead of DateTime.Now
  • Client applications must know that they will receive the date in UTC format and act accordingly, for example, invoking the ToLocalTime() method

In this way, the minimal API is truly globalized and can work with any time zone; without having to worry about explicit conversion, all times input or output will be always in UTC, so it will be much easier to handle them.

Summary

Developing minimal APIs with globalization and localization support in mind is fundamental in an interconnected world. ASP.NET Core includes all the features needed to create services that can react to the culture of the user and provide translations based on the request language: the usage of localization middleware, resource files, and custom validation messages allows the creation of services that can support virtually every culture. We have also talked about the globalization-related problems that could arise when working with different time zones and shown how to solve it using the centralized UTC date time format so that our APIs can seamlessly work irrespective of the geographic location and time zone of clients.

In Chapter 10, Evaluating and Benchmarking the Performance of Minimal APIs, we will talk about why minimal APIs were created and analyze the performance benefits of using minimal APIs over the classic controller-based approach.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset