воскресенье, 8 января 2017 г.

Использование промежуточных уровней (middleware) в качестве фильтров MVC в ASP.NET Core 1.1

Оригинальный пост

Одной из новых функций, которая появилась в ASP.NET Core 1.1, стала возможность использовать промежуточные уровни в качестве фильтров MVC. В этом посте я рассмотрю исходный код реализации этой функции, а не то, как ее использовать. В следующем посте я вернусь к тому, как ее использовать, чтобы добиться большего повторного использования кода.

Промежуточные уровни и фильтры

Для начала рассмотрим причины, по которым вы можете использовать помемежуточные уровни поверх фильтров и наоборот. Обе эти возможности разработаны для внедряемых действий (cross-cutting concern) в вашем приложении и обе используются в рамках конвейера обработки запроса, поэтому в некоторых случаях вы можете успешно использовать любую из них.

Главное отличие состоит в границах их действия. Фильтры являются частью MVC и они ограничены промежуточным слоем MVC. Промежуточный уровень имеет доступ только к HttpContext и всему, что было добавлено предыдущими middleware. Фильтры, напротив, имеют доступ ко всему контексту MVC и могут обращаться, например, к данным маршрутизации и информации биндинга модели.

В общем, если у вас есть внедряемые действия (cross-cutting concern), которые не привязаны к MVC, то используйте промежуточный уровень; если ваши внедряемые действия (cross-cutting concern) зависят от MVC или выполняются в рамках конвейера MVC, то используйте фильтры.

Итак, почему теперь вы можете захотеть использовать промежуточный уровень в качестве фильтров. Я могу представить несколько причин для этого.

Во-первых, у вас уже может реализован некоторый промежуточный уровень, который выполняет что-то полезное, и вам хочется использовать его поведение в рамках конвейера MVC. Вы можете переписать этот промежуточный уровень для использования в качестве фильтр, но было бы лучше просто использовать его так, как есть. Это особенно актуально, если вы используйте часть внешнего промежуточного уровня и не имеете доступа к его исходному коду.

Во-вторых, вы хотите использовать одну и ту же функциональность и в качестве промежуточного уровня, и в качестве фильтра. В этом случае у вас просто будет одна реализация, которую вы сможете использовать в обоих случаях.

Использование MiddlewareFilterAttribute

В посте с анонсом можно найти пример того, как использовать промежуточный уровень в качестве фильтра. Здесь я покажу обратный пример, в котором я хочу выполнить MyCustomMiddleware в тот момент, когда выполняется действие MVC.

Требуется реализовать две вещи. Во-первых нужно создать объект конвейера промежуточного уровня:
public class MyPipeline  
{
    public void Configure(IApplicationBuilder applicationBuilder) 
    {
        var options = // any additional configuration

        applicationBuilder.UseMyCustomMiddleware(options);
    }
}
и во-вторых пометить атрибутом MiddlewareFilterAttribute действие контроллера или сам контроллер:
[MiddlewareFilter(typeof(MyPipeline))]
public IActionResult ActionThatNeedsCustomfilter()  
{
    return View();
}
При таком объявлении MyCustomMiddleware будет выполняться каждый раз, когда вызывается метод действия ActionThatNeedsCustomfilter. Стоит отметить, что MiddlewareFilterAttribute для метода действия сам не принимает тип компонента промежуточного уровня (MyCustomMiddleware), он на самом деле принимает объект конвейера, который сам настраивает промежуточный уровень. Не сильно об этом беспокойтесь сейчас, мы вернемся к этому позже. В конце этого поста я покажу в коде MVC как реализована эта функциональность.

Атрибут MiddlewareFilterAttribute 

 Как мы могли убедиться ранее, функциональность промежуточного уровня-фильтра начитается с атрибута MiddlewareFilterAttribute, которым помечается весь контроллер или отдельный его метод. Этот атрибут реализует интерфейс IFilterFactory, который используется для инжектирования сервисов в фильтры MVC. Реализация этого интерфейса требует всего один метод CreateInstance(IServiceProvider provider):
public class MiddlewareFilterAttribute : Attribute, IFilterFactory, IOrderedFilter  
{
    public MiddlewareFilterAttribute(Type configurationType)
    {
        ConfigurationType = configurationType;
    }

    public Type ConfigurationType { get; }

    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        var middlewarePipelineService = serviceProvider.GetRequiredService();
        var pipeline = middlewarePipelineService.GetPipeline(ConfigurationType);

