воскресенье, 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.

Ссылки

четверг, 10 июня 2010 г.

Ускоряем виртуальную сеть Hyper-V в Windows Server 2008 R2

Сегодня обнаружил, что сервер на базе виртуальной машины Hyper-V очень медленно отдает данные. Первым делом возникло подозрение, что он просто-напросто сильно загружен, однако, эти подозрения не подтвердились - загрузка процессора составляла 2-3%, объем используемой оперативной памяти порядка 50%. Говоря простым языком, сервер "курил", но данные с него тянулись очень медленно. Естественно, такое положение вещей меня не устраивало, и я полез в сеть искать решение.

При поиске в Google информации по данной проблеме, я неизменно наталкивался на обсуждения сетевых адаптеров Intel и таинственное свойство TCP Large Send Offload (IPv4). Суть обсуждений в блогах и форумах сводилась к тому, что виртуальная сеть, построенная на базе адаптеров Intel с данным включенным свойством работала очень медленно. Несмотря на то, что у меня встроенная в материнскую плату сетевуха (какая-то модель Marvell), я решил попробовать выключить это свойство.

Люди в интернете советовали выключать свойство на виртуальной машине, но я для начала попробовал выключить его хостовом сервере. Итак, идем в диспетчер устройств, заходим в свойства сетевого адаптера, далее вкладка "Дополнительно" и для свойства "IPv4: Разгрузка большой отправки" выставляем значение "Отключено". Все эти нехитрые манипуляции я производил при продолжающимся процессе копирования данных. Не прошло и 10 секунд, как изменения были подхвачены, и данные стали копироваться на порядок быстрее.

Количество ссылок по данной теме в интернете наводит на мысль, что это очень частая проблема. Может, Майкрософт стоит включить статью по этому поводу в "Ресурсы и поддержка"? Тем не менее, проблема решена. Hyper-V и Google рулит!

пятница, 28 мая 2010 г.

Простой IoC контейнер на C++

Совсем недавно в проекте, над которым я работаю в данный момент, потребовалось использовать IoC-контейнер. У программистов на C# в этой области достаточно большой выбор (Unity, Ninject, Castle-Windsor); однако, мой проект на С++, поэтому все гораздо сложнее. Потратив некоторое время на поиски решения в Интернете, наткнулся на следующий проект: PocoCapsule, но при взгляде на размер инсталлятора (22 мб), решительно отказался от этого решения, оно показалось мне тяжеловесным. Таким образом, пришло осознание того, что придется писать IoC контейнер самому.

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

Первый вопрос, который мне предстояло решить – каким образом контейнер будет хранить объекты. Варианта было два. Первый – регистрировать в контейнере конкретные экземпляры классов; от этого варианта сразу пришлось отказаться, т.к. хранить в контейнере экземпляры объектов, которые, возможно, никогда не потребуются, – дорогое удовольствие. Второй вариант – регистрировать в контейнере не объекты, а механизмы создания объектов определенного типа, т.е. то, каким образом мы можем создать конкретный экземпляр требуемого объекта. Решение с указателем на функцию меня вполне устроило, на нем и остановился.

Второй вопрос, который предстояло решить, состоял в том, каким образом идентифицировать объекты в контейнере, как задать тип требуемого объекта при его получении из контейнера. Кроме того, возникла еще одна проблема – каким образом определять, что объект, лежащий в контейнере удовлетворяет нашему запросу. В качестве идентификаторов рассматривались три варианта – обычное число типа int, строка (const char* или std::string), либо type_info. Последний вариант отпал сам собой, т.к. в проекте не были включены механизмы RTTI, да и проблему соответствия объекта в контейнере запрашиваемому объекту он не решал; кроме того, оператор typeid в общем случае не гарантирует идентичности возвращаемых type_info при повторном запуске программы. Остались целое число и строка; выбор пал на число в виду его простоты, хотя вариант со строкой вполне допустим, а в некоторых случаях и более предпочтительней.

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

struct ITyped
{
    static 
    const int TypeOf = -1;
    virtual int GetType() const =0;
    virtual bool IsA(int type) const =0;
    virtual ~ITyped() { }
};

