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 template | Example matching URI | Request URI |
---|---|---|
hello | /hello | Match 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} | /Contact | Match and set the Page to Contact. |
{controller}/{action}/{id?} | /Products/List | Map to Products controller and List operation. |
{controller}/{action}/{id?} | /Products/Details/123 | Map 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?} | /Products | Map 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
constraint | Example | Match example | explain |
---|---|---|---|
int | {id:int} | 123456789, -123456789 | Match any integer |
bool | {active:bool} | true, FALSE | Match true or false. Case insensitive |
datetime | {dob:datetime} | 2016-12-31, 2016-12-31 7:32pm | Matches a valid DateTime value in a fixed culture. See previous warnings. |
decimal | {price:decimal} | 49.99, -1,000.01 | The validity of decimal in the matching region. See previous warnings. |
double | {weight:double} | 1.234, -1,001.01e8 | Matches a valid double value in a fixed culture. See previous warnings. |
float | {weight:float} | 1.234, -1,001.01e8 | Matches a valid float value in a fixed culture. See previous warnings. |
guid | {id:guid} | CD2C1638-1638-72D5-1638-DEADBEEF1638 | Match valid Guid value |
long | {ticks:long} | 123456789, -123456789 | Match valid long value |
minlength(value) | {username:minlength(4)} | Rick | The string must be at least 4 characters |
maxlength(value) | {filename:maxlength(8)} | MyFile | The string cannot exceed 8 characters |
length(length) | {filename:length(12)} | somefile.txt | The string must be exactly 12 characters |
length(min,max) | {filename:length(8,16)} | somefile.txt | The string must be at least 8 characters and no more than 16 characters |
min(value) | {age:min(18)} | 19 | The integer value must be at least 18 |
max(value) | {age:max(120)} | 91 | The integer value must not exceed 120 |
range(min,max) | {age:range(18,120)} | 91 | The integer value must be at least 18 and not more than 120 |
alpha | {name:alpha} | Rick | The 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-6789 | The string must match the regular expression. See Tips for defining regular expressions. |
required | {name:required} | Rick | Used to force the existence of non parameter values during URL generation |
Combination of multiple constraints | {id:int:min(1)} | 2 | The 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:
- 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"); }); }
- 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); } }); }