При необходимости переопределите культуру запроса через url / route в ASP.NET приложение Core 1.0

я пытаюсь переопределить культуру текущего запроса. Я заставил его работать частично с помощью custom ActionFilterAttribute.

public sealed class LanguageActionFilter : ActionFilterAttribute
{
    private readonly ILogger logger;
    private readonly IOptions<RequestLocalizationOptions> localizationOptions;

    public LanguageActionFilter(ILoggerFactory loggerFactory, IOptions<RequestLocalizationOptions> options)
    {
        if (loggerFactory == null)
            throw new ArgumentNullException(nameof(loggerFactory));

        if (options == null)
            throw new ArgumentNullException(nameof(options));

        logger = loggerFactory.CreateLogger(nameof(LanguageActionFilter));
        localizationOptions = options;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        string culture = context.RouteData.Values["culture"]?.ToString();

        if (!string.IsNullOrWhiteSpace(culture))
        {
            logger.LogInformation($"Setting the culture from the URL: {culture}");

#if DNX46
            System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
            System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
#else
            CultureInfo.CurrentCulture = new CultureInfo(culture);
            CultureInfo.CurrentUICulture = new CultureInfo(culture);
#endif
        }

        base.OnActionExecuting(context);
    }
}

на контроллере я использую LanguageActionFilter.

[ServiceFilter(typeof(LanguageActionFilter))]
[Route("api/{culture}/[controller]")]
public class ProductsController : Controller
{
    ...
}

это работает до сих пор, но у меня две проблемы с ним:

  1. мне не нравится объявлять {culture} на каждом контроллере, так как он мне понадобится на каждом маршруте.
  2. я, имеющий культуру по умолчанию, не работает с этим подходом, даже если я объявляю это как [Route("api/{culture=en-US}/[controller]")] по понятным причинам.

установка результатов маршрута по умолчанию также не работает.

app.UseMvc( routes =>
{
    routes.MapRoute(
        name: "DefaultRoute",
        template: "api/{culture=en-US}/{controller}"
    );
});

я также исследовал в обычае IRequestCultureProvider реализация и добавить его в UseRequestLocalization методом

app.UseRequestLocalization(new RequestLocalizationOptions
{
    RequestCultureProviders = new List<IRequestCultureProvider>
    {
        new UrlCultureProvider()
    },
    SupportedCultures = new List<CultureInfo>
    {
        new CultureInfo("de-de"),
        new CultureInfo("en-us"),
        new CultureInfo("en-gb")
    },
    SupportedUICultures = new List<CultureInfo>
    {
        new CultureInfo("de-de"),
        new CultureInfo("en-us"),
        new CultureInfo("en-gb")
    }
}, new RequestCulture("en-US"));

но тогда у меня нет доступа к маршрутам там (я предполагаю, что маршруты выполняются позже в конвейере). Конечно, я мог бы также попытаться проанализировать запрошенный url-адрес. И я даже не знаю, смогу ли я изменить маршрут. поместите так, чтобы он соответствовал вышеуказанному маршруту с культурой в нем.

передача культуры через параметр запроса или изменение порядка параметров маршрута-это не вариант.

оба URL-адреса api/en-us/products как то так api/products должен направлять к тому же контроллеру, где первый не изменяет культуру.

порядок, в котором будет определяться культура, должен быть

  1. если определено в url, возьмите его
  2. если не определенный в url, проверьте строку запроса и используйте это
  3. если не определено в запросе, проверьте cookies
  4. если не определено в cookie, используйте .

2-4 осуществляется через UseRequestLocalization и это работает. Также мне не нравится текущий подход, который должен добавить два атрибута к каждому контроллеру ({culture} по маршруту [ServiceFilter(typeof(LanguageActionFilter))]).

Edit: Мне также нравится ограничивать количество допустимых локалей одним набором в SupportedCultures собственность the RequestLocalizationOptions перешло к UseRequestLocalization.

IOptions<RequestLocalizationOptions> localizationOptions на LanguageActionFilter выше не работает, поскольку он возвращает новый экземпляр RequestLocalizationOptions здесь SupportedCultures всегда null и не передается.

FWIW это проект RESTful WebApi.

1 ответов


Обновить ASP.Net Ядро 1.1

новая RouteDataRequestCultureProvider идет как часть 1.1 релиз, что, надеюсь, означает, что вам больше не придется создавать свой собственный поставщик запросов. Вы все еще можете найти здесь полезную информацию (например, биты маршрутизации) или вы можете быть заинтересованы в создании собственного поставщика культуры запросов.


вы можете создать 2 маршрута, которые позволят вам получить доступ к конечным точкам с и без культуры сегмент в url. Оба!--9--> и /api/home будет перенаправлен на домашний контроллер. (Так /api/blah/home не будет соответствовать маршруту с культурой и получит 404, так как контроллер blah не существует)

для этих маршрутов для работы тот, который включает параметр culture, имеет более высокое предпочтение, а параметр culture включает регулярное выражение:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "apiCulture",
        template: "api/{culture:regex(^[a-z]{{2}}-[A-Z]{{2}}$)}/{controller}/{action=Index}/{id?}");

    routes.MapRoute(
        name: "defaultApi",
        template: "api/{controller}/{action=Index}/{id?}");                

});

