在《注冊URL模式與HttpHandler的映射關系》演示的實例中,我們總是利用一個RouteBuilder對象來為RouterMiddleware中間件創建所需的Router對象,接下來我們就著重來介紹這個對象。RouteBuilder是我們對所有實現了IRouteBuilder接口的所有類型以及對應對象的統稱。[本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、RouteBuilder
二、RouteCollection
三、多個Route共享同一個Handler
四、每個Route具有獨立的Handler
五、擴展方法MapVerb
如下面的代碼片段所示,RouteBuilder對Router對象的創建體現在它的Build方法上。除此之外,IRouteBuilder接口還定義了一系列屬性,我們可以利用它們得到用來注冊中間件的ApplicationBuilder和用來提供服務的ServiceProvider。我們可以將多個Router注冊到RouteBuilder上,這些注冊的Router保存在Routes(不是Routers)屬性上,而DefaultHandler屬性返回一個默認的Router。
1: public interface IRouteBuilder
2: {
3: IApplicationBuilder ApplicationBuilder { get; }
4: IServiceProvider ServiceProvider { get; }
5: IRouter DefaultHandler { get; set; }
6: IList<IRouter> Routes { get; }
7:
8: IRouter Build();
9: }
ASP.NET Core默認使用的是如下一個實現了IRouteBuilder的RouteBuilder類型。如下面的代碼片段所示,它的屬性ApplicationBuilder是調用構造函數時通過相應的參數指定的,作為服務提供者的ServiceProvider則直接來源於這個ApplicationBuilder對象。至於最為核心的Build方法,我們可以看出它返回的實際上是通過注冊的Router對象創建的一個RouteCollection對象。
1: public class RouteBuilder : IRouteBuilder
2: {
3: public IApplicationBuilder ApplicationBuilder { get; }
4: public IServiceProvider ServiceProvider { get; }
5: public IList<IRouter> Routes { get; }
6: public IRouter DefaultHandler { get; set; }
7:
8: public RouteBuilder(IApplicationBuilder applicationBuilder) : this(applicationBuilder, null){}
9:
10: public RouteBuilder(IApplicationBuilder applicationBuilder, IRouter defaultHandler)
11: {
12: this.ApplicationBuilder = applicationBuilder;
13: this.ServiceProvider = applicationBuilder.ApplicationServices;
14: this.DefaultHandler = defaultHandler;
15: this.Routes = new List<IRouter>();
16: }
17:
18: public IRouter Build()
19: {
20: RouteCollection routes = new RouteCollection();
21: foreach (IRouter router in this.Routes)
22: {
23: routes.Add(router);
24: }
25: return routes;
26: }
27: }
一個RouteCollection是一個特殊的Router,因為RouteCollection實現如下了如下這個IRouteCollection接口,後者最終實現了IRouter接口。一個RouteCollection對象實際上是對多個Router對象的封裝,我們可以調用其Add方法添加封裝的Router對象。
1: public interface IRouteCollection : IRouter
2: {
3: void Add(IRouter router);
4: }
為了更能更好的認識RouteCollection,尤其是實現在它的RouteAsync方法中路由解析原理,我們定義了如下這麼一個模擬的類型。如下面的代碼片段所示,當RouteAsync方法被執行的時候,它會遍歷每個注冊的Router對象並將當前RouteContext上下文作為參數調用它們的RouteAsync方法,直到遇到第一個與當前請求相匹配的Router。由於只有路由規則與當前請求相匹配的Router才會去設置RouteContext的Handler,所以判斷Router是否與當前請求匹配的方法很簡單,那就是判斷當前RouteContext的Handler屬性是否為null。
1: public class RouteCollection : IRouteCollection
2: {
3: private readonly List<IRouter> _routes = new List<IRouter>();
4:
5: public void Add(IRouter router)
6: {
7: _routes.Add(router);
8: }
9:
10: public async Task RouteAsync(RouteContext context)
11: {
12: var snapshot = context.RouteData.PushState(null, values: null, dataTokens: null);
13: foreach (var router in _routes)
14: {
15: context.RouteData.Routers.Add(router);
16: try
17: {
18: await router.RouteAsync(context);
19: if (null != context.Handler)
20: {
21: break;
22: }
23: }
24: finally
25: {
26: if (null == context.Handler)
27: {
28: snapshot.Restore();
29: }
30: }
31: }
32: }
33: public IRouter this[int index] => _routes[index];
34: public int Count => _routes.Count;
35: …
36: }
當整個路由解析流程完成之後,最終的RouteData的狀態應該只與那個匹配的Router對象有關。換句話說,對於路由規則與當前請求不匹配的Router來說,針對它們的路由解析過程不應該“污染”最終的這個RouteData對象。為了達到這個目的,上面介紹的關於RouteData的快照機制被應用在這個RouteAsync方法上,上面所示的代碼片段也體現了這一點。
由於RouteBuilder對RouterMiddleware中間件提供的Router對象實際上是一個RouteCollection對象,換句話說這其實是一個由多個Router對象組成的“路由表”。所謂的路由注冊,本質上就是在這個路由表中添加相應的Router對象。RouteBuilder具有若干擴展方法幫助我們以一種很簡潔的方式相這個路由表中添加Router,我們先來介紹如下這四個MapRoute重載。
1: public static class MapRouteRouteBuilderExtensions
2: {
3: public static IRouteBuilder MapRoute(this IRouteBuilder routeBuilder, string name, string template);
4: public static IRouteBuilder MapRoute(this IRouteBuilder routeBuilder, string name, string template, object defaults);
5: public static IRouteBuilder MapRoute(this IRouteBuilder routeBuilder, string name, string template, object defaults, object constraints);
6: public static IRouteBuilder MapRoute(this IRouteBuilder routeBuilder, string name, string template, object defaults, object constraints, object dataTokens);
7: }
上述這四個MapRoute方法執行之後在路由表中添加的都是一個Route對象,這個Route對象的名稱、路由模板、路由參數的默認值和約束和DataToken都是由對應的參數來指定的。我們知道Route對象其實是對另一個Router對象的封裝,那麼被封裝的究竟是個怎樣的Router呢?
1: public static IRouteBuilder MapRoute(this IRouteBuilder routeBuilder, string name, string template, object defaults, object constraints, object dataTokens)
2: {
3: IInlineConstraintResolver requiredService = routeBuilder.ServiceProvider.GetRequiredService<IInlineConstraintResolver>();
4: routeBuilder.Routes.Add(new Route(routeBuilder.DefaultHandler, name, template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), new RouteValueDictionary(dataTokens), requiredService));
5: return routeBuilder;
6: }
對於我們在《注冊URL模式與HttpHandler的映射關系》演示的關於獲取天氣預報信息的實例來說,我們也可以按照如下的形式調用RouteBuilder的MapRoute方法來注冊所需的兩個路由。為了以“流暢”的鏈式編程的方式來甚至RouteBuilder的默認處理器,我們特意定義了如下這個擴展方法SetDefaultHandler。
1: new WebHostBuilder()
2: .ConfigureServices(svcs => svcs.AddRouting())
3: .Configure(app =>app.UseRouter(builder=>builder
4: .SetDefaultHandler(WeatherForecast)
5: .MapRoute("route1", @"weather/{city:regex(^0\d{{2,3}}$)}/{days:int:range(1,4)}")
6: .MapRoute("route2", @"weather/{city:regex(^0\d{{2,3}}$)}/{@date}")))
7: …
8:
9: public static IRouteBuilder SetDefaultHandler(this IRouteBuilder builder, RequestDelegate handler)
10: {
11: builder.DefaultHandler = new RouteHandler(handler);
12: return builder;
13: }
對於上面通過調用MapRoute方法注冊的兩個Route對象來說,我們將路由約束以內聯的形式直接定義在路由模板上,其實我們也可以將路由約束作為MapRoute方法的參數。如下面的代碼片段所示,我們以不僅以參數的形式設置了路由約束,還設置了路由參數的默認值。
1: IRouteConstraint city = new RegexRouteConstraint(@"^0\d{2,3}$");
2: IRouteConstraint days = new CompositeRouteConstraint(new IRouteConstraint[] { new IntRouteConstraint(), new RangeRouteConstraint(1, 4) });
3:
4: new WebHostBuilder()
5: .ConfigureServices(svcs => svcs.AddRouting())
6: .Configure(app => app.UseRouter(builder => builder
7: .SetDefaultHandler(WeatherForecast)
8: .MapRoute(
9: name : "route1",
10: template : @"weather/{city}/{days}",
11: constraints : new { city = city, days = days },
12: defaults : new {city="010", days=4 })
13: .MapRoute(
14: name : "route2",
15: template : @"weather/{city}/{@date}",
16: constraints : new { city = city},
17: defaults :null)))
18: …
上面介紹的這四個MapRoute方法重載都會在路由表中注冊一個Route對象,它們都將RouteBuilder的DefaultHandler屬性返回的Router作為默認的處理器。如果每個注冊的Route具有如下圖所示各自不同的請求處理邏輯,我們又該如何注冊這樣的Route呢?
1: public static class RequestDelegateRouteBuilderExtensions
2: {
3: public static IRouteBuilder MapRoute(this IRouteBuilder builder, string template, RequestDelegate handler)
4: {
5: IInlineConstraintResolver resolver = builder.ServiceProvider.GetService< IInlineConstraintResolver>();
6: Route route = new Route(new RouteHandler(handler), template, null, null, null, resolver);
7: builder.Routes.Add(route);
8: return builder;
9: }
10:
11: public static IRouteBuilder MapRoute(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action)
12: {
13: IApplicationBuilder appBuilder = builder.ApplicationBuilder.New();
14: action(appBuilder);
15: return builder.MapRoute(template, appBuilder.Build());
16: }
17: }
對於我們實例來說,如果我們使用WeatherForecastForDays方法來返回未來指定天數的天氣信息,而使用另一個方法WeatherForecastForDate來返回指定日期的天氣信息,那麼我們就可以采用如下的形式調用上面這個MapRoute方法來注冊所需的兩個路由。
1: new WebHostBuilder()
2: .ConfigureServices(svcs => svcs.AddRouting())
3: .Configure(app => app.UseRouter(builder => builder
4: .MapRoute(@"/weather/{city:^0\d{{2,3}}$}/{days:int:range(1,2)", WeatherForecastForDays)
5: .MapRoute(@"/weather/{city:^0\d{{2,3}}$}/{*date}", WeatherForecastForDate)))
6: …
7:
8: public static Task WeatherForecastForDays(HttpContext context);
9: public static Task WeatherForecastForDate(HttpContext context);
另一個MapRoute方法除了接收一個作為路由模板的字符串作為第一個參數之外,它的第二個參數是一個類型為Action<IApplicationBuilder>的委托對象。我們可以利用這個委托注冊一個或者多個中間件,這些中間件最終會裝換成一個RequestDelegate對象並作為注冊Route的處理器。如下所示的代碼片段展示了這個方法重載的實現。如果改用這個MapRoute方法來注冊我們實例中所需的兩個路由,我們可以采用如下的編程方式。
1: new WebHostBuilder()
2: .ConfigureServices(svcs => svcs.AddRouting())
3: .Configure(app => app.UseRouter(builder => builder
4: .MapRoute(@"/weather/{city:^0\d{{2,3}}$}/{days:int:range(1,2)",appBuilder => appBuilder.Run(WeatherForecastForDays))
5: .MapRoute(@"/weather/{city:^0\d{{2,3}}$}/{*date}", appBuilder=> appBuilder.Run(WeatherForecastForDate))))
6: …
在《注冊URL模式與HttpHandler的映射關系》演示的實例中,我們實際上是調用RouteBuilder的另一個名為MapGet的擴展方法來進行路由注冊的,這個方法要求被成功路由的HTTP請求必須采用GET方法。除了針對GET請求,RouteBuilder還具有如下這些針對POST、PUT和DELETE請求的擴展方法(MapPost、MapPut和MapDelete)。
1: public static class RequestDelegateRouteBuilderExtensions
2: {
3: public static IRouteBuilder MapGet(this IRouteBuilder builder, string template, RequestDelegate handler);
4: public static IRouteBuilder MapGet(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action);
5:
6: public static IRouteBuilder MapPost(this IRouteBuilder builder, string template, RequestDelegate handler);
7: public static IRouteBuilder MapPost(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action);
8:
9: public static IRouteBuilder MapPut(this IRouteBuilder builder, string template, RequestDelegate handler);
10: public static IRouteBuilder MapPut(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action);
11:
12: public static IRouteBuilder MapDelete(this IRouteBuilder builder, string template, RequestDelegate handler);
13: public static IRouteBuilder MapDelete(this IRouteBuilder builder, string template, Action<IApplicationBuilder> action);
14:
15: public static IRouteBuilder MapVerb(this IRouteBuilder builder, string verb, string template, RequestDelegate handler);
16: public static IRouteBuilder MapVerb(this IRouteBuilder builder, string verb, string template, Action<IApplicationBuilder> action);
17: }
實際上MapGet、MapPost、MapPut和MapDelete方法重載最終都會調用MapVerb方法,後者可以采用字符串的形式指定任意HTTP方法名稱(比如“HEAD”和“OPTIONS”等)。這些方法針對HTTP方法的過濾是同一個類型為HttpMethodRouteConstraint的路由約束來實現的,它要求被路由的請求必須采用指定的方法。這兩個MapVerb方法重載的實現原理體現在如下所示的代碼片段中。
1: public static IRouteBuilder MapVerb(this IRouteBuilder builder, string verb, string template, RequestDelegate handler)
2: {
3: string[] allowedMethods = new string[] { verb };
4: Route item = new Route(new RouteHandler(handler), template, null, new RouteValueDictionary(new {
5: httpMethod = new HttpMethodRouteConstraint(allowedMethods) }), null, GetConstraintResolver(builder));
6: builder.Routes.Add(item);
7: return builder;
8: }
9:
10: public static IRouteBuilder MapVerb(this IRouteBuilder builder, string verb,string template, Action<IApplicationBuilder> action)
11: {
12: IApplicationBuilder builder2 = builder.ApplicationBuilder.New();
13: action(builder2);
14: return builder.MapVerb(verb, template, builder2.Build());
15: }