11
votes

We have built an ASP.NET Core 2.1 website where URLs like www.example.org/uk and www.example.org/de determine what resx file and content to show. After upgrading to ASP.NET Core 2.2, pages load but all links generated produce blank/empty href's.

For example, a link this:

<a asp-controller="Home" asp-action="Contact">@Res.ContactUs</a>

will in 2.2 produce an empty href like so:

<a href="">Contact us</a>

But in 2.1 we get correct href:

<a href="/uk/contact">Contact us</a>

We are using a Constraint Map to manage the URL-based language feature - here is the code:

Startup.cs

// configure route options {lang}, e.g. /uk, /de, /es etc
services.Configure<RouteOptions>(options =>
{
    options.LowercaseUrls = true;
    options.AppendTrailingSlash = false;
    options.ConstraintMap.Add("lang", typeof(LanguageRouteConstraint));
 });

 ...

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

LanguageRouteConstraint.cs

public class LanguageRouteConstraint : IRouteConstraint
{
    private readonly AppLanguages _languageSettings;

    public LanguageRouteConstraint(IHostingEnvironment hostingEnvironment)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(hostingEnvironment.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

        IConfigurationRoot configuration = builder.Build();

        _languageSettings = new AppLanguages();
        configuration.GetSection("AppLanguages").Bind(_languageSettings);
    }

    public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.ContainsKey("lang"))
        {
            return false;
        }

        var lang = values["lang"].ToString();
        foreach (Language lang_in_app in _languageSettings.Dict.Values)
        {
            if (lang == lang_in_app.Icc)
            {
                return true;
            }
        }
        return false;
    }
}

I narrowed down the problem but can't find a way to solve it; Basically in 2.2. some parameters are not set in the above IRouteConstraint Match method, e.g.

httpContext = null
route = {Microsoft.AspNetCore.Routing.NullRouter)

In 2.1

 httpContext = {Microsoft.AspNetCore.Http.DefaultHttpContext}
 route = {{lang:lang}/{controller=Home}/{action=Index}/{id?}}

The only difference I made between 2.1 and 2.2 is that I changed

var builder = new ConfigurationBuilder()
   .SetBasePath(Directory.GetCurrentDirectory())
   .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

to the following (due to https://github.com/aspnet/AspNetCore/issues/4206)

 var builder = new ConfigurationBuilder()
    .SetBasePath(hostingEnvironment.ContentRootPath) // using IHostingEnvironment 
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

Any ideas?

Update According to https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-2.2#parameter-transformer-reference ASP.NET Core 2.2 uses EndpointRouting whereas 2.1 uses IRouter basic logic. That explains my problem. Now, my question would then what will code look like for 2.2 to use the new EndpointRouting?

3

3 Answers

7
votes
// Use the routing logic of ASP.NET Core 2.1 or earlier:
services.AddMvc(options => options.EnableEndpointRouting = false)
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
4
votes

Differences from earlier versions of routing explains what is happening here (emphasis mine):

The link generation ambient value invalidation algorithm behaves differently when used with endpoint routing.

Ambient value invalidation is the algorithm that decides which route values from the currently executing request (the ambient values) can be used in link generation operations. Conventional routing always invalidated extra route values when linking to a different action. Attribute routing didn't have this behavior prior to the release of ASP.NET Core 2.2. In earlier versions of ASP.NET Core, links to another action that use the same route parameter names resulted in link generation errors. In ASP.NET Core 2.2 or later, both forms of routing invalidate values when linking to another action.

...

Ambient values aren't reused when the linked destination is a different action or page.

In your example, lang is an ambient value and so it is not being reused when going from Home/Index to Home/About (different action). Without a value specified for lang, there is no matching action and so an empty href is generated. This is also described in the docs as an endpoint-routing difference:

However, endpoint routing produces an empty string if the action doesn't exist. Conceptually, endpoint routing doesn't assume that the endpoint exists if the action doesn't exist.

If you want to continue to use endpoint routing, it looks like you're going to need to pass the lang value from your controller into your view and then set it explicitly. Here's an example:

public class HomeController : Controller
{
    public IActionResult Index(string lang)
    {
        ViewData["lang"] = lang; // Using ViewData just for demonstration purposes.
        return View();
    }
}
<a asp-controller="Home" asp-action="Contact"
    asp-route-lang="@ViewData["lang"]">@Res.ContactUs</a>

You can make this a little less repetitive with e.g. an Action Filter, but the concepts are still the same. I can't see that there's another way to handle this (e.g. being able to mark a specific value as being ambient), but perhaps someone else will be able to chime in on that.

1
votes

You need to explicitly pass the values from the route data:

@using Microsoft.AspNetCore.Routing;
<a ... asp-route-storeId="@this.Context.GetRouteValue("storeId")">Pay Button</a>