aspnetcore:如何以编程方式调用另一个基于URI的控制器?

时间:2018-05-21 01:58:21

标签: c# asp.net-core asp.net-core-routing

这个问题的备选标题可能有:

什么是HttpControllerSelector的Asp.NET核心等价物?

我有一个asp.net核心2.0网站,我想委托/代理http请求,如下所示:

  1. 客户端发出“包裹”请求,例如GET server/api/batch?url1=api/thing1&url2=api/thing2
  2. 服务器解包已包装的请求 - 在此示例中,我们现在有两个api/thing1api/thing2
  3. 的网址
  4. 服务器内部调用基础网址 - 在此示例中模拟GET api/thing并获取api/thing2
  5. 在旧的asp.net WebApi中,关键是要新建一个内部HttpRequest,然后调用Configuration.Services.GetHttpControllerSelector().SelectController(request)来获取针对该URL配置的控制器。

    在Asp.Net核心中,我找不到怎么做?我意识到如果我事先知道控制器我可以创建它的实例,但我似乎无法通过URL - >路由 - >控制器类型间隙

1 个答案:

答案 0 :(得分:1)

因此,在问这个问题时,我翻阅了ASPNetCore 2源代码,自己进行了编译,并试图找出实现它的方法。

我得出的结论是,不更改框架就不可能做到这一点。我不记得几个月前的确切原因,但是基本上我们需要访问主IRouteCollection才能将路由字符串映射到控制器/动作,但是IRouteCollection仅在路由中间件中。因为ASPNetCore中的中间件只是一串lambda函数,所以一个中间件无法查询其他中间件,甚至无法获得它们的集合。您所获得的只是Lambda的列表,而无法深入挖掘另一侧的Routing数据。

因此,考虑到我无法确定构建通用解决方案的方法,对于原始问题(“批处理”多个请求),我为遇到的特定问题构建了特定解决方案。在我的实例中,我将请求一起批处理,但是我只批处理了特定的请求子集(约6个控制器/操作方法)。

有一整堆的辅助代码,但总的想法是这段摘录:

if(httpContext.Request.Path.TryMatchRoute("api/{itemType}/{id}/updates/{bookmark?}", out routeValues) &&
    routeValues.TryGetString("itemType", out string itemType) &&
    routeValues.TryGetInt("id", out int id)) 
    {
        routeValues.TryGetString("bookmark", out string bookmark);
        queryParams.TryGetString("fields", out var fields);

        switch (itemType) {
        case "animals":
            return InvokeController<AnimalsController>(async controller =>
                await controller.GetUpdates(id, bookmark, fields).ConfigureAwait(false));
        case "vegetables":
            return InvokeController<VegetablesController>(async controller =>
               await controller.GetUpdates(id, bookmark, fields).ConfigureAwait(false));
        ...

注意:TryMatchRoute是我拥有的一个辅助函数,它使用TemplateSelector对路由字符串进行模式匹配。 InvokeController也是我所拥有的功能,它封装了实例化控制器并使用HttpContext设置IHttpContextFactory的样板,以及正确调用控制器所需的所有功能。

我想这样做是因为我对所有将被批处理的GetUpdates方法有特定的了解,并且希望以特定的方式合并它们的结果。


我确实有另一个实例,而不是一起“分批”处理请求,而是需要“隧道化”它们,并且不需要特定的结果对象知识(尽管这样做很方便)。通过有效地站起ASPNetCore服务器堆栈的整个内部副本,但用伪造的层替换了Kestrel,我实现了这一点。这有点恶心,但也很整洁。如上所述,这不是完整的代码,但是您可以理解:

void Main() 
{
    m_server = new FakeServer();

    var builder = Microsoft.AspNetCore.WebHost.CreateDefaultBuilder()
        .UseStartup<InternalServerStartup>()
        .UseSetting(WebHostDefaults.SuppressStatusMessagesKey, "True")
        .ConfigureServices(services => {
            services.AddSingleton<IServer>(m_server); // this causes the WebHostBuilder to fire requests at our fake server
        });

    var host = builder.Build();
    host.Start();
}

// This class solely exists for us capture the instance of HostingApplication that MVC creates internally so we can invoke it directly
protected class FakeServer : IServer
{
    public HostingApplication HostingApplication { get; private set; } = null;

    public IFeatureCollection Features => new FeatureCollection();

    public void Dispose()
    { }

    public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
    {
        if(application is HostingApplication standardContextApp) {
            HostingApplication = standardContextApp;
        }
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

TunneledResponse OnTunneledRequest(string url, string method, string[] headers, byte[] body)
{
    var host = m_server.HostingApplication;

    var responseBodyStream = new MemoryStream();

    var features = new FeatureCollection();
    features.Set<IHttpRequestFeature>(new HttpRequestFeature());
    features.Set<IHttpResponseFeature>(new HttpResponseFeature { Body = responseBodyStream });

    var ctx = host.CreateContext(features);

    var httpContext = ctx.HttpContext;

    var targetUri = new Uri(url);

    httpContext.Request.Scheme = "fakescheme";
    httpContext.Request.Host = new HostString("fakehost"); // if host is missing, things crash down the line: https://github.com/aspnet/Home/issues/2718
    httpContext.Request.Path = targetUri.AbsolutePath;
    httpContext.Request.Method = method;
    httpContext.Request.QueryString = new QueryString(targetUri.Query);

    for (var i = 0; i < headers.Length / 2; i++)
        httpContext.Request.Headers.Add(headers[i * 2], headers[i * 2 + 1]);


    try { 
        await host.ProcessRequestAsync(ctx);

        responseBodyStream.Position = 0; // aspnet will have written to it, seek back to the start

        return new TunneledResponse {
            statusCode = httpContext.Response.StatusCode,
            headers = httpContext.Response.Headers.SelectMany(h => new[] { h.Key, h.Value.FirstOrDefault() }).ToArray(),
            body = responseBodyStream.ToArray()
        };
    }
}