ASP. Routing in net core

Posted by lordvader on Thu, 03 Mar 2022 21:12:01 +0100

This article is a personal note based on the official documents. Some simple contents are useless. Please refer to the official documents: https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/routing?view=aspnetcore-5.0

Definition: routing is the executable endpoint (code processing unit) responsible for matching the incoming http request and then sending it to the application.

This article only introduces the lower level routing information. For the routing in MVC and Razor, please refer to:
https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0
https://docs.microsoft.com/zh-cn/aspnet/core/razor-pages/razor-pages-conventions?view=aspnetcore-5.0

1. Basic knowledge of routing

Routing is a pair of middleware registered by UseRouting and UseEndPoints:

  • UseRouting adds a routing configuration to the pipeline. This middleware will view the set of endpoints defined in the application and select the best configuration according to the request.
  • UseEndpoints adds an endpoint execution to the pipeline. It executes the delegate associated with the selected endpoint.
 app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });

The above code MapGet means that the delegate is executed when it is a get request and the requested root URL. If the requested method is not get or the requested is not the root URL, no routing configuration returns 404

1.1 endpoint

The MapGet method is used to define the endpoint. The endpoint can choose to run the delegate execution request by matching the URL and HTTP method. Methods similar to MapGet include:

  • Map,MapDelete,MapGet,MapPut,MapPost,MapHealthChecks
  • MapRazorPages: razorpages for
  • MapController: for controller
  • Maphub < Thub >: for SingalR
  • Mapgrpcservice < tservice >: for gRpc

1.2 routing template

/ hello/{name:alpha} in the following code is a routing template, which matches a route similar to / hello/jim format. Alpha is called routing constraint, which means that the name attribute should be alphabetic, so it will not match / hello/123

endpoints.MapGet("/hello/{name:alpha}",async c=>
{
    var name=c.Request.RouteValues["name"];
})

Introduction to common routing templates:

Routing templateExample matching URIRequest URI
hello/helloMatch only a single path / hello.
hello/{name:minlength(4)}/hello/jim mismatch
/hello/jimm match
name minimum length 4
files/{filename}.{ext?}/files/a.txt
/files/a
Because that point is optional, it can match two
{Page=Home}/Match and set the Page to the default value Home.
{Page=Home}/ContactMatch and set the Page to Contact.
{controller}/{action}/{id?}/Products/ListMap to Products controller and List operation.
{controller}/{action}/{id?}/Products/Details/123Map to the Products controller and the Details operation and set the id to 123.
{controller=Home}/{action=Index}/{id?}/Map to Home controller and Index method. id will be ignored.
{controller=Home}/{action=Index}/{id?}/ProductsMap to Products controller and Index method. id will be ignored.

1.3 routing constraints

Routing constraints cannot be equated with input validation because routes that do not meet the constraints directly return 404

constraintExampleMatch exampleexplain
int{id:int}123456789, -123456789Match any integer
bool{active:bool}true, FALSEMatch true or false. Case insensitive
datetime{dob:datetime}2016-12-31, 2016-12-31 7:32pmMatches a valid DateTime value in a fixed culture. See previous warnings.
decimal{price:decimal}49.99, -1,000.01The validity of decimal in the matching region. See previous warnings.
double{weight:double}1.234, -1,001.01e8Matches a valid double value in a fixed culture. See previous warnings.
float{weight:float}1.234, -1,001.01e8Matches a valid float value in a fixed culture. See previous warnings.
guid{id:guid}CD2C1638-1638-72D5-1638-DEADBEEF1638Match valid Guid value
long{ticks:long}123456789, -123456789Match valid long value
minlength(value){username:minlength(4)}RickThe string must be at least 4 characters
maxlength(value){filename:maxlength(8)}MyFileThe string cannot exceed 8 characters
length(length){filename:length(12)}somefile.txtThe string must be exactly 12 characters
length(min,max){filename:length(8,16)}somefile.txtThe string must be at least 8 characters and no more than 16 characters
min(value){age:min(18)}19The integer value must be at least 18
max(value){age:max(120)}91The integer value must not exceed 120
range(min,max){age:range(18,120)}91The integer value must be at least 18 and not more than 120
alpha{name:alpha}RickThe string must consist of one or more alphabetic characters, a-z, and be case sensitive.
regex(expression){ssn:regex(^\d{{3}}-\d{{2}}-\d{{4}}$)}123-45-6789The string must match the regular expression. See Tips for defining regular expressions.
required{name:required}RickUsed to force the existence of non parameter values during URL generation
Combination of multiple constraints{id:int:min(1)}2The minimum integer is 1
Route parameter conversion{path:slugify}The path is converted through the sluify class