вышеуказанные маршруты будут работать с контроллером стиля MVC, но если вы создаете интерфейс rest, используя стиль WB api контроллеры, маршрутизация атрибутов является предпочтительным способом в MVC 6.

  • один из вариантов-использовать маршрутизацию атрибутов, но использовать базовый класс для всех ваших контроллеров api, если вы можете установить базовые сегменты url:

    [Route("api/{language:regex(^[[a-z]]{{2}}-[[A-Z]]{{2}}$)}/[controller]")]
    [Route("api/[controller]")]
    public class BaseApiController: Controller
    {
    }
    
    public class ProductController : BaseApiController
    {
        //This will bind to /api/product/1 and /api/en-EN/product/1
        [HttpGet("{id}")]
        public IActionResult GetById(string id)
        {
            return new ObjectResult(new { foo = "bar" });
        }
    } 
    
  • быстрый способ избежать базового класса без необходимости слишком много пользовательского кода через Web api совместимость прокладка:

    • добавить пакет "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-rc1-final"
    • добавить соглашения прокладки:

      services.AddMvc().AddWebApiConventions();
      
    • убедитесь, что ваши контроллеры наследовать от ApiController, который добавляется пакетом прокладки
    • определите маршруты, включая параметр culture с перегрузкой Te MapApiRoute:

      routes.MapWebApiRoute("apiLanguage", 
       "api/{language:regex(^[a-z]{{2}}-[A-Z]{{2}}$)}/{controller}/{id?}");
      
      routes.MapWebApiRoute("DefaultApi", 
       "api/{controller}/{id?}");
      
  • более чистый и лучший вариант будет создавать и применять свой собственный IApplicationModelConvention который заботится о добавлении префикса культуры к вашему атрибутивные маршруты. Это выходит за рамки этого вопроса, но я реализовал идеи для этого статья локализация

тогда вам нужно создать новый IRequestCultureProvider это будет смотреть на url-адрес запроса и извлекать оттуда культуру (если это предусмотрено).

после обновления до ASP .Net Core 1.1 можно избежать ручного анализа url-адреса запроса и извлечения сегмента культуры.

я проверил реализация RouteDataRequestCultureProvider in ASP.Net Core 1.1, и они используют метод расширения HttpContext GetRouteValue(string) для получения сегментов url внутри поставщика запросов:

culture = httpContext.GetRouteValue(RouteDataStringKey)?.ToString();

однако я подозреваю (у меня еще не было возможности попробовать), что это будет работать только при добавлении промежуточное ПО в качестве фильтров MVC. Таким образом, ваше промежуточное ПО запускается после промежуточного по маршрутизации, которое добавляет IRoutingFeature в HttpContext. Как быстрый тест, добавляющ после промежуточного до UseMvc будет вам данные маршрута:

app.Use(async (context, next) =>
{
    //always null
    var routeData = context.GetRouteData();
    await next();
});

для того, чтобы реализовать новый IRequestCultureProvider вам нужно:

  • Поиск параметра culture в url-адресе запроса.
  • если параметр не найден, возвращается null. (Если все поставщики возвращают null, будет использоваться культура по умолчанию)
  • если найден параметр culture, верните новый ProviderCultureResult с этим культура.
  • на промежуточное по локализации будет откат к умолчанию, если он не является одним из поддерживаемых культур.

реализация будет выглядеть так:

public class UrlCultureProvider : IRequestCultureProvider
{
    public Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
    {
        var url = httpContext.Request.Path;

        //Quick and dirty parsing of language from url path, which looks like "/api/de-DE/home"
        //This could be skipped after 1.1 if using the middleware as an MVC filter
        //since you will be able to just call the method GetRouteValue("culture") 
        //on the HttpContext and retrieve the route value
        var parts = httpContext.Request.Path.Value.Split('/');
        if (parts.Length < 3)
        {
            return Task.FromResult<ProviderCultureResult>(null);
        }
        var hasCulture = Regex.IsMatch(parts[2], @"^[a-z]{2}-[A-Z]{2}$");
        if (!hasCulture)
        {
            return Task.FromResult<ProviderCultureResult>(null);
        }

        var culture = parts[2];
        return Task.FromResult(new ProviderCultureResult(culture));
    }
}

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

var localizationOptions = new RequestLocalizationOptions
{
    SupportedCultures = new List<CultureInfo>
    {
        new CultureInfo("de-DE"),
        new CultureInfo("en-US"),
        new CultureInfo("en-GB")
    },
    SupportedUICultures = new List<CultureInfo>
    {
        new CultureInfo("de-DE"),
        new CultureInfo("en-US"),
        new CultureInfo("en-GB")
    }
};
//Insert this at the beginning of the list since providers are evaluated in order until one returns a not null result
localizationOptions.RequestCultureProviders.Insert(0, new UrlCultureProvider());

//Add request localization middleware
app.UseRequestLocalization(localizationOptions, new RequestCulture("en-US"));