We are upgrading an ASP.NET Core 2.2. MVC app to 3.0 but Url.RouteUrl gives blank href's and inconsistency on link creation. What are we doing wrong?
Steps to reproduce the behavior:
Example 1 -wrong result
Returns https://example.org/us/blog/some-title-6 in 2.2 but is blank in 3.0:
string url = Url.RouteUrl("blog-details", new { title = item.Title, id = item.Id });
[Route("~/{lang}/blog/{title}-{id}", Name= "blog-details")]
public async Task<IActionResult> Details(string title, int id)
{
}
We have tried these constructions but result is not satisfactory:
<a asp-action="Details" asp-controller="Blog" asp-route-title="item.Title" asp-route-id="@item.Id">Link here</a>
url = Url.Action("Details", "Blog", new { id = item.Id, title = item.Title });
url = Url.RouteUrl(new { action = "Details", controller = "Blog", id = item.Id, title = item.Title });
// Returns https://example.org/us/blog/details/6?title=some-title
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
...
services.AddControllersWithViews(options =>
{
options.Filters.Add(new MiddlewareFilterAttribute(typeof(LocalizationPipeline)));
})
.AddViewLocalization(LanguageViewLocationExpanderFormat.SubFolder)
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
services.AddRazorPages();
services.AddRouting(options =>
{
options.ConstraintMap.Add("lang", typeof(LanguageRouteConstraint));
options.LowercaseUrls = true;
options.AppendTrailingSlash = false;
});
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseResponseCompression();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers(); // attribute routing
endpoints.MapControllerRoute("areas", "{lang:lang}/{area:exists}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("LocalizedDefault", "{lang:lang}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("Naked", "/{controller=Home}/{action=Index}");
endpoints.MapHealthChecks("/health");
});
}
Example 2 - wrong result
Returns https://example.org/us/home/pricing instead of
https://example.org/us/pricing
<a asp-controller="Home" asp-action="Pricing">Pricing</a>
[Route("~/{lang}/pricing")]
public async Task<IActionResult> Pricing()
{
...
}
Example 3 - correct result
Returns correct https://example.org/us/signup/customer
<a asp-controller="Signup" asp-action="Customer">Sign up</a>
[Route("~/{lang}/signup/customer")]
public IActionResult Customer()
{
...
}
dotnet --info
.NET Core SDK (reflecting any global.json):
Version: 3.0.100
Commit: 04339c3a26
Runtime Environment:
OS Name: Windows
OS Version: 10.0.18362
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\3.0.100\
Host (useful for support):
Version: 3.0.0
Commit: 7d57652f33
.NET Core SDKs installed:
2.1.202 [C:\Program Files\dotnet\sdk]
2.1.502 [C:\Program Files\dotnet\sdk]
2.1.504 [C:\Program Files\dotnet\sdk]
2.1.505 [C:\Program Files\dotnet\sdk]
2.1.507 [C:\Program Files\dotnet\sdk]
2.1.602 [C:\Program Files\dotnet\sdk]
2.1.604 [C:\Program Files\dotnet\sdk]
2.1.700 [C:\Program Files\dotnet\sdk]
2.1.801 [C:\Program Files\dotnet\sdk]
2.1.802 [C:\Program Files\dotnet\sdk]
2.2.100 [C:\Program Files\dotnet\sdk]
2.2.102 [C:\Program Files\dotnet\sdk]
2.2.104 [C:\Program Files\dotnet\sdk]
2.2.105 [C:\Program Files\dotnet\sdk]
2.2.203 [C:\Program Files\dotnet\sdk]
2.2.301 [C:\Program Files\dotnet\sdk]
3.0.100 [C:\Program Files\dotnet\sdk]
.NET Core runtimes installed:
Microsoft.AspNetCore.All 2.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.All 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.12 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 2.2.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
@shapeh Thanks for contacting us.
In order for us to investigate this issue, could you please provide a link to a github repo with a minimal repro project and detailed steps to reproduce the issue?
@javiercn Thanks for getting back so quickly. It is impossible for me to do a github repo given the size of the project + confidentiality.
We have a LocalizationPipeline.cs
middleware responsible for reading a list of languages in appsettings.json and turning that in correct URL - e.g. "en-gb" will be mapped "/uk" in URL {lang}
param.
using System.Collections.Generic;
using System.Globalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Localization;
using example.Helpers;
using Microsoft.Extensions.Configuration;
using System.IO;
namespace example
{
public class LocalizationPipeline
{
public void Configure(IApplicationBuilder app)
{
var options = new RequestLocalizationOptions();
ConfigureOptions(app, options);
app.UseRequestLocalization(options);
}
public static void ConfigureOptions(IApplicationBuilder app, RequestLocalizationOptions options)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
IConfigurationRoot configuration = builder.Build();
AppLanguages languageSettings = new AppLanguages();
GeneralConfiguration generalConfiguration = new GeneralConfiguration();
configuration.GetSection("AppLanguages").Bind(languageSettings);
configuration.GetSection("GeneralConfiguration").Bind(generalConfiguration);
var supportedCultures = new List<CultureInfo>();
foreach (Language lang_in_app in languageSettings.Dict.Values)
{
supportedCultures.Add(new CultureInfo(lang_in_app.Culture));
}
options.DefaultRequestCulture = new RequestCulture(culture: "en-gb", uiCulture: "en-gb");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[] {
new RouteDataRequestCultureProvider(languageSettings)
{
Options = options,
RouteDataStringKey = generalConfiguration.RouteDataStringKey
}
};
app.UseRequestLocalization(options);
}
}
}
And this is the LanguageRouteConstraint.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using example.Helpers;
using System.IO;
namespace example
{
public class LanguageRouteConstraint : IRouteConstraint
{
private readonly AppLanguages _languageSettings;
public LanguageRouteConstraint()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.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;
}
}
}
@shapeh we are not looking for the whole app, rather a minimalistic repro - trimmed down to only the changes on top of a new project template, which demonstrates the issue.
@mkArtakMSFT Got it / will see what I can do and revert :)
@shapeh we're closing this issue as we haven't heard back from you for some time now. Feel free to comment/reopen this issue when you have a repro available.
Sorry about the late response - this is still open / I need just to free up an hour to do the repro.
I'm experiencing this issue as well in cloudscribe SimpleContent. I'm using named routes that expect a culture route constraint:
routes.MapControllerRoute(
name: "pageedit-culture",
pattern: "{culture}/editpage/{slug?}"
, defaults: new { controller = "Page", action = "Edit" }
, constraints: new { culture = cultureConstraint }
);
but when I'm on a page like /fr/about which is the view url for a cms page, the culture parameter from the current url is not picked up when generating a link to the edit page like this:
model.EditPath = Url.RouteUrl("pageedit-culture", new { slug = model.CurrentPage.Slug });
the model.EditPath ends up as null, whereas before 3.0 it would have been /fr/editpage/about
@joeaudette can you please provide a minimalistic repro (ideally GitHub hosted) so we can investigate this further?
@mkArtakMSFT cloudscribe is a pretty complex setup with route constraints for folder multi-tenancy and culture, I'm not sure I can create a small repro of the issue.
Endpoint routing was a major rewrite of the routing system so this is surely a bug. I hope you can investigate it on your end based on the description of the problem and not close the issue if I don't make a small repro for you.
For now I'm going to have to disable endpoint routing and go back to traditional mvc routing as a workaround, maybe I'll try endpoint routing again after you guys ship the next version or patch.
Talked to @rynowak regarding this and this is and this is one of the differences Endpoint Routing has in comparison to routing in 2.2. Specifically, Endpoint routing doesn't preserve ambient values when generating links to other actions. We'll look into solving this somehow during 5.0.
My observation is that it does preserve my culture route parameter and constraint when using Url.Action(...), but it does not with Url.RouteUrl(...)
Just to keep this alive and ensure it's known that this is an issue for more than one group of people, we hit this issue today as we attempted to migrate to dotnet core 3.0 and found that the implicit routing didn't work as we expected it to from 2.2. You can see a quick sample we created to see if there were any simple workarounds from having to write out many properties manually for our redirects and internal routing pieces: https://github.com/accu-jmellottlillie/WebRoutingBug
I have the same issue - see my report to Microsoft: https://github.com/aspnet/AspNetCore/issues/17097
I have researched the issue more and found that this is the result of change of algorithm that is used for url generation. The old algorithm did sometimes generate incorrect url so Microsoft changed the algorithm. They use something they call ambient Values for generating URL
You can read about the algoritm here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.0#url-generation-reference
One solution if its possible, is to manually inject Ambient value for culture into the pipeline. I asked Microsoft if that was possible a couple of days ago, but haven't got any answer.
It's a bit strange that this does work:
"sv-se/{action}/{id?}"
This doesn't work:
"{culture}/{action}/{id?}"
We could theoretically create a loop and insert one Route for each culture, but this doesn't feel like a good design pattern.
I have done a HACK / Workaround that may help you until Microsoft come out with a solution.
No guaranties of course :-)
//Startup.cs
EndPointGenerator generator = new EndPointGenerator(new List<string>(){"en-gb",'"sv-se","ru-ru"});
app.UseEndpoints(generator.AddEndPoints);
//Separate EndPointGenerator.cs
public class EndPointGenerator
{
#region Members
private List<string> Cultures { get; set; }
#endregion
private EndPointGenerator() { }
public EndPointGenerator(List<string> cultures)
{
Cultures = cultures;
}
public void AddEndPoints(IEndpointRouteBuilder endpoints)
{
//add routes that shall be BEFORE culture routes
//endpoints.MapControllerRoute(..)
Cultures.ForEach(culture =>
{
//Example, shall contain multiple routes
endpoints.MapControllerRoute(
name: "home_actions",
pattern: $"{culture}/{{action}}/{{id?}}", //
defaults: new { controller = "Home" });
//Next
//endpoints.MapControllerRoute(..)
//Next
//endpoints.MapControllerRoute(..)
});
//add routes that shall be After culture routes
//endpoints.MapControllerRoute(..)
}
}
Just thought I'd weigh in here too as someone who has come across a related issue with routing differences between 2.2 and 3.0. I believe the root cause is the same as this issue is describing.
To cut a long story short, RedirectToAction on 3.0 doesn't seem to resolve the URL if the action that it's redirecting to depends on a shared controller-level parameter. E.g. take this controller as an example:
[Route("redirect/{someIdentifier}")]
public class RedirectController : Controller
{
[HttpGet]
[Route("initroute")]
public IActionResult InitRoute(string someIdentifier)
{
return RedirectToAction("NextRoute", "Redirect");
}
[HttpGet]
[Route("nextroute")]
public IActionResult NextRoute(string someIdentifier)
{
Debug.WriteLine(someIdentifier);
return View();
}
}
In 2.2, if you navigate to the controller at redirect/123/initroute
then it will redirect you correctly to redirect/123/nextroute
, but doing the same with identical code in 3.0 will give you an error saying that there's no matching route.
Doing some digging it appears to be because the IUrlHelper
implementation (found by accessing the Url
property on the controller) is a UrlHelper
in 2.2, but a EndpointRoutingUrlHelper
in 3.0, and these resolve the URLs differently to each other.
Here's a small sample with two web apps, one on 2.2 and one on 3.0 which repros the issue: https://github.com/jtb637/RoutingIssue
Figured I'd add that we ran into this issue when upgrading from 2.2 to 3.1. UrlHelper.RouteUrl, when called with a named attribute route, did not apply ambient values correctly, so a call to url.RouteUrl(name)
returned null while a call to url.RouteUrl(name, new { ambientValue = "valueFromCallingUrl" })
worked.
This was not consistent; some URLs worked, and some didn't, and we couldn't find why some worked. But specifying ambient values worked.
As stated earlier, this is a problem with ambient values not being passed on unless specified explicitly like so:
@using Microsoft.AspNetCore.Routing;
<a ... asp-route-my-ambient-value="@this.Context.GetRouteValue("my-ambient-value")">My link</a>
For us, this is a major problem as we have 500+ URLs generated with either Url.RouteUrl or link. Decorating every single link with ambient values seems the wrong design pattern.
Someone here https://stackoverflow.com/questions/59267971/using-routedatarequestcultureprovider-in-asp-net-core-3-1 created a new taghelper to solve the problem but, this means customization again.
I would like to hear how other people have solved this? Also, MSFT are there any specific plans to mitigate these problems with "ambient-value-decoration"?
Any update on this? I am on core 3.1 and neither Url.Page nor Url.RouteUrl is working.
We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.
I have also run into this issue upgrading to 3.1. We were using the ambient route values in a multi tenant app's admin screen where the tenants config was managed. We have created a tag helper that injects the currently managed tenant id and a custom UrlHelper extension. For this you can access current route values and append them to an action quite easily:
public static string TenantAdminAction(this IUrlHelper url, string action, string controller, object routeValues)
{
var routeValueDictionary = new RouteValueDictionary(routeValues);
routeValueDictionary["tenantId"] = url.ActionContext.RouteData.Values["tenantId"];
return url.Action(action, controller, routeValueDictionary);
}
I noticed that the issue also exists in my API project, when a controller doesn't have a default, GET
action. Adding this bit solved the issue in my case:
[HttpGet] public string Ping() => "Pong";
Most helpful comment
I'm experiencing this issue as well in cloudscribe SimpleContent. I'm using named routes that expect a culture route constraint:
but when I'm on a page like /fr/about which is the view url for a cms page, the culture parameter from the current url is not picked up when generating a link to the edit page like this:
the model.EditPath ends up as null, whereas before 3.0 it would have been /fr/editpage/about