        return new MiddlewareFilter(pipeline);
    }
}
Реализация этого атрибута весьма выразительна. Сначала из IoC контейнера извлекается объект MiddlewareFilterBuilder. Затем у этого объекта вызывается метод GetPipeline, в него передается ConfigurationType, который был задан при объявлении атрибута (MyPipeline в предыдущем примере). GetPipeline возвращает RequestDelegate, который представляет собой конвейер промежуточного уровня, который принимает HttpContext и возвращает Task:
public delegate Task RequestDelegate(HttpContext context). 
Наконец, этот делегат используется для создания нового MiddlewareFilter, который возвращается как результат метода. Этот подход использования атрибута IFilterFactory для создания актуального экземпляра фильтра очень часто встречается в коде MVC, и решает проблему внедрения сервисов в атрибуты, а так же заставляет каждый компонент следовать принципу единственной ответственности.

Построение конвейера с использованием MiddlewareFilterBuilder

 В последнем примере кода мы видели, как MiddlewareFilterBuilder использовался для превращения типа MyPipeline в актуальную, запускаемую часть промежуточного уровня. Вызов GetPipeline инициализирует конвейер для переданного типа, используя метод BuildPipeline, который представлен ниже в краткой форме:
private RequestDelegate BuildPipeline(Type middlewarePipelineProviderType)  
{
    var nestedAppBuilder = ApplicationBuilder.New();

    // Get the 'Configure' method from the user provided type.
    var configureDelegate = _configurationProvider.CreateConfigureDelegate(middlewarePipelineProviderType);
    configureDelegate(nestedAppBuilder);

    nestedAppBuilder.Run(async (httpContext) =>
    {
        // additional end-middleware, covered later
    });

    return nestedAppBuilder.Build();
}
Этот метод создает новый IApplicationBuilder и использует его для настройки конвейера промежуточного уровня, используя конвейер, заданный ранее (MyPipeline). Затем он добавляет кусочек "конечного промежуточного уровня" в конец конвейера, к которому я вернусь позже, и создает конвейер в RequestDelegate.

Создание конвейера из MyPipeline выполняется в MiddlewareFilterConfigurationProvider, в котором ищется подходящий для него метод Configure.

Вы можете подумать, что класс MyPipeline является классом мини-Startup. Как и в классе Startup вам потребуется метод Configure для добавления промежуточного уровня в IApplicationBuilder, и, как и в классе Startup, вы можете инжектировать дополнительные сервисы в этот метод. Одним из основных отличий является то, что у вас нет специфичных для окружения методов Configure, таких как ConfigureDevelopment, - ваш класс должен иметь один и только один конфигурационный метод Configure.

Фильтр MiddlewareFilter

Итак, в качестве резюме - вы помечаете атрибутом MiddlewareFilterAttribute один из ваших методов действия или контроллер, передаете конвейер в качестве фильтра, например, MyPipeline. Он использует MiddlewareFilterBuilder для создания RequestDelegate, который, в свою очередь, создает MiddlewareFilter. И уже этот объект передается в конвейер обработки фильтров MVC.

MiddlewareFilter реализует IAsyncResourceFilter, поэтому в конвейере фильтров он выполняется раньше - сразу после того, как отработает AuthorizationFilter, но перед фильтрами Model Binding и Action. Это позволит вам полностью прервать выполнение запроса, если потребуется.

MiddlewareFilter реализует всего один обязательный метод OnResourceExecutionAsync. Этот метод очень простой. Сначала он сохраняет в отдельный объект MiddlewareFilterFeature контекст выполнения ResourceExecutingContext и следующий фильтр ResourceExecutionDelegate. Затем с помощью HttpContext вызывается следующий промежуточный уровень в конвейере.
public class MiddlewareFilter : IAsyncResourceFilter  
{
    private readonly RequestDelegate _middlewarePipeline;
    public MiddlewareFilter(RequestDelegate middlewarePipeline)
    {
        _middlewarePipeline = middlewarePipeline;
    }

    public Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var httpContext = context.HttpContext;

        var feature = new MiddlewareFilterFeature()
        {
            ResourceExecutionDelegate = next,
            ResourceExecutingContext = context
        };
        httpContext.Features.Set<IMiddlewareFilterFeature>(feature);

        return _middlewarePipeline(httpContext);
    }
}
Промежуточный уровень, который мы только что создали, считает, что он вызывается в рамках нормального конвейера - он принимает только HttpContext. Если ему потребуется, он может получить доступ к MVC контексту через доступ к MiddlewareFilterFeature.