1.3.1 custom routing constraints

This is rarely necessary. When model binding cannot meet your requirements, you can implement IRouteConstraint interface to create custom routing constraints

1.4 route parameter conversion

Usage scenario: / subscription management / get all url can be matched to subscriptionmanagementcontrol On the getall method.
First, define the route

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

Then write a class to implement IOutboundParameterTransformer interface:

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();
    }
}

Finally, configure in startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddRouting(options =>
    {
        options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
    });
}

When using URL When an action ("getall", "subscriptionmangement") generates a path, it will also generate / subscription management / get all

1.5 relationship between other middleware and Routing and EndPoint

//Because app has not been called yet Userouting, so it is always null here
app.Use(next => context =>
{
    Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});

app.UseRouting();

//If the url you requested matches an EndPoint, null will not be returned here
app.Use(next => context =>
{
    var point=context.GetEndpoint();
    //The metadata following the endpoint can be obtained for processing
    var cls=point?.Metadata.GetMetadata<Class>();
    Console.WriteLine($"2. Endpoint: {point?.DisplayName ?? "(null)"}");
    
    return next(context);
});

app.UseEndpoints(endpoints =>
{
    // Match the root Url. If it is matched, the whole pipeline ends here and subsequent code will not be executed
    endpoints.MapGet("/", context =>
    {
        Console.WriteLine(
            $"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
        return Task.CompletedTask;
    }).WithDisplayName("Hello").WithMetadata(new Class());//Class object with metadata
});

//If the url you requested does not match any EndPoint, it will be executed here
//So either no or null is output here
app.Use(next => context =>
{
    Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
    return next(context);
});
  • The middleware can be executed before UseRouting to modify the data of routing operations, such as UseRewriter, UseHttpMethodOverride, UsePathBase
  • The middleware can run between UseRouting and UseEndPoints to process the routing metadata before executing the endpoint, such as UseAuthorization and usecos.

1.6 matching between route and Host

Usage scenario: for a url from the request, the Host field in the request header should conform to a domain name rule. For example:

  1. The host of the requested root path can only be Contoso Com or adventure works com. The port number of the host requesting the health check can only be 8080
public void Configure(IApplicationBuilder app)
{
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", context => context.Response.WriteAsync("Hi Contoso!"))
            .RequireHost("contoso.com");
        endpoints.MapGet("/", context => context.Response.WriteAsync("AdventureWorks!"))
            .RequireHost("adventure-works.com");
        endpoints.MapHealthChecks("/healthz").RequireHost("*:8080");
    });
}
  1. The host requested to a controller can only be Contoso Com or adventure works com. However, the host requesting Privacy is an exception. It can only be example com:8080.
[Host("contoso.com", "adventure-works.com")]
public class ProductController : Controller
{
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
    [Host("example.com:8080")]
    public IActionResult Privacy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

1.7 routing performance

When an application has a performance problem, if the developer eliminates the code logic problem, it is generally considered to be a routing problem. But the most common root cause is the poor performance of custom middleware. The following demonstrates how to test the execution time of a middleware:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();
        logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });
    app.UseRouting();
    app.Use(next => async context =>
    {
        var sw = Stopwatch.StartNew();
        await next(context);
        sw.Stop();
        logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
    });
}

The performance of UseRouting middleware is measured by subtracting Time2 from Time1. The above code can also be optimized as follows:

public sealed class MyStopwatch : IDisposable
{
    ILogger<Startup> _logger;
    string _message;
    Stopwatch _sw;
    public MyStopwatch(ILogger<Startup> logger, string message)
    {
        _logger = logger;
        _message = message;
        _sw = Stopwatch.StartNew();
    }
    private bool disposed = false;
    public void Dispose()
    {
        if (!disposed)
        {
            _logger.LogInformation("{Message }: {ElapsedMilliseconds}ms",_message, _sw.ElapsedMilliseconds);
            disposed = true;
        }
    }
}
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    int count = 0;
    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });
    app.UseRouting();
    app.Use(next => async context =>
    {
        using (new MyStopwatch(logger, $"Time {++count}"))
        {
            await next(context);
        }
    });
}

Topics: ASP.NET .NET