53

ASP.NET Core uses CamelCase-Routes like http://localhost:5000/DashboardSettings/Index by default. But I want to use lowercase routes, which are delimitted by dashes: http://localhost:5000/dashboard-settings/index They're more common and consistent, cause my application extends a website running Wordpress, which also has lowercase urls with dashes.

I learned that I can change the urls to lowercase using the routing-options:

services.ConfigureRouting(setupAction => {
    setupAction.LowercaseUrls = true;
});

This works but gave me urls without any delimiter like http://localhost:5000/dashboardsettings/index which are badly readable. I could define custom routes using the route attribute like

[Route("dashboard-settings")]
class DashboardSettings:Controller {
    public IActionResult Index() {
        // ...
    }
}

But that causes extra-work and is error-prone. I would prefer an automatic solution which search for uppercase chars, insert a dash before them and make the uppercase-char lowercase. For the old ASP.NET this was not a big issue, but on ASP.NET Core I see no direction how to handle this.

Whats the way to do this here? I need some kind of interface where I can generate urls (like for the tag helpers) and replace there the CamelCase by dash-delimiters. Then I need another kind of interface for the routing, so that the dash-delimiter urls are converted back to CamelCase for correct matching with my controller/action names.

2
  • what is the namespace of services pls? and where do you plug in ConfigureRouting? Commented Jun 11, 2019 at 9:44
  • services.ConfigureRouting no longer seems to exist - I think you now use services.AddRouting instead Commented Nov 6, 2019 at 9:19

6 Answers 6

57

Update in ASP.NET Core Version >= 2.2

To do so, first create the SlugifyParameterTransformer class should be as follows:

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        // Slugify value
        return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
    }
}

For ASP.NET Core 2.2 MVC:

In the ConfigureServices method of the Startup class:

services.AddRouting(option =>
{
    option.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});

And route configuration should be as follows:

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

For ASP.NET Core 2.2 Web API:

In the ConfigureServices method of the Startup class:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options => 
    {
        options.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()));
    }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

For ASP.NET Core >=3.0 MVC:

In the ConfigureServices method of the Startup class:

services.AddRouting(option =>
{
    option.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});

and route configuration should be as follows:

app.UseEndpoints(endpoints =>
{
    endpoints.MapAreaControllerRoute(
        name: "AdminAreaRoute",
        areaName: "Admin",
        pattern: "admin/{controller:slugify=Dashboard}/{action:slugify=Index}/{id:slugify?}");

    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller:slugify}/{action:slugify}/{id:slugify?}",
        defaults: new { controller = "Home", action = "Index" });
});

For ASP.NET Core >=3.0 Web API:

In the ConfigureServices method of the Startup class:

services.AddControllers(options => 
{
    options.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()));
});

For ASP.NET Core >=3.0 Razor Pages:

In the ConfigureServices method of the Startup class:

services.AddRazorPages(options => 
{
    options.Conventions.Add(new PageRouteTransformerConvention(new SlugifyParameterTransformer()));
});

This is will make /Employee/EmployeeDetails/1 route to /employee/employee-details/1

Sign up to request clarification or add additional context in comments.

7 Comments

Bazillion likes to you!
Doesn't work for [Route("MyController/MyAction")]. MyController becomes my-controller, but MyAction stays unchanged.
@МаксимКошевой You have added the pure string in your route so you have to write it by yourself. You can write: [Route("my-controller/my-action")] or [Route("[controller]/[action]")]
Oh, so this solution only affects tokens, got it. Whole URL is affected by LowercaseUrls though. Would be great if there would be something for slugify
|
15

A little late to the party here but.. Can do this by implementing IControllerModelConvention.

 public class DashedRoutingConvention : IControllerModelConvention
 {
        public void Apply(ControllerModel controller)
        {
            var hasRouteAttributes = controller.Selectors.Any(selector =>
                                               selector.AttributeRouteModel != null);
            if (hasRouteAttributes)
            {
                // This controller manually defined some routes, so treat this 
                // as an override and not apply the convention here.
                return;
            }

            foreach (var controllerAction in controller.Actions)
            {
                foreach (var selector in controllerAction.Selectors.Where(x => x.AttributeRouteModel == null))
                {
                    var template = new StringBuilder();

                    if (controllerAction.Controller.ControllerName != "Home")
                    {
                        template.Append(PascalToKebabCase(controller.ControllerName));
                    }

                    if (controllerAction.ActionName != "Index")
                    {
                        template.Append("/" + PascalToKebabCase(controllerAction.ActionName));
                    }

                    selector.AttributeRouteModel = new AttributeRouteModel()
                    {
                        Template = template.ToString()
                    };
                }
            }
        }

        public static string PascalToKebabCase(string value)
        {
            if (string.IsNullOrEmpty(value))
                return value;

            return Regex.Replace(
                value,
                "(?<!^)([A-Z][a-z]|(?<=[a-z])[A-Z])",
                "-$1",
                RegexOptions.Compiled)
                .Trim()
                .ToLower();
        }
}

Then registering it in Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc(options => options.Conventions.Add(new DashedRoutingConvention()));
}

Can find more info and example here https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing

1 Comment

It works, but if I have /Home/UrlWithDash action method and UrlWithDashController, throws an Exception (ambiguous reference). So, be careful while naming controllers and actions.
12

I'm using Asp.NetCore 2.0.0 and Razor Pages (no explicit controller necessary), so all that's needed is:

  1. Enable Lowercase Urls:

    services.AddRouting(options => options.LowercaseUrls = true);

  2. Create a file named Dashboard-Settings.cshtml and the resulting route becomes /dashboard-settings

Comments

7

Copied from ASP.NET Core 3.0 (unchanged from 2.2) documentation:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Conventions.Add(new RouteTokenTransformerConvention(
                                     new SlugifyParameterTransformer()));
    });
}

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
    public string TransformOutbound(object value)
    {
        if (value == null) { return null; }

        // Slugify value
        return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
    }
}

3 Comments

For some reason this doesn't work for me, whereas the answer that uses ConstraintMap does
This is the solution that eventually worked for me.
Why does the TransformOutbound method receives an object parameter instead of string?
1

See Docs for the latest way to do it. Here is how you would do it for ASP.NET Core 7.0:

// IMPORTS
using System.Text.RegularExpressions;

namespace YourApi;

public class Program {
    public static void Main(string[] args) {
        // replace builder.Services.AddControllers() with the following
        builder.Services.AddControllersWithViews(options => {
            options.Conventions.Add(new RouteTokenTransformerConvention(
                                         new SlugifyParameterTransformer()));
        });
        // EXISTING CODE
    }
}

// https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing#use-a-parameter-transformer-to-customize-token-replacement
public class SlugifyParameterTransformer : IOutboundParameterTransformer {
    public string? TransformOutbound(object? value) {
        if (value == null) { return null; }
         return Regex.Replace(value.ToString(),
                             "([a-z])([A-Z])",
                             "$1-$2",
                             RegexOptions.CultureInvariant,
                                 TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
    }
}

Comments

0

Thanks for the information, however it's better to filter the selectors, in order to skip those with a custom route template : [HttpGet("/[controller]/{id}")] for example)

foreach (var selector in controllerAction.Selectors
                                         .Where(x => x.AttributeRouteModel == null))

1 Comment

Where are you putting that code?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.