Если вы раньше писали фильтры, то вам может показаться, что чего-то не хватает. Обычно, перед возвратом из метода вы вызываете await next() для выполнения следующего фильтра в конвейере, но мы просто возвращаем Task, который получили от вызова RequestDelegate. Но как в этом случае продолжается выполнение конвейера? Для этого мы вернемся к "конечному промежуточного уровню", который я рассматривал в рамках BuildPipeline.

Использование "конечного-middleware" для продолжения конвейера фильтров

Промежуточный уровень, который мы добавили в конце метода BuildPipeline отвечает за продолжение выполнения конвейера фильтров. Схематично он выглядит так:
nestedAppBuilder.Run(async (httpContext) =>  
{
    var feature = httpContext.Features.Get<IMiddlewareFilterFeature>();

    var resourceExecutionDelegate = feature.ResourceExecutionDelegate;
    var resourceExecutedContext = await resourceExecutionDelegate();

    if (!resourceExecutedContext.ExceptionHandled && resourceExecutedContext.Exception != null)
    {
        throw resourceExecutedContext.Exception;
    }
});

Этот промежуточный уровень выполняет две главные функции. Первая цель - убедиться, что конвейер фильтров не прерывается после вызова MiddlewareFilter. Это реализуется путем получения IMiddlewareFeatureFeature, сохраненной в HttpContext до вызова фильтра. После этого можно получить доступ к следующему фильтру через его свойство ResourceExecutionDelegate и асинхронно выполнить его с помощью await.

Вторая цель - сэмулировать поведение конвейера промежуточного уровня вместо конвейера фильтров в тот момент, когда возбуждается исключение. То есть, если один из последующих фильтров или действий возбуждает исключение и ни один фильтр не обрабатывает его, то "конечный-middleware" возбуждает его снова; таким образом, конвейер middleware, используемый в фильтре, может обработать его обычным образом (с помощью try-catch).

Обратите внимание, что Get() будет вызван внутри каждого MiddlewareFilter. Если у вас будет несколько MiddlewareFilters в конвейере, то каждый из них будет добавлять экземпляр IMiddlewareFilterFeature, перезаписывая значение, добавленное ранее. Я не буду углубляться, но это может вызвать проблему, если у вас есть промежуточный уровень в вашем MyCustomMiddleware, которые оба обрабатывают ответ конвейера выполнения других middleware и пытаются получить IMiddlewareFilterFeature. В этом случае они получат IMiddlewareFilterFeature, ассоциированную с разными MiddlewareFilter. Этот сценарий маловероятен, но имейте его в виду.

Заключение

Мы закончили заглядывать под капот промежуточных уровней-фильтров, надеюсь, что это было интересно. Я был рад изучить исходный код в качестве источника вдохновения, которое мне понадобится в будущем для реализации чего-то подобного. Надеюсь, и вам понравилось.

четверг, 5 января 2017 г.

Локализация ASP.NET Core приложения


В этом посте я рассмотрю процесс локализации ASP.NET Core приложения, используя рекомендованный подход с файлами ресурсов resX.

Введение в локализацию

Локализация в ASP.NET Core в целом аналогична тому, как она работает в ASP.NET 4.x. В общем случае вы добавляете в приложение несколько файлов ресурсов с расширением .resx - по одному на каждую культуру, которую вы собираетесь поддерживать. В дальнейшем вы ссылаетесь на ресурсы с использованием ключа и текущей культуры; подходящее значение выбирается из первого подходящего файла ресурсов.

В то время, как концепция одного .resX файла для каждой культуры осталась ASP.NET Core, способ их использования кардинально изменился. В предыдущей версии при добавлении .resX файла в приложении создавался файл дизайна, который предоставлял статический строго типизированный доступ к вашим ресурсам через вызовы наподобие Resources.MyTitleString.

В ASP.NET Core  для доступа к ресурсам используются две абстракций - IStringLocalizer и IStringLocalizer, которые обычно инжектируются в нужные места с помощью механизма внедрения зависимостей. Эти интерфейсы имеют свойство-индексатор, которое позволяет вам получать доступ к ресурсу по строковому ключу. Если для данного ключа не существует ресурса (например, вы не создали подходящий .resX файл с нужным ресурсом), то сам ключ будет возвращен в качестве результата.

Рассмотрим следующий пример:
using Microsoft.AspNet.Mvc;  
using Microsoft.Extensions.Localization;

