>

Implementing an Owin Globalization Module with Katana

Introduction.

Owin  became popular in the Mvc arena with the Mvc5 release whose authentication and authorization components are implemented as Owin application delegates.

An Owin based application is implemented as a pipe composed of  transformation modules, the middleware, and application modules. Roughly, the middleware components are equivalent to Asp.net modules, while application components are equivalent to Asp.net Handlers. Both application components, and middleware are implemented as application delegates:

Func<IDictionary<string, object>, Task>

That is, as functions that take  a dictionary containing information extracted from the client request and provided by other application delegates as input and that return a task.

The use of tasks avoids that the block of an application delegate waiting for a long-running operation might block the whole Owin pipeline thread.

While Owin is an abstract specification Katana is a concrete implementation. Katana architecture is based on 4 logical layers: the middleware and the application layers that I already mentioned, the host, and the web server.

The host is responsible for creating and orchestrating all underlying processes, and for creating and connecting the pipeline composed by the middleware and the application, while the web server is responsible for opening a network socket, listening for requests, and sending them through the Owin pipeline after having filled the initial IDictionary<string, object>.

We may  either use  IIS/Asp.net to implement the host/Web Server pair or we may  launch a custom host process that calls a web server implementation.

Thus, one of the advantages of implementing our Globalization module as Owin middleware instead of an Asp.net module is that we may use it also with custom-hosted applications(typically WebApi and SignalR custom-hosted applications). Moreover Katana updates are released as Nuget packages that are independent of the Asp.net stack, so we may benefit of more frequent updates and improvements.

Finally, the implicit use of Tasks in the Owin pipeline furnishes an easy way to improve performance, without any supplementary programming effort.

Everything is needed to define and use Katana Middleware is contained in the dlls:

  • Owin.dll, that contains the abstract definition of Owin
  • Microsoft.Owin, that contains Katana core
  • Microsoft.Owin.Host.SystemWeb that contains a Katana server able to interact with Asp.net pipeline.

 

You may get everything by installing the Microsoft.Owin.Host.SystemWeb  Nuget package in your Asp.net project.

If you implement the the middleware in a separate dll you may simply install the  Microsoft.Owin Nuget package, since you don’t need the Microsoft.Owin.Host.SystemWeb dll.

The Problem

Typically web site supports just a limited set of cultures since each different language requires specific resources that must be produced manually, such as .resx files and/or whole Views/Pages specific for that language. However, the support of a specific culture based on a supported language typically doesn’t require any specific effort since .Net already contains the numeric formats and date formats for all cultures and one may use a subset of each language that is common to all cultures based on that language.

Thus, for instance, we may use an unique .resx file for the English language while supporting most of the English based culture(en-US, en-CA, en-GB, en-AU,etc.) if we use a subset of the English language that is common to all cultures. Accordingly, we may set  the UICulture to en and the Culture to a whole language-country pair.

Any attempt to set a specific culture, must set only of the actually supported cultures, according to a best match algorithm,and when no culture is explicitly set by the user we should extract a culture from the Browser preferences.

