您是否曾經想停止使用Microsoft的內置URL路由並將其替換為自己的實現?在本教程中,我將向您展示如何在ASP.NET Core 3 Web API中實現自定義路由。這可以通過用我們自己的Microsoft替換請求管道中間件來實現。在本教程結束時,我們將使用以下路由語法提供一個具有兩個端點的有效Web Api:
這篇文章將介紹以下內容:
1. 先決條件
2. 創建ExampleController
3. 創建RouteSettings
4. 創建RouteManager
5. 創建EndpointActivator
6. 創建CustomRoutingMiddleware
7. 注冊中間件並測試
先決條件
在開始本教程之前,您應該熟悉反射和ASP.NET Core Web API請求管道。
首先,創建一個ASP.NET Core 3 Web API項目並刪除Startup.ConfigureServices和Startup.Configure中的方法主體。
1 public class Startup 2 { 3 public Startup(IConfiguration configuration) 4 { 5 Configuration = configuration; 6 } 7 8 public IConfiguration Configuration { get; } 9 10 // This method gets called by the runtime. Use this method to add services to the container. 11 public void ConfigureServices(IServiceCollection services) 12 { 13 14 } 15 16 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 17 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 18 { 19 20 } 21 }
創建控制器
創建控制器很簡單-我們將有兩個端點。一個不接受任何輸入並返回一個字符串,另一個接受一個請求對象並重復該字符串x次。
1 public class ExampleController 2 { 3 public async Task<string> Marco() 4 { 5 return await Task.FromResult("Polo"); 6 } 7 8 public async Task<string> Echo(EchoRequest echoRequest) 9 { 10 StringBuilder echoBuilder = new StringBuilder(); 11 12 for(int i = 0; i < echoRequest.EchoCount; i++) 13 { 14 echoBuilder.Append($"{ echoRequest.Content}..."); 15 } 16 17 return await Task.FromResult(echoBuilder.ToString()); 18 } 19 } 20 21 public class EchoRequest 22 { 23 public string Content { get; set; } 24 public int EchoCount { get; set; } 25 }
創建RouteSettings
RouteSettings只是一個模型類,用於存儲每個路由的設置。
1 public class RouteSettings 2 { 3 public string URL { get; set; } 4 public string Action { get; set; } 5 public Type Controller { get; set; } 6 public string Endpoint { get; set; } 7 }
創建RouteManager
RouteManager承擔兩項職責-添加路由和解析URL。
添加路由時,該類將使用反射來獲取設置中描述的端點的MethodInfo,然后使用“(Action)(URL)”作為路由鍵添加路由。
要解析URL,RouteManager會根據給定的路由鍵返回MethodInfo。
1 public class RouteManager 2 { 3 private IDictionary<string, MethodInfo> _routes = new Dictionary<string, MethodInfo>(); 4 5 public RouteManager AddRoute(Action<RouteSettings> setup) 6 { 7 var routeSettings = new RouteSettings(); 8 setup(routeSettings); 9 10 string routeKey = $"{routeSettings.Action} {routeSettings.URL}"; 11 12 var endpointMethod = Assembly.GetExecutingAssembly() 13 .GetTypes() 14 .FirstOrDefault(type => type.Equals(routeSettings.Controller)) 15 .GetMethod(routeSettings.Endpoint); 16 17 _routes.Add(routeKey, endpointMethod); 18 19 return this; 20 } 21 22 public MethodInfo Resolve(string action, string url) 23 { 24 if (url.StartsWith("/")) 25 { 26 url = url.Remove(0, 1); 27 } 28 29 string routeKey = $"{action} {url}"; 30 31 if(_routes.TryGetValue(routeKey, out MethodInfo methodEndpoint)) 32 { 33 return methodEndpoint; 34 } 35 36 throw new Exception($"No matching route for {routeKey}"); 37 } 38 }
創建EndpointActivator
EndPointActivator創建Controller的實例,然后執行給定的Endpoint。如果端點需要參數,例如ExampleController.Echo,則通過反序列化請求正文來初始化參數。
1 public class EndpointActivator 2 { 3 public async Task<object> ActivateAsync(MethodInfo endpointMethod, string requestBody) 4 { 5 // create an instance of the controller 6 var controllerType = endpointMethod.DeclaringType; 7 var controller = Activator.CreateInstance(controllerType); 8 9 var endpointParameter = endpointMethod.GetParameters().FirstOrDefault(); 10 11 if (endpointParameter is null) 12 { 13 var endpointResponse = endpointMethod.Invoke(controller, null); 14 var response = await IfAsync(endpointResponse); 15 return response; 16 } 17 else 18 { 19 var requestBodyParameter = DeserializeRequestBody(requestBody, endpointParameter); 20 var endpointResponse = endpointMethod.Invoke(controller, new object[] { requestBodyParameter }); 21 var response = await IfAsync(endpointResponse); 22 return response; 23 } 24 } 25 26 private static object DeserializeRequestBody(string requestBody, ParameterInfo endpointParameter) 27 { 28 var deserializedParamter = JsonConvert.DeserializeObject(requestBody, endpointParameter.ParameterType); 29 30 if (deserializedParamter is null) 31 { 32 throw new ArgumentException($"Unable to deserialze request body to type {endpointParameter.ParameterType.Name}"); 33 } 34 35 return deserializedParamter; 36 } 37 38 private static async Task<object> IfAsync(object endpointResponse) 39 { 40 var responseTask = endpointResponse as Task; 41 42 if (responseTask is null) 43 { 44 return endpointResponse; 45 } 46 47 await responseTask; 48 49 var responseTaskResult = responseTask.GetType() 50 .GetProperty("Result") 51 .GetValue(responseTask); 52 53 return responseTaskResult; 54 } 55 }
創建CustomRoutingMiddleware
CustomRoutingMiddleware匯集了RouteManager和EndpointActivator來處理從請求管道傳遞的HttpContext對象。它還公開了一個IApplicationBuilder擴展方法,該方法將自身注冊到應用程序的請求管道中並返回RouteManager實例,以便我們可以添加路由。
1 public static class CustomRoutingMiddleware 2 { 3 private static RouteManager _routeManager = new RouteManager(); 4 private static EndpointActivator _endpointActivator = new EndpointActivator(); 5 6 public static RouteManager UseCustomRouting(this IApplicationBuilder app) 7 { 8 // Add TryProcess() to request pipeline 9 app.Use(async (context, next) => 10 { 11 await TryProcess(context); 12 }); 13 14 return _routeManager; 15 } 16 17 public static async Task TryProcess(HttpContext context) 18 { 19 try 20 { 21 // get endpoint method 22 var endpointMethod = _routeManager.Resolve(context.Request.Method, context.Request.Path); 23 24 // read request body 25 string requestBody = await new StreamReader(context.Request.Body, Encoding.UTF8).ReadToEndAsync(); 26 27 // activate the endpoint 28 var response = await _endpointActivator.ActivateAsync(endpointMethod, requestBody); 29 30 // serialize the response 31 var serializedResponse = JsonConvert.SerializeObject(response, Formatting.Indented); 32 33 // return response to client 34 await context.Response.WriteAsync(serializedResponse); 35 } 36 catch(Exception error) 37 { 38 context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; 39 await context.Response.WriteAsync(error.Message); 40 } 41 } 42 }
注冊中間件並測試
現在,我們要做的就是調用IApplicationBuilder.UseCustomRouting並將路由添加到Startup.Configure方法中。
1 public class Startup 2 { 3 public Startup(IConfiguration configuration) 4 { 5 Configuration = configuration; 6 } 7 8 public IConfiguration Configuration { get; } 9 10 // This method gets called by the runtime. Use this method to add services to the container. 11 public void ConfigureServices(IServiceCollection services) 12 { 13 14 } 15 16 // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 17 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 18 { 19 app.UseCustomRouting() 20 .AddRoute(settings => 21 { 22 settings.URL = "example/marco"; 23 settings.Action = "GET"; 24 settings.Controller = typeof(ExampleController); 25 settings.Endpoint = nameof(ExampleController.Marco); 26 }) 27 .AddRoute(settings => 28 { 29 settings.URL = "example/echo"; 30 settings.Action = "POST"; 31 settings.Controller = typeof(ExampleController); 32 settings.Endpoint = nameof(ExampleController.Echo); 33 }); 34 } 35 }
我將使用Postman來測試API。