public class ExampleClass  
{
    private readonly IStringLocalizer<ExampleClass> _localizer;
    public ExampleClass(IStringLocalizer<ExampleClass> localizer)
    {
        _localizer = localizer;
    }

    public string GetLocalizedString()
    {
        return _localizer["My localized string"];
    }
}
В этом примере вызов GetLocalizedString() заставит IStringLocalizer взять текущую культуру и проверить, что подходящий ресурсный файл для ExampleClass содержит ресурс с именем/ключом "My localized string". Если он найдет указанный ресурс, то вернет его локализованную версию, в противном случае вернет "My localized string".

Идея, лежащая в основе этого подхода, состоит в том, чтобы изначально проектировать ваше приложение с учетом будущей локализации без необходимости создавать фиктивный .resX файл. Вместо этого, вы можете просто задавать значения по-умолчанию, а в будущем добавить локализованные ресурсы.

Что касается меня, то мне не очень нравится этот подход - меня раздражают все эти волшебные строки, которые являются ключами в словаре. Любые изменения в этих ключах могут привести к нежелательным последствиям, как я покажу дальше в этом посте.

Добавление локализации в ваше приложение

Теперь я откажусь от этой концепции и рассмотрю подход, который рекомендует Microsoft. Я начну со стандартного шаблона ASP.NET Core Web application без аутентификации - вы можете найти весь код на GitHub.

Первым шагом будет добавление в приложение сервисов локализации. Т.к. мы разрабатываем MVC приложение, то мы также настроим локализацию представлений и локализацию, основанную на DataAnnotations. Нужные пакеты для локализации уже содержатся в пакете Microsoft.AspNetCore.MVC в качестве ссылок, поэтому у вас есть возможность сразу добавлять сервисы и промежуточные уровни (middleware) в класс Startup:
public void ConfigureServices(IServiceCollection services)  
{
    services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });

    services.AddMvc()
        .AddViewLocalization(
            LanguageViewLocationExpanderFormat.Suffix,
            opts => { opts.ResourcesPath = "Resources"; })
        .AddDataAnnotationsLocalization();
}
Эти сервисы позволят вам инжектировать IStringLocalizer в ваши классы. Они также позволят вам иметь локализованные файлы представлений (т.о. вы можете иметь представления с именами наподобие MyView.fr.cshtml) и инжектировать IViewLocalizer, что даст вам возможность использовать локализацию в ваших файлах представлений. Вызов AddDataAnnotationsLocalization настраивает атрибуты валидации так, чтобы они извлекали ресурсы с помощью IStringLocalizer.

Параметр ResourcePath объекта Options задает директорию вашего приложения, в которой расположены файлы с ресурсами. Если корень вашего приложения расположен по пути ExampleProject, то мы уточнили, что наши ресурсы будут находится в папке ExampleProject/Resources.

Настройка указанных классов - это все, что требуется для того, чтобы использоваться сервисы локализации в приложении. Однако, обычно требуется некоторый способ для того, чтобы определять текущую культуру для заданного запроса.

Чтобы сделать это, мы будем использовать RequestLocalizationMiddleware. Этот промежуточный уровень использует несколько различных провайдеров для определения текущей культуры. Для того, чтобы настроить ее с провайдерами по-умолчанию, нам требуется определить поддерживаемые культуры, и какая культура будет использоваться по-умолчанию.
public void ConfigureServices(IServiceCollection services)  
{
    // ... previous configuration not shown

    services.Configure<RequestLocalizationOptions>(
        opts =>
        {
            var supportedCultures = new[]
            {
                new CultureInfo("en-GB"),
                new CultureInfo("en-US"),
                new CultureInfo("en"),
                new CultureInfo("fr-FR"),
                new CultureInfo("fr"),
            };

            opts.DefaultRequestCulture = new RequestCulture("en-GB");
            // Formatting numbers, dates, etc.
            opts.SupportedCultures = supportedCultures;
            // UI strings that we have localized.
            opts.SupportedUICultures = supportedCultures;
        });
}

