具有自己的一组控制器的多个 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 中有一个丑陋的解决方法。如果您需要做的只是将请求从一台主机转发到另一台主机,这可能就足够了。

最后的结果是:

GEThttp://localhost:1234/api/app/test returns "HellofromAController" (直接调用 AController)

GET to http://localhost:5678/api/app/test returns "(FromBController): \"HellofromAController\"" (调用 BController,它将请求转发给 AController)

full source on github

我保留了日志记录代码以防它有用,但它与解决方案无关。

废话少说:

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}");
        }
    }
}

有空的时候我可能会再试一次。

如果你有任何进展,请告诉我!