Summing up, our specifications are:

  • The developer may specify a default culture and a list of supported cultures(all cultures that are supported simultaneously by UI widgets like date pickers, and by the the .resx files included in the project).
  • The selected culture is assigned to the current thread, and stored in a cookie and/or in the Url(http://mywebsite.com/en-US/...).
  • A culture may be selected either by creating a globalization cookie and adding it to the response, if culture storing in cookies is enabled, or by issuing to the server a request that contains the chosen culture  as the first segment of the Url path(http://mywebsite.com/en-US/...), if culture storing in the Url is enabled.
  • If no culture is selected the culture is extracted from the browser list of preferred cultures with a best match algorithm, and then it is set as the currently selected culture.
  • If the culture specified in the path is different from the culture specified in the cookie, the module assumes that the user just clicked a link to change the culture, and uses the culture specified in the path as selected culture.
  • If there is no perfect match between either a selected culture or the browser cultures the module look for a partial match of just the language part (for instance matching en-CA with en-US, or en). In case of ambiguity the first matching supported language is chosen. If no partial match is found, the module reverts to the default culture. In any case the culture computed this way is set as the currently selected culture
  • When adding a culture to the list supported cultures one may specify to use only the language part asUICulture (the one used to select the appropriate .resx file). This way, for instance, we may specify both en-US, and en-CA, so that dates, numbers and currencies are displayed according to the country, while using an unique en .resx file for the English language.

If possible it is advised to select both cookie and and Url path culture storing (that should be the default), because the cookie is useful to remember the selected language in future visits, while including the culture in the path improves SEO and furnishes an easy way to change the selected culture (it is enough to add links with all supported cultures in their path to the page).

Creating Owin Middleware with Katana

In Katana the whole Owin function delegates pipeline is specified  within the static Configure method of a Startup class that is automatically called by the Katana framework. As a default Katana looks for a class named Startup in namespace with the same name of the main assembly. There are several ways to change this default.

Thus, we have something like:

Middleware pipeline
  1. public partial class Startup
  2.     {
  3.         public void Configuration(IAppBuilder app)
  4.         {
  5.             app.Use<MyModule>(options);
  6.             ...
  7.             ...
  8.             ...
  9.             app.Use<MyModuleN>(optionsN);
  10.  
  11.         }
  12.     }

 

The IAppBuilder interface contains the IDictionary used by all function delegates, some dictionary entries exposed as strongly typed properties, and utility methods like the Use<T> method that inserts middleware function delegates in the pipeline. Each middleware function delegate is implemented by the class passed in the generic T of the Use<T>(…) method and it must inherit from the OwinMiddleware class.

We need  to implement just the constructor and the Invoke method:

Middleware module
  1. public class MyMiddleware : OwinMiddleware
  2. {
  3.     public LoggerMiddleware(OwinMiddleware next,
  4.         MyOptionType myOptions)
  5.         : base(next)
  6.     {
  7.         _...
  8.          ...
  9.     }
  10.  
  11.     public override async Task Invoke(IOwinContext context)
  12.     {
  13.         ...
  14.         ...
  15.         ...
  16.         await this.Next.Invoke(context);
  17.         ...
  18.         ...
  19.         ...
  20.  
  21.     }
  22. }

 

The constructor is passed the next element in the Owin pipeline, and the same option object we pass to the Use<T>(option) IAppBuilder method.

In the invoke method we first do the job of our module and then we call the Invoke method of the next element in the Owin pipeline.

In order to simplify the Owin pipeline we may group one or several calls to IAppBuilder.Use<T>(object o) into an IAppBulder extension method:

IAppBuilder extension
  1. namespace MVCControlsToolkit.Owin.Globalization
  2. {
  3.     public static class OwinGlobalizationHelpers
  4.     {
  5.         
  6.           public static void UseGlobalization(thisIAppBuilder app,
  7.             OwinGlobalizationOptions options)
  8.         {
  9.             app.Use<GlobalizationMidlleware>(options);
  10.         }
  11.     }
  12. }

 

After that, in the pipeline definition we may simply call the newly defined extension method instead of IAppBuilder.Use<T>(object o):

 

Code Snippet
  1. public partial class Startup
  2. {
  3.     public void Configuration(IAppBuilder app)
  4.     {
  5.         app.UseGlobalization(new OwinGlobalizationOptions("en-US")
  6.             .Add("it-IT", true).Add("en-US", true));
  7.         ...
  8.         ...
  9.         ...
  10.             
  11.     }
  12. }

 

Once the globalization functionality is exposed through the UseGlobalization extension method the globalization module may be declared internal in our Owin globalization library:

Globalization middleware
  1. namespace MVCControlsToolkit.Owin.Globalization.Modules
  2. {
  3.     internal class GlobalizationMidlleware : OwinMiddleware
  4.     {
  5.         private OwinGlobalizationOptions options;
  6.         ...
  7.         ...
  8.         ...
  9.     }

The Globalization Middleware

In the constructor we just store the options object:

The constructor
  1. public GlobalizationMidlleware(OwinMiddleware next,
  2.     OwinGlobalizationOptions options)
  3.     : base(next)
  4. {
  5.     if (options == null) throw new ArgumentNullException("options");
  6.     this.options = options;
  7.             
  8. }

 

in the Invoke method:

Extracting selected language
  1. public override async Task Invoke(IOwinContext context)
  2. {
  3.     string urlLanguage = options.CulturePathEnabled ? getPathLanguage(context) : null;
  4.     string cookieLanguage = options.CulturePathEnabled ? getCookieLanguage(context) : null;
  5.     string currLanguage = urlLanguage ?? cookieLanguage;

we extracts the culture from the Url and from the cookie if the associated option is enabled. The Url culture is preferred (see specifications). The extraction is performed by two private methods

If no culture is selected we revert to the Browser culture, otherwise we select the best match between the selected culture and the supported cultures:

Best match
  1. OwinSupportedCulture chosenLanguage = currLanguage == null ?
  2.     BrowserBestMatch(context) :
  3.     options.ClosestSupported(currLanguage);

 

The OwinSupportedCulture class represents the matched supported culture:

  1. internal class OwinSupportedCulture
  2.     {
  3.         public string Name { set; get; }
  4.         public string UIName { set; get; }
  5.         public int MatchLevel { get; set; }
  6.     }

 

Where MatchLevel is an integer that encodes the kind of match that took place with the supported culture (perfect match, language part only, no match the default culture was selected)

Then we may set the chosen cultures in the current thread:

Setting cultures
  1. Thread.CurrentThread.CurrentUICulture =
  2.     new CultureInfo(chosenLanguage.UIName);
  3. Thread.CurrentThread.CurrentCulture =
  4.     new CultureInfo(chosenLanguage.Name);

 

Now we must store the selected culture in the cookie, and/or Url if it is not already stored there.

Setting the culture in the Url
  1. if (options.CulturePathEnabled &&
  2.     options.AlwaysRedirectOnCulturePath &&
  3.     chosenLanguage.Name != urlLanguage)
  4. {
  5.     var Path = context.Request.Path;
  6.     ;
  7.     string fPath = null;
  8.     if (!Path.HasValue) fPath = chosenLanguage.Name;
  9.     else if (urlLanguage == null)
  10.     {
  11.         fPath = chosenLanguage.Name +
  12.             (Path.Value[0] == '/' ? Path.Value :
  13.                 "/" + Path.Value
  14.             );
  15.  
  16.     }
  17.     else
  18.     {
  19.         int position = Path.Value.Substring(1).IndexOf('/');
  20.         if (position < 0) fPath = chosenLanguage.Name;
  21.         else fPath = chosenLanguage.Name + Path.Value.Substring(position + 1);
  22.     }
  23.     var builder = new UriBuilder(context.Request.Uri);
  24.     builder.Path = fPath;
  25.     context.Response.Redirect(builder.Uri.AbsoluteUri);
  26.     return;
  27. }

 

If the  the culture in the path is handled,  if automatic redirect to an Url with a culture in the path is enabled (the default), and if the selected culture is not already contained in the path, then an Url  containing the newly selected culture is computed (any previous culture possibly contained in the Url is removed). After that the execution of the Owin pipeline is aborted and a redirect to the newly computed Url is performed by calling the Response.Redirect method of the context object passed as argument to the Invoke method.

If instead culture storing is enabled and cookie doesn’t contain already the newly selected culture, a new cookie is added to the response:

Cookie preparation
  1. if (options.CookieEnabled &&
  2.     cookieLanguage != chosenLanguage.Name)
  3. {
  4.     context.Response.Cookies
  5.         .Append(options.CookieName, chosenLanguage.Name);
  6. }

Finally we call the next element of the Owin pipeline:

  1. await this.Next.Invoke(context);

 

The OwinGlobalizationOptions.ClosestSupported method that performs the best match between the culture the user is trying to set and any of the supported culture is quite simple:

Best match
  1. internal OwinSupportedCulture ClosestSupported(string culture)
  2. {
  3.     if (!ValidateCulture(culture, true)) return
  4.         new OwinSupportedCulture {
  5.             Name = defaultCulture,
  6.             UIName = defaultCulture,
  7.             MatchLevel = 0 };
  8.     culture = culture.ToLowerInvariant();
  9.     OwinGlobalizationCulture res;
  10.     if (supportedCultures.TryGetValue(culture, out res))
  11.     {
  12.         return new OwinSupportedCulture
  13.         {
  14.             Name = res.Name,
  15.             UIName = res.GetUiCulture(),
  16.             MatchLevel = 2
  17.         };
  18.     }
  19.     if (culture.Length > 2)
  20.     {
  21.         culture = culture.Substring(0, 2);
  22.         if (supportedCultures.TryGetValue(culture, out res))
  23.         {
  24.             return new OwinSupportedCulture
  25.             {
  26.                 Name = res.Name,
  27.                 UIName = res.GetUiCulture(),
  28.                 MatchLevel = 1
  29.             };
  30.         }
  31.     }
  32.     return new OwinSupportedCulture {
  33.         Name = defaultCulture,
  34.         UIName = defaultCulture,
  35.         MatchLevel = 0 };
  36. }

 

If the culture string is invalid, the default culture is returned.

Otherwise the method looks in the dictionary of all supported cultures. If no match is found a match with just the language part is tried, and in case of failure the default culture is returned. The GetUICulture method is defined below:

Supported culture class
  1. public class OwinGlobalizationCulture
  2. {
  3.     public string Name { set; get; }
  4.     public bool UIIsNeutral { set; get; }
  5.     public string GetUiCulture()
  6.     {
  7.         if (UIIsNeutral && Name.Length > 2)
  8.             return Name.Substring(0, 2);
  9.         else return Name;
  10.     }
  11. }

 

The best match with the cultures supported by the browser is a little bit more complex since the browser may support several cultures and may give a preference (0 to 1 float) to each of them (Accept-Language header):

Browser best match
  1.         }
  2. private OwinSupportedCulture BrowserBestMatch(IOwinContext context)
  3. {
  4.     string[] acceptLanguageHeader = null;
  5.     if (context.Request.Headers
  6.         .TryGetValue("Accept-Language", out acceptLanguageHeader)
  7.         && acceptLanguageHeader != null)
  8.     {
  9.         float maxVote = -1.0f;
  10.         OwinSupportedCulture maxElement = null;
  11.         foreach (var language in parseLanguages(acceptLanguageHeader))
  12.         {
  13.             OwinSupportedCulture currLanguage =
  14.                 options.ClosestSupported(language.Name);
  15.             if (currLanguage == null) continue;
  16.             float currVote = currLanguage.MatchLevel * 10
  17.                 + language.Q;
  18.             if (currVote > maxVote)
  19.             {
  20.                 maxVote = currVote;
  21.                 maxElement = currLanguage;
  22.             }
  23.         }
  24.         return maxElement ?? new OwinSupportedCulture
  25.         {
  26.             Name = options.DefaultCulture,
  27.             UIName = options.DefaultCulture,
  28.             MatchLevel = 0
  29.         };
  30.     }
  31.     else return new OwinSupportedCulture
  32.     {
  33.         Name = options.DefaultCulture,
  34.         UIName = options.DefaultCulture,
  35.         MatchLevel = 0
  36.     };
  37. }

 

The “Accept-Language” entry in the context.Request.Headers dictionary contains an array with all preferred cultures. The parseLanguages private method parse  each culture entry to extract the current name and the Q 0-1 float representing the preference. A loop is performed to compute the the culture with the maximum vote, where the vote takes into account the match level with the closest supported culture and the Q of each browser culture.

The whole sources are available on Codeplex.

How to use the Owin Globalization Module

Install the MVCControlsToolkit.Owin.Globalization Nuget package in your Mvc 5 project, then open the Startup.cs file located in the Web Site root, and add the Owin Globalization module as follows:

Globalization helper

If culture storing in the Url is enabled you need also to add the {language}/ segment to all routing rules. Thus for instance:

Standard routing rule

becomes:

Culture enhanced routing rule

Owin Globalization Options

The UseGlobalization extension accepts an OwinGlobalizatioOptions object containing all globalization options as its sole argument. In turn, the OwinGlobalizatioOptions  constructor accepts the default culture as its first obligatory argument, and a second optional boolean that specifies if the UI culture must use only the language part of the default culture. Further options may be added with a fluent interface:

.Add(string culture, bool uIIsNeutral = false)

Adds a new supported culture. If uIIsNeutral is true only the language part of the provided culture is used to set the UICulture.

As a default the selected culture is stored both in the first segment of the Url path and in a cookie whose name is "preferred_culture". The methods below change these default options:

.DisableCultureInPath()

Disables storing the selected culture in the Url.

.DisableRedirect()

If culture storing in the Url is enabled it disables the automatic redirect to an Url containing the selected culture as first segment of the path when an Url containing no culture in the path is received.

.DisableCookie()

Disables storing the selected culture in a cookie

.CustomCookieName(string name)

If storing the selected culture in a cookie is enabled it changes the globalization cookie default name.