public void Configure(IApplicationBuilder app)  
{
    app.UseStaticFiles();

    var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
    app.UseRequestLocalization(options.Value);

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Использование локализации в ваших классах

Теперь у нас есть все части для добавления локализации в приложение. Но у нас по-прежнему отсутствует способ, с помощью которого пользователь может задать требуемую культуру, но мы скоро реализуем его. А пока давайте рассмотрим способ, с помощью которого можно получать локализованные строки.

Контроллеры и сервисы

Каждый раз, когда вам в вашем сервисе или контроллере нужно получить локализованную строку, вы можете инжектировать IStringLocalizer и использовать его свойство-индексатор. Представим, что нам нужно получить локализованную строку в контроллере:
public class HomeController: Controller  
{
    private readonly IStringLocalizer<HomeController> _localizer;

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

    public IActionResult Index()
    {
        ViewData["MyTitle"] = _localizer["The localised title of my app!"];
        return View(new HomeViewModel());
    }
}
При вызове _localizer[] будет осуществляться поиск требуемой строки с учетом текущей культуры и типа HomeController. Представим, что наше приложение настроено так, как мы обсуждали выше - HomeController расположен в пространстве имен ExampleProject.Controllers и мы используем fr культуру; в этом случае локализатор будет просматривать следующие ресурсные файлы:
  • Resources/Controller.HomeController.fr.resx
  • Resources/Controller/HomeController.fr.resx
Если один из этих файлов содержит ресурс с ключом "The localised title of my app!", то он будет использован. В противном случае сам ключ будет использован в качестве ресурса. Это означает, что вам не нужно добавлять файлы ресурсов, чтобы начать использовать локализацию - вы можете просто использовать строку на исходном языке в качестве ключа, и вернуться к добавлению файлов .resX в будущем.

Представления

Существуют два способа локализации представлений. Как было описано раньше, вы можете локализовать все представление, скопировав и отредактировав его, добавив суфикс культуры. Этот подход будет полезен, если представления сильно отличаются в различных культурах.

Вы также можете локализовать строки тем же способом, каким мы локализовали HomeController. Вместо IStringLocalizer вы инжектируете IViewLocalizer в представление. Он обрабатывает HTML кодирование немного по-другому, что позволяет вам хранить HTML-разметку в качестве ресурсов и она не будет кодироваться при рендеринге. Как правило, это не требуется и вы будете использовать локализацию строк, а не HTML.

IViewLocaliser использует имя представления для поиска ресурсов, так для представления Index.cshtml контроллера HomeController культуры fr локализатор будет просматривать следующие файлы:
  • Resources/Views.Home.Index.fr.resx
  • Resources/Views/Home/Index.fr.resx
Использование IViewLocaliser совпадает с использованием IStringLocalizer - вы передаете строку на исходном языке в качестве ключа:
@using Microsoft.AspNetCore.Mvc.Localization
@model AddingLocalization.ViewModels.HomeViewModel
@inject IViewLocalizer Localizer
@{
    ViewData["Title"] = Localizer["Home Page"];
}
<h2>@ViewData["MyTitle"]</h2>  

DataAnnotations

Последней часто используемой частью MVC, которая требуется локализация, являются атрибуты DataAnnotations. Эти атрибуты могут быть использованы для валидации, именования и UI-хинтов в ваших MVC моделях. При использовании они предоставляют много декларативных метаданных в MVC, позволяя инфраструктуре выбрать подходящий элемент управления для редактирования свойства модели.

Сообщения об ошибках валидации DataAnnotation проходят через IStringLocalizer, если вы настроили MVC с использованием AddDataAnnotationsLocalization(). Как и раньше, это позволяет вам настроить сообщение об ошибках на исходном языке и использовать его в качестве ключа в будущем.
public class HomeViewModel  
{
    [Required(ErrorMessage = "Required")]
    [EmailAddress(ErrorMessage = "The Email field is not a valid e-mail address")]
    [Display(Name = "Your Email")]
    public string Email { get; set; }
}
В этом коде вы можете увидеть три атрибута DataAnnotation, два из которых являются ValidationAttribute, и один - DisplayAttribute, который им не является. Значение ErrorMessage, которое задано для каждого ValidationAttribute используется в качестве ключа для поиска подходящего ресурса с помощью IStringLocalizer. И снова, следующие файлы будут использоваться для поиска:
  • Resources/ViewModels.HomeViewModel.fr.resx
  • Resources/ViewModels/HomeViewModel.fr.resx
Ключевой особенностью, о которой стоит беспокоиться, является то, что DisplayAttribute не локализуется с использованием IStringLocalizer. Это далеко не идеальное решение, и я вернусь к нему в моем следующем посте.

Возможность для пользователей задавать культуру

Последней частью пазла является возможность для пользователей выбирать свою культуру. RequestLocalizationMiddleware использует расширяемый механизм провайдеров для получения текущей культуры запроса и поставляется с тремя встроенными провайдерами:
  • QueryStringRequestCultureProvider
  • AcceptLanguageHeaderRequestCultureProvider
  • CookieRequestCultureProvider
Они позволяют задавать культуру в параметре запроса (?culture=fr-FR), с помощью заголовка Accept-Language или через cookie-файл. Из всех этих подходов cookie-файлы являются наименее навязчивыми, т.к. они передаются с каждым запросом и не требуют от пользователя установки заголовка Accept-Language или формирования строки запроса.

Опять же, проект-пример Localization.StarterWeb предоставляет удобную реализацию того, как можно добавить поле выбора в нижний колонтитул сайта, которое позволить пользователю задавать язык. Выбор пользователя сохраняется в cookie-файле, который обрабатывается CookieRequestCultureProvider для каждого запроса. Этот провайдер затем устанавливает значение CurrentCulture и CurrentUICulture для потока, который обрабатывает запрос.

Для добавления поля выбора в приложение создайте частичное представление _SelectLanguagePartial.cshtml в папке Shared для представлений:
@using System.Threading.Tasks
@using Microsoft.AspNetCore.Builder
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.Extensions.Options

@inject IViewLocalizer Localizer
@inject IOptions<RequestLocalizationOptions> LocOptions

@{
    var requestCulture = Context.Features.Get<IRequestCultureFeature>();
    var cultureItems = LocOptions.Value.SupportedUICultures
        .Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
        .ToList();
}

<div title="@Localizer["Request culture provider:"] @requestCulture?.Provider?.GetType().Name">  
    <form id="selectLanguage" asp-controller="Home"
          asp-action="SetLanguage" asp-route-returnUrl="@Context.Request.Path"
          method="post" class="form-horizontal" role="form">
        @Localizer["Language:"] <select name="culture"
                                        asp-for="@requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems"></select>
        <button type="submit" class="btn btn-default btn-xs">Save</button>

    </form>
</div>  
Нам хочется отображать это представление на каждой странице сайта, поэтому добавим его в нижний колонтитул представления _Layout.cshtml:
<footer>  
    <div class="row">
        <div class="col-sm-6">
            <p>&copy; 2016 - Adding Localization</p>
        </div>
        <div class="col-sm-6 text-right">
            @await Html.PartialAsync("_SelectLanguagePartial")
        </div>
    </div>
</footer>  
Наконец, нам нужно добавить код в контроллер для обработки пользовательского выбора. Он ссылается на SetLanguage контроллера HomeController.
[HttpPost]
public IActionResult SetLanguage(string culture, string returnUrl)  
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );

    return LocalRedirect(returnUrl);
}
Вот и все. Если сейчас мы откроем домашнюю страницу нашего сайта, то увидим поле выбора культуры в правом нижнем углу. На этом шаге я не добавлял ни одного ресурсного файла, поэтому если я вызову исключение валидации, то увижу, что ключ ресурса будет использован в качестве значения.


