Skip to content

Blazor Custom Route Constraints #28938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
GravlLift opened this issue Dec 30, 2020 · 23 comments
Open

Blazor Custom Route Constraints #28938

GravlLift opened this issue Dec 30, 2020 · 23 comments
Labels
affected-few This issue impacts only small number of customers area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future feature-routing Pillar: Complete Blazor Web severity-minor This label is used by an internal tool

Comments

@GravlLift
Copy link

I am trying to use custom route constraint in Blazor, similar to how it currently works for both razor pages and MVC routes:

services.AddRouting(options =>
{
    options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
});

... but it appears that blazor route constraints do not consider any custom constraints that have been registered:

private static RouteConstraint? CreateRouteConstraint(string constraint)
{
switch (constraint)
{
case "bool":
return new TypeRouteConstraint<bool>(bool.TryParse);
case "datetime":
return new TypeRouteConstraint<DateTime>((string str, out DateTime result)
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
case "decimal":
return new TypeRouteConstraint<decimal>((string str, out decimal result)
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
case "double":
return new TypeRouteConstraint<double>((string str, out double result)
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
case "float":
return new TypeRouteConstraint<float>((string str, out float result)
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
case "guid":
return new TypeRouteConstraint<Guid>(Guid.TryParse);
case "int":
return new TypeRouteConstraint<int>((string str, out int result)
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
case "long":
return new TypeRouteConstraint<long>((string str, out long result)
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
default:
return null;
}

So I instead encounter the ArgumentException, "Unsupported constraint '{constraint}' in route '{template}'.".

It would be ideal if RouteConstraint could be modified to reference RouteOptions.ContraintMap as the other routing engines do. Alternatively, needing to use some Blazor-specific registration method to register constraints only for Blazor would also be acceptable.

@mkArtakMSFT mkArtakMSFT added area-blazor Includes: Blazor, Razor Components feature-routing labels Dec 30, 2020
@pranavkm pranavkm added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Dec 30, 2020
@ghost
Copy link

ghost commented Jan 5, 2021

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.

@dotnet dotnet deleted a comment Jan 5, 2021
@SteveSandersonMS SteveSandersonMS added affected-few This issue impacts only small number of customers severity-minor This label is used by an internal tool labels Jan 26, 2021 — with ASP.NET Core Issue Ranking
@javiercampos
Copy link

Extensible route constraints should be a must (and I thought that was already solved in asp.net ~12/13 years ago).
The SetParametersAsync parsing "hack" might work for some very contrived scenarios... but it completely disallows generalization, and even forbids doing certain scenarios without many more hacks...

Just imagine some route like: /users/{id}/{action:createOrEditConstraint} where you want one page to resolve both /create and /edit, but you want other different page to resolve /list (or others). Since you can't have user-defined constraints, /users/{id}/{action} matches both pages and you'll get an error.

Yes, it could be solved by creating -yet another component- and either redirect or include the create/edit component and list component in it, but that's yet another hack, and a ton more of boilerplate (one more component, with all its initialization and parsing per "CRUD" entity in a project).

One more scenario (which I used in razor pages/webapi/MVC a lot) is having short guids as parameters (while internally using Guid). Yes, you can just have a string and parse it on SetParametersAsync, but you can't generalize that (on a base class, for example), because you wouldn't have access to the inherited class parameter, so you basically need to specify the SetParameterAsync method and parse specifically on every single routable view with a short guid parameter (and again, since you can't constraint it -with a regex or something-, you can't distinguish it from other string routes).

Again, nothing that can't be "hacked away", but this is basic, and I'm sure many people (even those not on this thread) would miss this, so I believe affected-few doesn't look like a correct label here, to me.

@javiercn javiercn added the feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future label Apr 20, 2021
@Tragen
Copy link

Tragen commented Sep 22, 2021

After 2 hours of debugging why my ulong constraint isn't working, I stumbled upon this bug.
For me it looks like a very basic feature. Please fix this soon.
Also ulong and uint should be added to the default constraints.

@Alerinos
Copy link

I believe this should be introduced in ASP.NET Core 7

@pranavkm pranavkm modified the milestones: Backlog, .NET 7 Planning Nov 18, 2021
@Alerinos
Copy link

@pranavkm I would like to remind you of this, it is quite an important and useful thing.

@kyletinsley
Copy link

Regex route constraints would be wonderful. Trying to constrain a string route parameter to a finite list of string values and want to 404 otherwise.

@TehGM
Copy link

TehGM commented Feb 28, 2023

It'd be super useful if you depend on enum in routes, too. It's a case in my project https://stalcraftclan.com - and currently I have to work it around by grabbing string and manually trying to parse the value. It results in a lot of boilerplate code and is also quite error-prone.

It's supported in other ASP.NET Core areas, I don't see why it couldn't be in Blazor.

@haefele
Copy link

haefele commented Apr 14, 2023

This would be really really useful to have, for example when I want to allow just a couple string values (a enum basically).
Right now it's very tedious and annoying code to write.

@mkArtakMSFT mkArtakMSFT added this to the Backlog milestone Jun 29, 2023
@ghost
Copy link

ghost commented Jun 29, 2023

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.

@kcabral817
Copy link

It appears that folks are tone deaf. This is a requested requirement and now it appears not even 'short' is supported. I fail to understand why foundational issues like this keep getting put to the back burner.

@akhozov
Copy link

akhozov commented Sep 26, 2023

Hello,
I've faced with the same situation.
I need to customize routing constraint and have no ways to do this.
Is it possible to up priority of this feature request?

@Tragen
Copy link

Tragen commented Sep 26, 2023

This ticket is nearly 3 years old and a very basic feature. I wouldn't bet they will fix this anytime soon.

@ghost
Copy link

ghost commented Dec 19, 2023

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.

@StephenWBertrand
Copy link

yep for sure would like this feature. Looking to be able to have locale codes embedded into urls as way to support direct links for desired languages etc.

domain.com/{locale?}/myPage
domain.com/myPage
domain.com/en-US/myPage
domain.com/es-US/myPage

@ax-meyer
Copy link

I just spent a few hours trying to figure out why my constraint doesn't work... at least now I know.

But please, support such a basic feature. I am very suprised it is not supported for blazor.

@AndreyZ1
Copy link

here is working solution for ulong data type, works fine:

Route constraint:

public class UlongRouteConstraint : IRouteConstraint
{
    public static string UlongRouteConstraintName = "ulong";

    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        if (routeKey == null)
        {
            throw new ArgumentNullException(nameof(routeKey));
        }

        if (values == null)
        {
            throw new ArgumentNullException(nameof(values));
        }

        if (!values.TryGetValue(routeKey, out var routeValue) || routeValue == null)
        {
            return false;
        }
        if (routeValue is ulong)
        {
            return true;
        }

        var valueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
        var parsed = ulong.TryParse(valueString, out var value);
        if (parsed)
        {
            values[routeKey] = value;
        }

        return parsed;
    }
}

Register:

builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add(UlongRouteConstraint.UlongRouteConstraintName, typeof(UlongRouteConstraint));
});

Use:

@page "/user/servers/{ServerId:ulong}/{ProductId:ulong}/details"

...


[Parameter, EditorRequired]
public ulong ServerId { get; set; }

[Parameter, EditorRequired]
public ulong ProductId { get; set; }

@zorgoz
Copy link

zorgoz commented Feb 3, 2025

It looked promising, and I am trying this approach (works in server-side WebApi), but in my Blazor WASM I am getting an exception:

Unhandled exception rendering component: The constraint entry 'parameter1' - 'ulid' on the route '/path/{parameter1:ulid}/{parameter2:ulid}' could not be resolved by the constraint resolver of type 'DefaultInlineConstraintResolver'.
System.InvalidOperationException: The constraint entry 'parameter1' - 'ulid' on the route '/path/{parameter1:ulid}/{parameter2:ulid}' could not be resolved by the constraint resolver of type 'DefaultInlineConstraintResolver'.

@page "/path/{parameter1:ulid}/{parameter2:ulid}"

Even though I have:

public class UlidRouteConstraint : IRouteConstraint
{
    public const string ConstraintName = "ulid";

    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out var value) && value is string stringValue)
        {
            return Ulid.TryParse(stringValue, out _);
        }
        return false;
    }
}

and

builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add(UlidRouteConstraint.ConstraintName, typeof(UlidRouteConstraint));
});

