具有自己的一组控制器的多个 owin 侦听器,以及用于 DI 的 Autofac
Multiple owin listeners with their own set of controllers, with Autofac for DI
我正在尝试使用多个进程内 owin 侦听器。每个都应该有一组不同的控制器,其中它们可能具有由不同控制器处理的相同路由。例如
localhost:1234/api/app/test
should resolve to ControllerA
localhost:5678/api/app/test
should resolve to ControllerB
控制器a,在owin主机1中,有route属性
[Route("api/app/test")]
控制器 b,在 owin 主机 2 中,具有路由属性
[Route("api/app/{*path}")]
并用于将请求转发到其他 owin 主机。
我们正在使用 Autofac 进行依赖注入。路由是通过属性路由配置的。
autofac 需要一行
builder.RegisterApiControllers(typeof(ControllerA).Assembly)
我们的 OWIN 配置包含:
var config = ConfigureWebApi();
// Configure Autofac
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
但是当启动两个侦听器时,我需要包含两个程序集以进行控制器解析。这会导致 'duplicate route' 异常:
Multiple controller types were found that match the URL. This can
happen if attribute routes on multiple controllers match the requested
URL.\r\n\r\nThe request has found the following matching controller
types:
\r\nLib1.Controllers.ControllerA\r\nLib2.Controllers.ControllerB"
当 运行 单独进程中的 OWIN 侦听器时,没有问题。
我也曾尝试使用多个 DI 容器,每个 OWIN 侦听器一个,但这与 Web Api 2 冲突,因为它需要设置 GlobalConfiguration.Configuration.DependencyResolver。这与多个 DI 容器的概念冲突。
有人可以指导我如何配置这样的设置吗?
使用OWIN
环境并自定义HttpControllerSelector
使用 OWIN
管道,您可以将有关请求的信息传递给自定义 HttpControllerSelector
。这使您可以选择使用哪些控制器来匹配哪些路由。
当然,说起来容易做起来难。 WebAPI 在路由方面的内部工作不是很透明 - source code 通常是该领域最好的文档。
我无法让 HttpControllerSelector
完全工作,所以 CustomHttpActionSelector
中有一个丑陋的解决方法。如果您需要做的只是将请求从一台主机转发到另一台主机,这可能就足够了。
最后的结果是:
GET
到 http://localhost:1234/api/app/test
returns "HellofromAController" (直接调用 AController)
GET
to http://localhost:5678/api/app/test
returns "(FromBController): \"HellofromAController\"" (调用 BController,它将请求转发给 AController)
我保留了日志记录代码以防它有用,但它与解决方案无关。
废话少说:
CustomHttpControllerSelector.cs:
使用端口特定的 OWIN 环境变量 ApiControllersAssembly
来过滤控制器。
public sealed class CustomHttpControllerSelector : DefaultHttpControllerSelector
{
private static readonly ILog Logger;
static CustomHttpControllerSelector()
{
Logger = LogProvider.GetCurrentClassLogger();
}
public CustomHttpControllerSelector(HttpConfiguration configuration) : base(configuration)
{
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var apiControllerAssembly = request.GetOwinEnvironment()["ApiControllersAssembly"].ToString();
Logger.Debug($"{nameof(CustomHttpControllerSelector)}: {{{nameof(apiControllerAssembly)}: {apiControllerAssembly}}}");
var routeData = request.GetRouteData();
var routeCollectionRoute = routeData.Route as IReadOnlyCollection<IHttpRoute>;
var newRoutes = new List<IHttpRoute>();
var newRouteCollectionRoute = new RouteCollectionRoute();
foreach (var route in routeCollectionRoute)
{
var filteredDataTokens = FilterDataTokens(route, apiControllerAssembly);
if (filteredDataTokens.Count == 2)
{
var newRoute = new HttpRoute(route.RouteTemplate, (HttpRouteValueDictionary)route.Defaults, (HttpRouteValueDictionary)route.Constraints, filteredDataTokens);
newRoutes.Add(newRoute);
}
}
var newRouteDataValues = new HttpRouteValueDictionary();
foreach (var routeDataKvp in routeData.Values)
{
var newRouteDataCollection = new List<IHttpRouteData>();
var routeDataCollection = routeDataKvp.Value as IEnumerable<IHttpRouteData>;
if (routeDataCollection != null)
{
foreach (var innerRouteData in routeDataCollection)
{
var filteredDataTokens = FilterDataTokens(innerRouteData.Route, apiControllerAssembly);
if (filteredDataTokens.Count == 2)
{
var newInnerRoute = new HttpRoute(innerRouteData.Route.RouteTemplate, (HttpRouteValueDictionary)innerRouteData.Route.Defaults, (HttpRouteValueDictionary)innerRouteData.Route.Constraints, filteredDataTokens);
var newInnerRouteData = new HttpRouteData(newInnerRoute, (HttpRouteValueDictionary)innerRouteData.Values);
newRouteDataCollection.Add(newInnerRouteData);
}
}
newRouteDataValues.Add(routeDataKvp.Key, newRouteDataCollection);
}
else
{
newRouteDataValues.Add(routeDataKvp.Key, routeDataKvp.Value);
}
HttpRouteData newRouteData;
if (newRoutes.Count > 1)
{
newRouteCollectionRoute.EnsureInitialized(() => newRoutes);
newRouteData = new HttpRouteData(newRouteCollectionRoute, newRouteDataValues);
}
else
{
newRouteData = new HttpRouteData(newRoutes[0], newRouteDataValues);
}
request.SetRouteData(newRouteData);
}
var controllerDescriptor = base.SelectController(request);
return controllerDescriptor;
}
private static HttpRouteValueDictionary FilterDataTokens(IHttpRoute route, string apiControllerAssembly)
{
var newDataTokens = new HttpRouteValueDictionary();
foreach (var dataToken in route.DataTokens)
{
var actionDescriptors = dataToken.Value as IEnumerable<HttpActionDescriptor>;
if (actionDescriptors != null)
{
var newActionDescriptors = new List<HttpActionDescriptor>();
foreach (var actionDescriptor in actionDescriptors)
{
if (actionDescriptor.ControllerDescriptor.ControllerType.Assembly.FullName == apiControllerAssembly)
{
newActionDescriptors.Add(actionDescriptor);
}
}
if (newActionDescriptors.Count > 0)
{
newDataTokens.Add(dataToken.Key, newActionDescriptors.ToArray());
}
}
else
{
newDataTokens.Add(dataToken.Key, dataToken.Value);
}
}
return newDataTokens;
}
}
CustomHttpActionSelector.cs:
您不需要 CustomHttpActionSelector
,它的存在只是为了解决 BController 的 ActionDescriptors 的问题。只要 BController 只有一种方法,它就可以工作,否则您将需要实现一些特定于路由的逻辑。
public sealed class CustomHttpActionSelector : ApiControllerActionSelector
{
private static readonly ILog Logger;
static CustomHttpActionSelector()
{
Logger = LogProvider.GetCurrentClassLogger();
}
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
try
{
var actionDescriptor = base.SelectAction(controllerContext);
return actionDescriptor;
}
catch (Exception ex)
{
Logger.WarnException(ex.Message, ex);
IDictionary<string, object> dataTokens;
var route = controllerContext.Request.GetRouteData().Route;
var routeCollectionRoute = route as IReadOnlyCollection<IHttpRoute>;
if (routeCollectionRoute != null)
{
dataTokens = routeCollectionRoute
.Select(r => r.DataTokens)
.SelectMany(dt => dt)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
else
{
dataTokens = route.DataTokens;
}
var actionDescriptors = dataTokens
.Select(dt => dt.Value)
.Where(dt => dt is IEnumerable<HttpActionDescriptor>)
.Cast<IEnumerable<HttpActionDescriptor>>()
.SelectMany(r => r)
.ToList();
return actionDescriptors.FirstOrDefault();
}
}
}
Program.cs:
internal class Program
{
private static readonly ILog Logger;
static Program()
{
Log.Logger = new LoggerConfiguration()
.WriteTo
.LiterateConsole()
.MinimumLevel.Is(LogEventLevel.Verbose)
.CreateLogger();
Logger = LogProvider.GetCurrentClassLogger();
}
internal static void Main(string[] args)
{
var builder = new ContainerBuilder();
builder.RegisterModule(new LogRequestModule());
builder.RegisterApiControllers(typeof(AController).Assembly);
builder.RegisterApiControllers(typeof(BController).Assembly);
var container = builder.Build();
var config = GetHttpConfig();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
var options = new StartOptions();
options.Urls.Add("http://localhost:1234");
options.Urls.Add("http://localhost:5678");
var listener = WebApp.Start(options, app =>
{
app.Use((ctx, next) =>
{
if (ctx.Request.LocalPort.HasValue)
{
var port = ctx.Request.LocalPort.Value;
string apiControllersAssemblyName = null;
if (port == 1234)
{
apiControllersAssemblyName = typeof(AController).Assembly.FullName;
}
else if (port == 5678)
{
apiControllersAssemblyName = typeof(BController).Assembly.FullName;
}
ctx.Set("ApiControllersAssembly", apiControllersAssemblyName);
Logger.Info($"{nameof(WebApp)}: Port = {port}, ApiControllersAssembly = {apiControllersAssemblyName}");
}
return next();
});
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
});
Logger.Info(@"Press [Enter] to exit");
Console.ReadLine();
listener.Dispose(); ;
}
private static HttpConfiguration GetHttpConfig()
{
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
config.Services.Add(typeof(IExceptionLogger), new LogProviderExceptionLogger());
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Services.Replace(typeof(IHttpControllerSelector), new CustomHttpControllerSelector(config));
config.Services.Replace(typeof(IHttpActionSelector), new CustomHttpActionSelector());
var traceSource = new TraceSource("LibLog") { Switch = { Level = SourceLevels.All } };
traceSource.Listeners.Add(new LibLogTraceListener());
var diag = config.EnableSystemDiagnosticsTracing();
diag.IsVerbose = false;
diag.TraceSource = traceSource;
return config;
}
}
LibA\Controllers\AController.cs:
[RoutePrefix("api/app")]
public class AController : ApiController
{
private static readonly ILog Logger;
static AController()
{
Logger = LogProvider.GetCurrentClassLogger();
Logger.Debug($"{nameof(AController)}: Static Constructor");
}
public AController()
{
Logger.Debug($"{nameof(AController)}: Constructor");
}
[HttpGet, Route("test")]
public async Task<IHttpActionResult> Get()
{
Logger.Debug($"{nameof(AController)}: Get()");
return Ok($"Hello from {nameof(AController)}");
}
}
LibB\Controllers\BController.cs:
[RoutePrefix("api/app")]
public class BController : ApiController
{
private static readonly ILog Logger;
static BController()
{
Logger = LogProvider.GetCurrentClassLogger();
Logger.Debug($"{nameof(BController)}: Static Constructor");
}
public BController()
{
Logger.Debug($"{nameof(BController)}: Constructor");
}
[HttpGet, Route("{*path}")]
public async Task<IHttpActionResult> Get([FromUri] string path)
{
if (path == null)
{
path = Request.RequestUri.PathAndQuery.Split(new[] {"api/app/"}, StringSplitOptions.RemoveEmptyEntries)[1];
}
Logger.Debug($"{nameof(BController)}: Get({path})");
using (var client = new HttpClient {BaseAddress = new Uri("http://localhost:1234/api/app/")})
{
var result = await client.GetAsync(path);
var content = await result.Content.ReadAsStringAsync();
return Ok($"(From {nameof(BController)}): {content}");
}
}
}
有空的时候我可能会再试一次。
如果你有任何进展,请告诉我!
我正在尝试使用多个进程内 owin 侦听器。每个都应该有一组不同的控制器,其中它们可能具有由不同控制器处理的相同路由。例如
localhost:1234/api/app/test
should resolve toControllerA
localhost:5678/api/app/test
should resolve toControllerB
控制器a,在owin主机1中,有route属性
[Route("api/app/test")]
控制器 b,在 owin 主机 2 中,具有路由属性
[Route("api/app/{*path}")]
并用于将请求转发到其他 owin 主机。
我们正在使用 Autofac 进行依赖注入。路由是通过属性路由配置的。 autofac 需要一行
builder.RegisterApiControllers(typeof(ControllerA).Assembly)
我们的 OWIN 配置包含:
var config = ConfigureWebApi();
// Configure Autofac
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
但是当启动两个侦听器时,我需要包含两个程序集以进行控制器解析。这会导致 'duplicate route' 异常:
Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.\r\n\r\nThe request has found the following matching controller types: \r\nLib1.Controllers.ControllerA\r\nLib2.Controllers.ControllerB"
当 运行 单独进程中的 OWIN 侦听器时,没有问题。
我也曾尝试使用多个 DI 容器,每个 OWIN 侦听器一个,但这与 Web Api 2 冲突,因为它需要设置 GlobalConfiguration.Configuration.DependencyResolver。这与多个 DI 容器的概念冲突。
有人可以指导我如何配置这样的设置吗?
使用OWIN
环境并自定义HttpControllerSelector
使用 OWIN
管道,您可以将有关请求的信息传递给自定义 HttpControllerSelector
。这使您可以选择使用哪些控制器来匹配哪些路由。
当然,说起来容易做起来难。 WebAPI 在路由方面的内部工作不是很透明 - source code 通常是该领域最好的文档。
我无法让 HttpControllerSelector
完全工作,所以 CustomHttpActionSelector
中有一个丑陋的解决方法。如果您需要做的只是将请求从一台主机转发到另一台主机,这可能就足够了。
最后的结果是:
GET
到 http://localhost:1234/api/app/test
returns "HellofromAController" (直接调用 AController)
GET
to http://localhost:5678/api/app/test
returns "(FromBController): \"HellofromAController\"" (调用 BController,它将请求转发给 AController)
我保留了日志记录代码以防它有用,但它与解决方案无关。
废话少说:
CustomHttpControllerSelector.cs:
使用端口特定的 OWIN 环境变量 ApiControllersAssembly
来过滤控制器。
public sealed class CustomHttpControllerSelector : DefaultHttpControllerSelector
{
private static readonly ILog Logger;
static CustomHttpControllerSelector()
{
Logger = LogProvider.GetCurrentClassLogger();
}
public CustomHttpControllerSelector(HttpConfiguration configuration) : base(configuration)
{
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var apiControllerAssembly = request.GetOwinEnvironment()["ApiControllersAssembly"].ToString();
Logger.Debug($"{nameof(CustomHttpControllerSelector)}: {{{nameof(apiControllerAssembly)}: {apiControllerAssembly}}}");
var routeData = request.GetRouteData();
var routeCollectionRoute = routeData.Route as IReadOnlyCollection<IHttpRoute>;
var newRoutes = new List<IHttpRoute>();
var newRouteCollectionRoute = new RouteCollectionRoute();
foreach (var route in routeCollectionRoute)
{
var filteredDataTokens = FilterDataTokens(route, apiControllerAssembly);
if (filteredDataTokens.Count == 2)
{
var newRoute = new HttpRoute(route.RouteTemplate, (HttpRouteValueDictionary)route.Defaults, (HttpRouteValueDictionary)route.Constraints, filteredDataTokens);
newRoutes.Add(newRoute);
}
}
var newRouteDataValues = new HttpRouteValueDictionary();
foreach (var routeDataKvp in routeData.Values)
{
var newRouteDataCollection = new List<IHttpRouteData>();
var routeDataCollection = routeDataKvp.Value as IEnumerable<IHttpRouteData>;
if (routeDataCollection != null)
{
foreach (var innerRouteData in routeDataCollection)
{
var filteredDataTokens = FilterDataTokens(innerRouteData.Route, apiControllerAssembly);
if (filteredDataTokens.Count == 2)
{
var newInnerRoute = new HttpRoute(innerRouteData.Route.RouteTemplate, (HttpRouteValueDictionary)innerRouteData.Route.Defaults, (HttpRouteValueDictionary)innerRouteData.Route.Constraints, filteredDataTokens);
var newInnerRouteData = new HttpRouteData(newInnerRoute, (HttpRouteValueDictionary)innerRouteData.Values);
newRouteDataCollection.Add(newInnerRouteData);
}
}
newRouteDataValues.Add(routeDataKvp.Key, newRouteDataCollection);
}
else
{
newRouteDataValues.Add(routeDataKvp.Key, routeDataKvp.Value);
}
HttpRouteData newRouteData;
if (newRoutes.Count > 1)
{
newRouteCollectionRoute.EnsureInitialized(() => newRoutes);
newRouteData = new HttpRouteData(newRouteCollectionRoute, newRouteDataValues);
}
else
{
newRouteData = new HttpRouteData(newRoutes[0], newRouteDataValues);
}
request.SetRouteData(newRouteData);
}
var controllerDescriptor = base.SelectController(request);
return controllerDescriptor;
}
private static HttpRouteValueDictionary FilterDataTokens(IHttpRoute route, string apiControllerAssembly)
{
var newDataTokens = new HttpRouteValueDictionary();
foreach (var dataToken in route.DataTokens)
{
var actionDescriptors = dataToken.Value as IEnumerable<HttpActionDescriptor>;
if (actionDescriptors != null)
{
var newActionDescriptors = new List<HttpActionDescriptor>();
foreach (var actionDescriptor in actionDescriptors)
{
if (actionDescriptor.ControllerDescriptor.ControllerType.Assembly.FullName == apiControllerAssembly)
{
newActionDescriptors.Add(actionDescriptor);
}
}
if (newActionDescriptors.Count > 0)
{
newDataTokens.Add(dataToken.Key, newActionDescriptors.ToArray());
}
}
else
{
newDataTokens.Add(dataToken.Key, dataToken.Value);
}
}
return newDataTokens;
}
}
CustomHttpActionSelector.cs:
您不需要 CustomHttpActionSelector
,它的存在只是为了解决 BController 的 ActionDescriptors 的问题。只要 BController 只有一种方法,它就可以工作,否则您将需要实现一些特定于路由的逻辑。
public sealed class CustomHttpActionSelector : ApiControllerActionSelector
{
private static readonly ILog Logger;
static CustomHttpActionSelector()
{
Logger = LogProvider.GetCurrentClassLogger();
}
public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
try
{
var actionDescriptor = base.SelectAction(controllerContext);
return actionDescriptor;
}
catch (Exception ex)
{
Logger.WarnException(ex.Message, ex);
IDictionary<string, object> dataTokens;
var route = controllerContext.Request.GetRouteData().Route;
var routeCollectionRoute = route as IReadOnlyCollection<IHttpRoute>;
if (routeCollectionRoute != null)
{
dataTokens = routeCollectionRoute
.Select(r => r.DataTokens)
.SelectMany(dt => dt)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
else
{
dataTokens = route.DataTokens;
}
var actionDescriptors = dataTokens
.Select(dt => dt.Value)
.Where(dt => dt is IEnumerable<HttpActionDescriptor>)
.Cast<IEnumerable<HttpActionDescriptor>>()
.SelectMany(r => r)
.ToList();
return actionDescriptors.FirstOrDefault();
}
}
}
Program.cs:
internal class Program
{
private static readonly ILog Logger;
static Program()
{
Log.Logger = new LoggerConfiguration()
.WriteTo
.LiterateConsole()
.MinimumLevel.Is(LogEventLevel.Verbose)
.CreateLogger();
Logger = LogProvider.GetCurrentClassLogger();
}
internal static void Main(string[] args)
{
var builder = new ContainerBuilder();
builder.RegisterModule(new LogRequestModule());
builder.RegisterApiControllers(typeof(AController).Assembly);
builder.RegisterApiControllers(typeof(BController).Assembly);
var container = builder.Build();
var config = GetHttpConfig();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
var options = new StartOptions();
options.Urls.Add("http://localhost:1234");
options.Urls.Add("http://localhost:5678");
var listener = WebApp.Start(options, app =>
{
app.Use((ctx, next) =>
{
if (ctx.Request.LocalPort.HasValue)
{
var port = ctx.Request.LocalPort.Value;
string apiControllersAssemblyName = null;
if (port == 1234)
{
apiControllersAssemblyName = typeof(AController).Assembly.FullName;
}
else if (port == 5678)
{
apiControllersAssemblyName = typeof(BController).Assembly.FullName;
}
ctx.Set("ApiControllersAssembly", apiControllersAssemblyName);
Logger.Info($"{nameof(WebApp)}: Port = {port}, ApiControllersAssembly = {apiControllersAssemblyName}");
}
return next();
});
app.UseAutofacMiddleware(container);
app.UseAutofacWebApi(config);
app.UseWebApi(config);
});
Logger.Info(@"Press [Enter] to exit");
Console.ReadLine();
listener.Dispose(); ;
}
private static HttpConfiguration GetHttpConfig()
{
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
config.Services.Add(typeof(IExceptionLogger), new LogProviderExceptionLogger());
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Services.Replace(typeof(IHttpControllerSelector), new CustomHttpControllerSelector(config));
config.Services.Replace(typeof(IHttpActionSelector), new CustomHttpActionSelector());
var traceSource = new TraceSource("LibLog") { Switch = { Level = SourceLevels.All } };
traceSource.Listeners.Add(new LibLogTraceListener());
var diag = config.EnableSystemDiagnosticsTracing();
diag.IsVerbose = false;
diag.TraceSource = traceSource;
return config;
}
}
LibA\Controllers\AController.cs:
[RoutePrefix("api/app")]
public class AController : ApiController
{
private static readonly ILog Logger;
static AController()
{
Logger = LogProvider.GetCurrentClassLogger();
Logger.Debug($"{nameof(AController)}: Static Constructor");
}
public AController()
{
Logger.Debug($"{nameof(AController)}: Constructor");
}
[HttpGet, Route("test")]
public async Task<IHttpActionResult> Get()
{
Logger.Debug($"{nameof(AController)}: Get()");
return Ok($"Hello from {nameof(AController)}");
}
}
LibB\Controllers\BController.cs:
[RoutePrefix("api/app")]
public class BController : ApiController
{
private static readonly ILog Logger;
static BController()
{
Logger = LogProvider.GetCurrentClassLogger();
Logger.Debug($"{nameof(BController)}: Static Constructor");
}
public BController()
{
Logger.Debug($"{nameof(BController)}: Constructor");
}
[HttpGet, Route("{*path}")]
public async Task<IHttpActionResult> Get([FromUri] string path)
{
if (path == null)
{
path = Request.RequestUri.PathAndQuery.Split(new[] {"api/app/"}, StringSplitOptions.RemoveEmptyEntries)[1];
}
Logger.Debug($"{nameof(BController)}: Get({path})");
using (var client = new HttpClient {BaseAddress = new Uri("http://localhost:1234/api/app/")})
{
var result = await client.GetAsync(path);
var content = await result.Content.ReadAsStringAsync();
return Ok($"(From {nameof(BController)}): {content}");
}
}
}
有空的时候我可能会再试一次。
如果你有任何进展,请告诉我!