Мой процесс разработки не прервался из-за отсутствия ресурсных файлов. Я просто мог разрабатывать приложение с использованием исходного языка и добавить resX файлы позже. Если в будущем я добавлю подходящие ресурсные файлы для fr культуры, и пользователь изменит свою культуру с помощью поля выбора, то он увидит результаты локализации в атрибутах валидации и других локализованных строках.


Как вы можете видеть, атрибуты валидации и заголовок страницы локализовались, но метка поля 'Your Email' нет, хотя и была помечена атрибутом DisplayAttribute.

Заключение

В этом посте я показал как добавить локализацию в ваше ASP.NET Core приложение с использованием рекомендованного способа с ресурсами в виде ключей на исходном языке и добавлением дополнительных ресурсов в будущем.

Вот приблизительные шаги для локализации вашего приложения:
  1. Добавить требуемые сервисы локализации
  2. Настроить промежуточный уровень локализации и провайдер культуры, если потребуется
  3. Инжектировать IStringLocalizer в ваши контролеры и сервисы для локализации строк
  4. Инжектировать IViewLocalizer в ваши представления для локализации строк в них
  5. Добавить файлы ресурсов для дополнительных культур
  6. Добавить механизм выбора пользователем его культуры
В следующем посте, я обращусь к некоторым проблемам, с которыми столкнулся в процессе локализации нашего приложения, а именно уязвимость волшебных строку в типах и локализация атрибута DisplayAttribute.

Ссылки