Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple owin listeners with their own set of controllers, with Autofac for DI

I am trying to use multiple in-process owin listeners. Each should have a distinct set of controllers, where they may have the same route handled by a different controller. For instance

localhost:1234/api/app/test should resolve to ControllerA

localhost:5678/api/app/test should resolve to ControllerB

controller a, in owin host 1, has route attribute

[Route("api/app/test")]

controller b, in owin host 2, has route attribute

[Route("api/app/{*path}")]

and is used to forward requests to the other owin host.

We are using Autofac for dependency injection. Routes are configured through attribute routing. autofac requires a line such as

builder.RegisterApiControllers(typeof(ControllerA).Assembly)

Our OWIN configuration contains:

var config = ConfigureWebApi(); // Configure Autofac config.DependencyResolver = new AutofacWebApiDependencyResolver(container); app.UseAutofacMiddleware(container); app.UseAutofacWebApi(config); app.UseWebApi(config);

However when starting two listeners, I need to include both assemblies for controller resolving. This leads to a 'duplicate route' exception:

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"

When running the OWIN listeners in separate processes, there are no issues.

I have also tried to use multiple DI containers, one for each OWIN listener, but that conflicts with Web Api 2 as it requires GlobalConfiguration.Configuration.DependencyResolver to be set. Which conflicts with the concept of multiple DI containers.

Can someone guide me how to configure such a setup?

like image 526
gd73 Avatar asked Sep 29 '16 13:09

gd73


1 Answers

Use the OWIN environment and customize the HttpControllerSelector

Using the OWIN pipeline you can pass information about the request to a custom HttpControllerSelector. This allows you to be selective about which controllers are used to match which routes.

Of course this is easier said than done. The inner workings of WebAPI with respect to routing are not very transparent - source code is often the best documentation in this area.

I could not get the HttpControllerSelector to fully work, so there's an ugly workaround in CustomHttpActionSelector. It may still be sufficient if all you need to do is forward requests from one host to the other.

The end result is:

GET to http://localhost:1234/api/app/test returns "HellofromAController" (directly invokes AController)

GET to http://localhost:5678/api/app/test returns "(FromBController): \"HellofromAController\"" (invokes BController, which forwards the request to AController)

See the full source on github

I left the logging code as-is in case it's useful, but it's not relevant to the solution.

So without further ado:

CustomHttpControllerSelector.cs:

Uses the port-specific OWIN env variable ApiControllersAssembly in to filter the controllers.

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:

You shouldn't need a CustomHttpActionSelector, this only exists to work around an issue with the ActionDescriptors for BController. It works as long as BController has only one method, otherwise you'll need to implement some route-specific logic.

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

I might have another go at it when I have more time.

Let me know if you make any progress!

like image 61
Fred Kleuver Avatar answered Oct 18 '22 01:10

Fred Kleuver