Aspnetcore: Url.Action doesn't respect constraints from IApplicationModelConvention

Created on 29 Oct 2018  路  23Comments  路  Source: dotnet/aspnetcore

Describe the bug

I implemented localized routes using a ApplicationModelConvention.
This works fine as long as I type the urls directly in the browser.

So, for example, german routes are only allowed in german culture. and routes from other languages are returning a 404. Exactly as expected.

But when I use Url.Action it doesn't respect the constraints. so for example if i'm on the english domain i get russian links (depending on the order they are inserted)

What am I doing wrong?

Startup Code:

 services.AddMvc(o =>
            {
                o.Conventions.Insert(0, new LocalizedRouteConvention());
            })

Convention

public class LocalizedRouteConvention : IApplicationModelConvention
{
    public void Apply(ApplicationModel application)
    {
        foreach (var controller in application.Controllers)
        {
            foreach (var action in controller.Actions)
            {
                var attributes = action.Attributes.OfType<RouteAttribute>().ToArray();
                if (!attributes.Any()) return;

                foreach (var attribute in attributes)
                {
                    SelectorModel defaultSelector = action.Selectors.First();

                    foreach (var localizedVersion in GetLocalized(attribute.Template))
                    {
                        if (!action.Selectors.Any(s => s.AttributeRouteModel.Template == localizedVersion.Template))
                        {
                            action.Selectors.Insert(0, new SelectorModel(defaultSelector)
                            {
                                AttributeRouteModel = localizedVersion,
                                ActionConstraints =
                                {
                                    new CultureActionConstraint { Culture = ((LocalizedRouteAttribute) localizedVersion.Attribute).Culture }
                                }
                            });
                        }
                    }
                }
            }
        }
    }
}

And here the constraint

    public class CultureActionConstraint : IActionConstraint
    {
        public string Culture { get; set; }

        public int Order => 0;

        public bool Accept(ActionConstraintContext context)
        {
            return CultureInfo.CurrentCulture.TwoLetterISOLanguageName == Culture;
        }
    }

Expected behavior

Url.Action should also only use the routes that are allowed.

area-mvc

All 23 comments

To reproduce, checkout and run https://github.com/cypressious/asp-net-core-localized-routes-issue, then open http://localhost:5000/about_ru?culture=ru. The generated link should be to http://localhost:5000/privacy_ru but it's actually http://localhost:5000/privacy_bg

When debugging, I noticed that the IActionConstraint is only evaluated when navigating to a URL but not when generating URLs.

Thanks guys for reproducing the issue, I will try to investigate on this soon

It takes long time for me to realize that the origin for the issue come from Routing specifically IRouter.GetVirtualPath().

Meanwhile I notice that the router fetches the route template with the first supported cultures sorted by name - I don't know why!! - in your case bg, so that's why the url is privacy_bg, f you add ar culture you will get privacy_ar

@rynowak @JamesNK any ideas what's going on here

Do you think its a bug or more likely a non specified feature?
Any workaround you can think of? currently this is a showstopper for us.

I'm not sure why you didn't use RouteDataCultureProvider to simplify the process instead of doing localized routes manually?!!

@hishamco Sorry i don't understand what exactly you mean. Do you have an example?

What I mean is you can make of use of Microsoft.AspNetCore.Localization.Routing, have a look to a unit test for more details https://github.com/aspnet/Localization/blob/master/test/Microsoft.AspNetCore.Localization.Routing.Tests/RouteDataRequestCultureProviderTest.cs

Sorry maybe its a dumb question. But I don't see how i can add localizations of a route this way.

E.g if we have a controller like this

[HttpGet("user")]
public async Task<IActionResult> Get()
{

We have in a storage informations that translates the "user" to "benutzer" in german. and therefore the route should be aviable through /benutzer in french it should be /utilisateur and not "user".

In the Test you provided I can only see how to set and recognize the culture a request shouldhave, but not how to create routes related to the culture.

What am I missing?

I see you want to create a localized routes, for instance:
http://your-domain.com/user for en culture
http://your-domain.com/benutzer for de culture

To be precise, we want

http://your-domain.com/user for en culture
http://your-domain.de/benutzer for de culture

Seems your implementation is similar to what I saw in this post https://www.strathweb.com/2015/11/localized-routes-with-asp-net-5-and-mvc-6/

Yes, it's directly inspired by it. However, The author doesn't say anything about GetVirtualPath().

@filipw did you faced this issue before while you wrote the post that I mentioned above?

@cypressious @BoasE let me try my last attempt to make workaround to your issue, hope to reply ASAP

Finally I come up with a simple workaround - perhaps stupid 馃槃 - to fix your issue

[HtmlTargetElement("a")]
    public class LocalizedLinkTagHelper : TagHelper
    {
        private HttpContext _httpContext;

        public LocalizedLinkTagHelper(IHttpContextAccessor httpContextAccessor)
        {
            _httpContext = httpContextAccessor.HttpContext;
        }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var selectedCulture = _httpContext.Features.Get<IRequestCultureFeature>().RequestCulture.Culture;
            if (output.Attributes.TryGetAttribute("href", out TagHelperAttribute hrefAttribute))
            {
                var url = hrefAttribute.Value?.ToString();
                var twoLetterISOLanguageName = url?.Split("_")?[1];
                if (twoLetterISOLanguageName != null)
                {
                    url = url.Replace(twoLetterISOLanguageName, selectedCulture.TwoLetterISOLanguageName);
                    output.Attributes.SetAttribute("href", url);
                }
            }
        }
    }

but you should use <a asp-action="Privacy" asp-controller="Home">Privacy</a> instead of @Html.ActionLink("Privacy", "Privacy", "Home"), which confirm that there's a bug here in routing

As a workaround this should work. But always has the risk that someone uses accidentially method Like Html.Action , Controller.RedirectToAction ...

We will think about the workabout.

In the meanwhile is there a chance to get the behavior in asp core fixed? Should we report it somewhere else?

As a workaround this should work. But always has the risk that someone uses accidentially method Like Html.Action , Controller.RedirectToAction ...

using asp-controller, asp-action taghelper will work fine in the views, so don't worry about the code, the issue in URL generation

In the meanwhile is there a chance to get the behavior in asp core fixed? Should we report it somewhere else?

IMHO you can move this bug or refer to it in routing repo

I'm not sure what's the current status of this?!!

Well it seems like this is just not implemented and the last comment on the other thread is, that a route constraint should be used, but in the IApplicationModelConvention we don't see a possibilty to add one

A route constraint is part of the route. For attribute routing the route constraint would need to be in the template, e.g. the template "{controller}/{action}/{culture:custom-constraint}" would resolve a constraint with the string "custom-constraint". This string is associated with the constraint type in the RouteOptions.

If you want to continue to add new SelectorModel instances then you would add the constraint in a modified template.

I found this blog post that goes into more detail about creating and registering route constraints: https://tommyb.com/blog/customizing-asp-net-cores-route-constraints-and-model-binding/

Thanks for contacting us. We're closing this issue as there was no community involvement here for quite a while now.

Was this page helpful?
0 / 5 - 0 ratings