Итак, во время постановки этого вопроса я покопался в исходном коде ASPNetCore 2, скомпилировал его сам и попытался найти способ его достижения.
Я пришел к выводу, что этобыло невозможно без изменений в структуре.Я не могу вспомнить точные причины, как это было несколько месяцев назад, но в основном нам нужно получить доступ к основному IRouteCollection
, чтобы сопоставить строку маршрута с контроллером / действием, однако IRouteCollection находится только вМаршрутизация Middleware.Поскольку промежуточное ПО в ASPNetCore представляет собой просто цепочку лямбда-функций, одно промежуточное ПО не может опрашивать другие промежуточные программы или даже получать их набор.Все, что вы получите, - это список лямбда, у которого нет возможности покопаться в данных маршрутизации на другой стороне.
Итак, учитывая, что я не смог определить способ построения обобщенного решения для исходного вопроса(«пакетирование» нескольких запросов) Я разработал конкретное решение для конкретной проблемы, которая у меня была.В моем случае я пакетирую запросы вместе, но я пакетирую только определенное подмножество запросов (примерно до 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()
};
}
}