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:
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.
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:
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).
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:
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:
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:
After that, in the pipeline definition we may simply call the newly defined extension method instead of IAppBuilder.Use<T>(object o):
Once the globalization functionality is exposed through the UseGlobalization extension method the globalization module may be declared internal in our Owin globalization library:
In the constructor we just store the options object:
in the Invoke method:
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:
The OwinSupportedCulture class represents the matched supported culture:
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:
Now we must store the selected culture in the cookie, and/or Url if it is not already stored there.
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:
Finally we call the next element of the Owin pipeline:
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:
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:
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):
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.
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:
If culture storing in the Url is enabled you need also to add the {language}/ segment to all routing rules. Thus for instance:
becomes:
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.