ASP.NET Core API: Add custom route token resolver

Viewed 740

I'm using https://github.com/ardalis/ApiEndpoints (one action per controller) for my project, and I run into issue that [Route("[controller]")] is not really suitable for me since controllers look like this:

image

I need something more like [Route("[namespace]")], but that's not supported in ASP.NET Core.

Is there a way, to add custom route token resolution in Startup.cs?

My solutions so far:

  • Hardcode routes
  • Create custom attribute that would contain route with custom tokens, and source generator that would resolve custom tokens and generate Route attribute.
3 Answers

Big thanks to @Kahbazi for pointing me in the right direction!

Here's what I came up with:

private class CustomRouteToken : IApplicationModelConvention
{
    private readonly string _tokenRegex;
    private readonly Func<ControllerModel, string?> _valueGenerator;

    public CustomRouteToken(string tokenName, Func<ControllerModel, string?> valueGenerator)
    {
        _tokenRegex = $@"(\[{tokenName}])(?<!\[\1(?=]))";
        _valueGenerator = valueGenerator;
    }

    public void Apply(ApplicationModel application)
    {
        foreach (var controller in application.Controllers)
        {
            string? tokenValue = _valueGenerator(controller);
            UpdateSelectors(controller.Selectors, tokenValue);
            UpdateSelectors(controller.Actions.SelectMany(a => a.Selectors), tokenValue);
        }
    }

    private void UpdateSelectors(IEnumerable<SelectorModel> selectors, string? tokenValue)
    {
        foreach (var selector in selectors.Where(s => s.AttributeRouteModel != null))
        {
            selector.AttributeRouteModel.Template = InsertTokenValue(selector.AttributeRouteModel.Template, tokenValue);
            selector.AttributeRouteModel.Name = InsertTokenValue(selector.AttributeRouteModel.Name, tokenValue);
        }
    }

    private string? InsertTokenValue(string? template, string? tokenValue)
    {
        if (template is null)
        {
            return template;
        }

        return Regex.Replace(template, _tokenRegex, tokenValue);
    }
}

Configure the token in Startup.cs (this can be wrapped in an extension method):

services.AddControllers(options => options.Conventions.Add(
    new CustomRouteToken(
        "namespace",
        c => c.ControllerType.Namespace?.Split('.').Last()
    ));

After that custom token can be used for routing:

[ApiController]
[Route("api/[namespace]")]
public class Create : ControllerBase {}
[ApiController]
public class Get : ControllerBase 
{
    [HttpGet("api/[namespace]/{id}", Name = "[namespace]_[controller]")]
    public ActionResult Handle(int id) {}
}

You can achieve that with implementing IApplicationModelConvention. more info on here : Custom routing convention

public class NamespaceRoutingConvention : IApplicationModelConvention
{
    public void Apply(ApplicationModel application)
    {
        foreach (var controller in application.Controllers)
        {
            controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel()
            {
                Template = controller.ControllerType.Namespace.Replace('.', '/') + "/[controller]}"
            };
            
        }
    }
}

And then add it in startup.cs

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

Areas are exactly what you need. As mentioned in .net core documentation:

Areas are an ASP.NET feature used to organize related functionality into a group as a separate:

  • Namespace for routing.
  • Folder structure for views and Razor Pages.

To use area routes you need only to add them to the startup.

Like this:

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "MyArea",
        pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

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

After that the route will follow your folder structure inside the Areas folder. For more information visit the Areas in ASP.NET Core documentation.

Related