原文:https://www.stevejgordon.co.uk/asp-net-core-anatomy-part-3-addmvc 發佈於:2017年4月環境:ASP.NET Core 1.1 本系列前面兩篇文章介紹了ASP.NET Core中IServiceCollection兩個主要擴展方 ...
原文:https://www.stevejgordon.co.uk/asp-net-core-anatomy-part-3-addmvc
發佈於:2017年4月
環境:ASP.NET Core 1.1
本系列前面兩篇文章介紹了ASP.NET Core中IServiceCollection兩個主要擴展方法(AddMvcCore與AddMvc)。當你準備在程式中使用MVC中間件時,它們用來添加所需的MVC服務。
接下來,要在ASP.NET Core程式中啟動MVC還需要在Startup類的Configure方法中執行UseMvc IApplicationBuilder擴展方法。該方法註冊MVC中間件到應用程式管道中,以便MVC框架能夠處理請求並返迴響應(通常是view result或json)。本文我將分析一下在應用程式啟動時UserMvc方法做了什麼。
和先前文章一樣,我使用rel/1.1.2 MVC版本庫作為分析對象。代碼是基於原來的project.json,因為在VS2017中似乎沒有簡單辦法實現調試多個ASP.NET Core源代碼。
UseMvc是IApplicationBuilder的一個擴展方法,帶有一個Action<IRouteBuilder>委托參數。IRouteBuilder將被用於配置MVC的路由。UserMvc還有一個重載方法,不需要任何參數,它只是簡單的傳遞一個空委托調用主函數。如下:
public static IApplicationBuilder UseMvc(this IApplicationBuilder app) { if (app == null) { throw new ArgumentNullException(nameof(app)); } return app.UseMvc(routes => { }); }
主UseMvc方法:
public static IApplicationBuilder UseMvc( this IApplicationBuilder app, Action<IRouteBuilder> configureRoutes) { if (app == null) { throw new ArgumentNullException(nameof(app)); } if (configureRoutes == null) { throw new ArgumentNullException(nameof(configureRoutes)); } // Verify if AddMvc was done before calling UseMvc // We use the MvcMarkerService to make sure if all the services were added. if (app.ApplicationServices.GetService(typeof(MvcMarkerService)) == null) { throw new InvalidOperationException(Resources.FormatUnableToFindServices( nameof(IServiceCollection), "AddMvc", "ConfigureServices(...)")); } var middlewarePipelineBuilder = app.ApplicationServices.GetRequiredService<MiddlewareFilterBuilder>(); middlewarePipelineBuilder.ApplicationBuilder = app.New(); var routes = new RouteBuilder(app) { DefaultHandler = app.ApplicationServices.GetRequiredService<MvcRouteHandler>(), }; configureRoutes(routes); routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices)); return app.UseRouter(routes.Build()); }
我們來分析一下此方法。首先檢查IServiceProvider中是否有MvcMarkerService服務註冊。為此使用ApplicationServices屬性暴露IServiceProvider,再調用GetService,嘗試查找已註冊的MvcMarkerService。MvcMarkerService在AddMvcCore執行時已被註冊,因此如果沒有找到,則表明在ConfigureServices執行前沒有調用過AddMvc或AddMvcCore。這樣的Marker Services在很多地方使用,它可以幫助在代碼執行之前,檢查是否存已存在正確的依賴關係。
接下來,UserMvc從ServiceProvider請求一個MiddlewareFilterBuilder,並使用IApplicationBuilder.New()方法設定其ApplicationBuilder。當調用此方法時,ApplicationBuilder會創建並返回自身的新實例副本。
再下來,UserMvc初始化一個新的RouteBuilder,同時設定預設處理程式(default handler)為已註冊的MvcRouteHandler。此時DI就像有魔力,一堆依賴對象開始實例化。這裡請求MvcRouteHandler是因為其構造函數中有一些依賴關係,所以相關的其它類也會被初始化。每個這些類的構造函數都會請求額外的依賴關係,然後創建它們。這是DI系統工作的一個完美例子。雖然在ConfigureServices中所有的介面和具體實現已在container中註冊,但實際對象只會在被註入時創建。通過創建RouteBuilder,對象創建就像滾雪球,直到所有依賴關係都被構造。一些對象作為單例模式註冊,無論ServiceProvider是否請求都會創建,其生命周期貫穿整個應用程式。其它對象可能是在每次請求時通過構造函數創建或者其它情況,每次創建的對象都是特有的。瞭解依賴註入是如何工作的,特別是ASP.NET Core ServiceProvider的詳細信息超出了這篇文章的範圍。
RouteBuilder創建之後,Action<IRouteBuilder>委托被調用,使用RouteBuilder作為唯一參數。在MvcSandbox示列中,我們稱之為UseMvc方法,通過一個lambda傳遞給代理方法。此功能將映射一個名為“default”的路由,如下所示:
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
MapRoute是IrouteBuilder的一個擴展方法,主要的MapRoute方法如下所示:
public static IRouteBuilder MapRoute( this IRouteBuilder routeBuilder, string name, string template, object defaults, object constraints, object dataTokens) { if (routeBuilder.DefaultHandler == null) { throw new RouteCreationException(Resources.FormatDefaultHandler_MustBeSet(nameof(IRouteBuilder))); } var inlineConstraintResolver = routeBuilder .ServiceProvider .GetRequiredService<IInlineConstraintResolver>(); routeBuilder.Routes.Add(new Route( routeBuilder.DefaultHandler, name, template, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), new RouteValueDictionary(dataTokens), inlineConstraintResolver)); return routeBuilder; }
這將請求一個由ServiceProvider解析的IinlineConstraintResolver實例給DefaultInlineConstraintResolver。該類使用IOptions <RouteOptions>參數初始化構造函數。
Microsoft.Extensions.Options程式集調用RouteOptions作為參數的MvcCoreRouteOptionsSetup.Configure方法。這將添加一個KnownRouteValueConstraint類型的約束映射到約束映射字典。當首次構建RouteOptions時,該字典將初始化多個預設約束。我會在以後的文章中詳細介紹路由代碼。
通過提供的名稱和模板構建一個新的Route對象。在我們的例子中,新對象添到了RouteBuilder.Routes列表中。至此,在MvcSandbox示列程式運行時,我們的router builder就有了一條路由記錄。
請註意,MvcApplicationBuilderExtensions類還包括一個名為UseMvcWithDefaultRoute的擴展方法。此方法會調用UseMvc,通過硬編碼設定預設路由。
public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app) { if (app == null) { throw new ArgumentNullException(nameof(app)); } return app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
該路由使用與MvcSandbox程式中定義的相同名稱和模板進行定義。因此,它可能被用作MvcSandbox Startup類中的輕微代碼保護程式(slight code saver)。對於最基本的Mvc程式,使用該擴展方法可能足以滿足路由的需求。但在大多實際使用場景中,我相信你會傳遞一套更加完整的路由。這是一個很好的方式(shorthand),如果你想從基本路由模板開始,就可以直接調用UseMvc()而不使用任何參數。
接下來,調用靜態AttributeRouting.CreateAttributeMegaRoute方法,同時把生成的路由添加到RouteBuilder中的Routes List(索引位置為0)。CreateAttributeMegaRoute已在“Creates an attribute route using the provided services and provided target router”文中講述,它看起來像這樣:
public static IRouter CreateAttributeMegaRoute(IServiceProvider services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } return new AttributeRoute( services.GetRequiredService<IActionDescriptorCollectionProvider>(), services, actions => { var handler = services.GetRequiredService<MvcAttributeRouteHandler>(); handler.Actions = actions; return handler; }); }
該方法創建了一個實現了IRoute介面的新AttributeRoute。其構造函數看起來想象這樣:
public AttributeRoute( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, IServiceProvider services, Func<ActionDescriptor[], IRouter> handlerFactory) { if (actionDescriptorCollectionProvider == null) { throw new ArgumentNullException(nameof(actionDescriptorCollectionProvider)); } if (services == null) { throw new ArgumentNullException(nameof(services)); } if (handlerFactory == null) { _handlerFactory = handlerFactory; } _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; _services = services; _handlerFactory = handlerFactory; }
該構造函數需要一個IactionDescriptorCollectionProvider,一個IServiceProvider和一個接受ActionDescriptor數組參數,並返回IRouter的Func。預設情況下,通過AddMvc註冊服務時將獲得一個ActionDescriptorCollectionProvider實例,它是一個通過ServiceProvider註冊的單例(singleton)對象。ActionDescriptors表示在應用程式中創建和發現可用的MVC actions。這些對象我們另文分析。
創建新的AttributeRoute代碼(CreateAttributeMegaRoute方法內部)使用lambda來定義Func <ActionDescriptor [],IRouter>的代碼。在這種情況下,委托函數從ServiceProvider請求一個MvcAttributeRouteHandler。因為被註冊為transient,所以每次請求ServiceProvider將返回一個新的MvcAttributeRouteHandler實例。委托代碼然後使用傳入的ActionDescriptions數組在MvcAttributeRouteHandler上設置Actions屬性(ActionDescriptions的數組),最後返回新的處理程式。
返回UserMvc,IapplicationBuilder中的UseRouter擴展方法完成調用。傳遞給UseRouter的對象是通過調用RouteBuilder的Build方法來創建的。該方法將添加路由到路由集合。RouteCollection內部將追蹤哪些是已命名,哪些是未命名。在我們的MvcSandbox示例中,我們會得到一個命名為“default”的路由和一個未命名的AttributeRoute。
UseRouter有兩個簽名。此時我們傳遞一個IRouter,所以調用以下方法:
public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, IRouter router) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } if (router == null) { throw new ArgumentNullException(nameof(router)); } if (builder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) { throw new InvalidOperationException(Resources.FormatUnableToFindServices( nameof(IServiceCollection), nameof(RoutingServiceCollectionExtensions.AddRouting), "ConfigureServices(...)")); } return builder.UseMiddleware<RouterMiddleware>(router); }
該方法實現檢查ServiceProvider中的RoutingMarkerService。假設這是按預期註冊的,它將RouterMiddleware添加到中間件管道(middeware pipeline)中。正是這個中間件,使用IRouter來嘗試將控制器的請求和MVC中的動作(action)進行匹配,以便處理它們。這個過程的細節將在未來的博文中展現。
到此,應用程式管道已配置,應用程式已準備好接收請求。本文也可以結束了。
小結
本文我們分析了UseMvc並且看到它設置了將在稍後使用的MiddlewareFilterBuilder。然後它還通過RouteBuilder獲得一個IRouter,在這個階段的大部分工作都是註冊路由。一旦設置完成,路由中間件就被註冊到管道中。這個代碼(中間件)知道如何檢查傳入的請求,並將路徑映射到適當的控制器和可以處理它們的操作。
configureRoutes(routes);
routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(app.ApplicationServices));
return app.UseRouter(routes.Build());
}