ASP.NET Core如何基于URI编程调用其他控制器?HttpControllerSelector等效实现是什么?
我明白你在ASP.NET Core 2.0里遇到的这个内部请求路由的问题——原来在旧WebApi里用Configuration.Services.GetHttpControllerSelector().SelectController(request)那套确实没法直接照搬了,毕竟Core的路由系统完全重构过。下面给你几个可行的方案,一步步解决这个批量请求代理的需求:
这个方法最直接,相当于让服务器自己当客户端调用内部API,不用纠结路由到控制器的细节,适合快速落地:
首先在Startup.cs的ConfigureServices里注册HttpClient:
services.AddHttpClient();
然后在你的批量处理控制器里注入IHttpClientFactory,构造请求并调用:
[Route("server/api")] [ApiController] public class BatchController : ControllerBase { private readonly IHttpClientFactory _httpClientFactory; public BatchController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } [HttpGet("batch")] public async Task<IActionResult> BatchGet([FromQuery] List<string> url) { var client = _httpClientFactory.CreateClient(); // 拼接当前服务器的基础URL,避免硬编码 var baseUrl = $"{Request.Scheme}://{Request.Host}"; var results = new Dictionary<string, object>(); foreach (var path in url) { var fullUrl = $"{baseUrl}/{path.TrimStart('/')}"; var response = await client.GetAsync(fullUrl); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadFromJsonAsync<object>(); results[path] = content; } else { results[path] = new { StatusCode = (int)response.StatusCode, Error = response.ReasonPhrase }; } } return Ok(results); } }
优点:零路由解析成本,完全复用现有接口逻辑;缺点:会走网络栈,性能略差,但大部分场景下足够用。
如果追求极致性能,不想走网络,可以直接在服务器内部找到对应的Action并执行,这需要利用Core的ActionDescriptor和IActionInvoker体系:
首先在Startup.cs里注册必要服务:
services.AddHttpContextAccessor(); // 用于获取当前HttpContext
然后在控制器里注入相关服务:
private readonly IActionDescriptorCollectionProvider _actionDescriptorProvider; private readonly IActionInvokerFactory _actionInvokerFactory; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IRouter _router; public BatchController(IActionDescriptorCollectionProvider actionDescriptorProvider, IActionInvokerFactory actionInvokerFactory, IHttpContextAccessor httpContextAccessor, IRouter router) { _actionDescriptorProvider = actionDescriptorProvider; _actionInvokerFactory = actionInvokerFactory; _httpContextAccessor = httpContextAccessor; _router = router; }
第一步:准确匹配路由对应的ActionDescriptor
用IRouter来处理路由匹配(支持带参数的路由,比如api/thing/{id}):
private async Task<ActionDescriptor> MatchRouteAsync(string path, HttpMethod method) { var originalContext = _httpContextAccessor.HttpContext; var newContext = new DefaultHttpContext(originalContext.RequestServices) { Request = { Method = method.Method, Path = new PathString($"/{path.TrimStart('/')}") } }; var routeContext = new RouteContext(newContext); await _router.RouteAsync(routeContext); // 从匹配到的Endpoint里获取ActionDescriptor return routeContext.Handler is ActionEndpoint endpoint ? endpoint.Metadata.GetMetadata<ActionDescriptor>() : null; }
第二步:创建模拟上下文并执行Action
private async Task<object> InvokeActionAsync(ActionDescriptor descriptor, string path, HttpMethod method) { var originalContext = _httpContextAccessor.HttpContext; var newContext = new DefaultHttpContext(originalContext.RequestServices) { Request = { Method = method.Method, Path = new PathString($"/{path.TrimStart('/')}"), Scheme = originalContext.Request.Scheme, Host = originalContext.Request.Host } }; var actionContext = new ActionContext(newContext, new RouteData(), descriptor); var invoker = _actionInvokerFactory.CreateInvoker(actionContext); object resultValue = null; int? statusCode = null; // 监听Action执行完成事件,获取返回结果 invoker.ResultExecuted += (sender, args) => { if (args.Result is ObjectResult objResult) { resultValue = objResult.Value; statusCode = objResult.StatusCode; } // 可以扩展处理其他类型的ActionResult,比如JsonResult、StatusCodeResult等 return Task.CompletedTask; }; await invoker.InvokeAsync(); return statusCode.HasValue && statusCode >= 400 ? new { StatusCode = statusCode, Error = "Action execution failed" } : resultValue; }
第三步:批量处理请求
[HttpGet("batch")] public async Task<IActionResult> BatchGet([FromQuery] List<string> url) { var results = new Dictionary<string, object>(); foreach (var path in url) { var descriptor = await MatchRouteAsync(path, HttpMethod.Get); if (descriptor == null) { results[path] = new { Error = "No matching action found for this path" }; continue; } try { var content = await InvokeActionAsync(descriptor, path, HttpMethod.Get); results[path] = content; } catch (Exception ex) { results[path] = new { Error = ex.Message }; } } return Ok(results); }
优点:完全内部调用,无网络开销;缺点:需要处理路由匹配和上下文模拟,代码量稍大,但能精准控制请求流程。
- 如果你追求快速实现,选方案1即可;
- 如果你需要高性能或者要自定义内部请求的细节,选方案2。
ASP.NET Core里确实没有直接对应旧WebApi的GetHttpControllerSelector的API,因为Core把路由和控制器的映射逻辑改成了基于ActionDescriptor和Endpoint的体系,上面的方案就是基于这套新体系的替代实现。
内容的提问来源于stack exchange,提问作者Orion Edwards