@AndreyZ1
Copy link

AndreyZ1 commented Feb 3, 2025

It looked promising, and I am trying this approach (works in server-side WebApi), but in my Blazor WASM I am getting an exception:

Unhandled exception rendering component: The constraint entry 'parameter1' - 'ulid' on the route '/path/{parameter1:ulid}/{parameter2:ulid}' could not be resolved by the constraint resolver of type 'DefaultInlineConstraintResolver'.
System.InvalidOperationException: The constraint entry 'parameter1' - 'ulid' on the route '/path/{parameter1:ulid}/{parameter2:ulid}' could not be resolved by the constraint resolver of type 'DefaultInlineConstraintResolver'.

@page "/path/{parameter1:ulid}/{parameter2:ulid}"

Even though I have:

public class UlidRouteConstraint : IRouteConstraint
{
    public const string ConstraintName = "ulid";

    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out var value) && value is string stringValue)
        {
            return Ulid.TryParse(stringValue, out _);
        }
        return false;
    }
}

and

builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add(UlidRouteConstraint.ConstraintName, typeof(UlidRouteConstraint));
});

are you sure you registered RouteOptions for WASM project as well?

@zorgoz
Copy link

zorgoz commented Feb 3, 2025

Absolutely, That's from my actual code. I have put a breakpoint at the options.ConstraintMap.Add and it is never hit.