Константа TypeOf однозначно идентифицирует класс. Думаю, не стоит говорить о том, что TypeOf у каждого класса должен в приложении быть уникальным. Метод GetType() позволяет узнать типа объекта (аналог typeid), а IsA(int type) проверяет наследует ли данный объект класс, помеченный идентификатором type(аналог оператора is из C# или instanceof из Java).

Теперь вернемся к первому вопросу, а именно, к функциям создания объектов. Первоначально планировался такой их вид:

typedef ITyped* (*OBJECT_CREATE_FUNC)(IoCContainer& iocContainer);

Такая сигнатура мне поначалу показалась удачной – в процессе создания объекта ему могут потребоваться другие объекты, которые он может получить из контейнера (а-ля DI). Однако, немного подумав, я решил добавить еще один параметр – собственно идентификатор создаваемого типа. Это позволило бы использовать одну функцию для создания целого семейства объектов. Получилось вот что:

typedef ITyped* (*OBJECT_CREATE_FUNC)(int type, IoCContainer& iocContainer);

Напомню, что все, что мне требовалось получить от контейнера – это методы регистрации и получения объектов. В данный момент мы обладаем всем необходимым, что реализовать эти требования. Вот интерфейс нашего контейнера:

class IoCContainer
{
    public:
    void Register(int type, OBJECT_CREATE_FUNC createFunc);
    ITyped* Resolve(int type);
private:
map<int, OBJECT_CREATE_FUNC> m_types;
};

Реализация тривиальна:

void IoCContainer::Register(int type, OBJECT_CREATE_FUNC createFunc)
{
    m_types[type] = createFunc;
}
ITyped* IoCContainer::Resolve(int type)
{
    map<int, OBJECT_CREATE_FUNC>::const_iterator pos =     m_types.find(type);
    if (pos == m_types.end())
    {
        // raise exception
    }
    OBJECT_CREATE_FUNC func = pos->second;
    return func(type, *this);
}

Вот пример использования созданного нами контейнера. У нас имеется интерфейс IA, реализующий его класс A:

struct IA : public ITyped
{
    static const int TypeOf = 1;
    int GetType() const { return TypeOf; }
    bool IsA(int type) const { return TypeOf == type; }
    virtual void DoSomething() =0;
};
struct A : public IA
{
    static const int TypeOf = 2;
    int GetType() const { return TypeOf; }
    bool IsA(int type) const { return TypeOf == type || IA::IsA(type); }
    static ITyped* Create(int type, IoCContainer&)
    {
        return new A();
    }
    void DoSomething()
    {
        cout << "A::DoSomething" << endl;
    }
};
int main()
{
    IoCContainer container;
    container.Register(IA::TypeOf, &A::Create);
    IA* pA = (IA*)container.Resolve(IA::TypeOf);
    pA->DoSomething();
    delete pA;
    return 0;
}

На этом можно было бы закончить, но я решил добавить немного сахара в контейнер. Мне не очень нравилась строка:

IA* pA = (IA*)container.Resolve(IA::TypeOf);

во-первых, приходится явно приводить возвращаемый объект к требуемому интерфейсу, а во-вторых зачем писать IA::TypeOf, когда можно переложить эти обязанности на компилятор. Решается данный вопрос достаточно просто – нам на помощь приходят шаблоны:

class IoCContainer
{
    public:
    template <class T> 
    void Register(OBJECT_CREATE_FUNC createFunc)
    {
        Register(T::TypeOf, createFunc);
    }
    template<class T>
    T* Resolve()
    {
        return static_cast<T*>(T::TypeOf);
    }
    void Register(int type,     OBJECT_CREATE_FUNC createFunc);
    ITyped* Resolve(int type);
private:
    map<int, OBJECT_CREATE_FUNC> m_types;
};

Как можно заметить, с помощью шаблонов мы делаем то, что раньше приходилось делать ручками. Теперь нашу функцию main можно переписать следующим образом:

int main()
{
    IoCContainer container;
    container.Register<IA>(&A::Create);
    IA* pA = container.Resolve<IA>();
    pA->DoSomething();
    delete pA;
    return 0;
}

Итак, мы достигли поставленных вначале статьи целей – наш IoC контейнер умеет регистрировать и извлекать объекты. Однако, как и все в этом мире, он не идеален, вот лишь несколько недостатков:

  1. неплохо бы сделать наш контейнер синглтоном;
  2. возлагать удаление полученных от контейнера объектов на пользователя – не лучшая идея; возможно, стоит воспользоваться каким-либо типом умного указателя (например, shared_ptr), и возвращать из контейнера умный, а не обычный указатель;
  3. при получении из контейнера каждый раз создается новый экземпляр объекта. В большинстве сценариев такое поведение не требуется, предпочтительнее, чтобы при каждом запросе возвращался указатель на однажды созданный объект. Однако, эту функциональность я реализую в следующей статье в ближайшее время.

четверг, 11 марта 2010 г.

OpenSUSE 11.1 и Virtual PC 2007 SP1

Сегодня появилась необходимость установить дистрибутив openSUSE 11.1 на виртуальную машину, работающую под управлением Microsoft Virtual PC 2007 SP1. В данный момент на сайте www.opensuse.org доступна версия 11.2, однако, ее под рукой не оказалось, пришлось устанавливать 11.1.

При установке у меня возникли проблемы - после выбора пункта меню Installation в загрузочном диске виртуальная машина радовала меня черным экраном и никак не реагировала на мои действия. После недолгих поисков в Интернете нашлось решение - в параметрах запуска установки следует прописать следующую строку:

noreplace-paravirt i8042.noloop clock=pit

Поступаем подобным образом и оп-ля установка запускается ))) После нескольких десятков минут ожиданий имеем установленный openSUSE 11.1 на Microsoft Virtual PC 2007 SP1.

четверг, 21 мая 2009 г.

Извлечь TIMESTAMP поле из базы данных в виде unix timestamp

Сегодня столкнулся с такой проблемой - нужно было извлечь из базы поле типа TIMESTAMP и результат выборки преобразовать в различные форматы дат. Естественно, для подобных преобразований хотелось бы использовать функции работы со временем из библиотеки Си. Оставалось только преобразовать TIMESTAMP из формата БД в unix формат. После недолгих поисков нашел в интернете такое решение (пример для IBM DB2, однако с некоторыми изменениями подойдет и для любой другой СУБД):

CREATE FUNCTION EPOCH (DB2TIME TIMESTAMP)
RETURNS INTEGER
LANGUAGE SQL
CONTAINS SQL
DETERMINISTIC
RETURN CAST (DAYS(DB2TIME) - DAYS('1970-01-01') AS INTEGER) * 86400 + MIDNIGHT_SECONDS(DB2TIME);

Напомню, что тип time_t в Си или поле TIMESTAMP в БД хранит количество секунд, прошедших с полуночи (00:00:00) 1 Января 1970 года по всемирному времени (UTC, Coordinated Universal Time).