@AndreyZ1
Copy link

AndreyZ1 commented Feb 3, 2025

Absolutely, That's from my actual code. I have put a breakpoint at the options.ConstraintMap.Add and it is never hit.

Can not help you here.
As long as server and client knows how to handle custom type - it should work

@zorgoz
Copy link

zorgoz commented Feb 3, 2025

Absolutely, That's from my actual code. I have put a breakpoint at the options.ConstraintMap.Add and it is never hit.

Can not help you here. As long as server and client knows how to handle custom type - it should work

You can confirm that you are using in a Blazor WebAssembly standalone (not interactive WebAssembly ) and it is working for you?

@AndreyZ1
Copy link

AndreyZ1 commented Feb 3, 2025

Absolutely, That's from my actual code. I have put a breakpoint at the options.ConstraintMap.Add and it is never hit.

Can not help you here. As long as server and client knows how to handle custom type - it should work

You can confirm that you are using in a Blazor WebAssembly standalone (not interactive WebAssembly ) and it is working for you?

Currently I don't have solution running anymore, but I used it in WASM client with .net Core app as a server.

@danroth27 danroth27 modified the milestones: Backlog, .NET 10 Planning Feb 5, 2025
@scss-sconnors
Copy link

scss-sconnors commented May 6, 2025

Above solution doesnt seem to work if setting the rendermode on Routes.razor to InteractiveServer and adding additional assemblies via the router component

e.g.

<head>
    <HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
    <Routes @rendermode="InteractiveServer" />
</body>
<Router
    AppAssembly="..."
    AdditionalAssemblies="[ typeof(BlazorSample.Client._Imports).Assembly ]">
</Router>

It seems in interactive mode, even with constraints defined in program.cs, the router component seems to set up a brand new DefaultInlineConstraintResolver, discarding our custom-mapped constaints.

Is there anyway around this huge limitation (Besides removing all custom constraints and verifying the validity of the passed argument within onparametersset? I just want the app to display a loading spinner instead of pre-rendering the entire app just for it to be thrown away seconds later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
affected-few This issue impacts only small number of customers area-blazor Includes: Blazor, Razor Components enhancement This issue represents an ask for new feature or an enhancement to an existing one feature-blazor-builtin-components Features related to the built in components we ship or could ship in the future feature-routing Pillar: Complete Blazor Web severity-minor This label is used by an internal tool
Projects
None yet
Development

No branches or pull requests