如何根据ASP.NET VNEXT MVC6中给出的路径进行虚拟路由/重定向?

时间:2015-01-20 04:19:07

标签: c# asp.net asp.net-mvc routes asp.net-core

我有一个网站,它在不同的路径上公开了几个API,每个路由一个特定于应用程序部分的控制器处理,比如example.com/Api/Controller/Action?param1=stuff,其中Controller发生了变化,但是动作保持相当一致。

我有几个调用这些API的集成设备。问题是这些集成设备无法轻易更改,我希望它们指向的特定控制器将来需要更改。

我的计划是使用类似虚拟重定向的内容,其中所有设备都会调用固定网址,例如example.com/Api/VRedirect/{deviceId}/MethodName?param1=test

根据deviceId的值,使用的实际Controller会发生变化(基于某些数据库查找逻辑)。

例如,如果deviceId 1234被查找并返回“示例”,则调用example.com/Api/VRedirect/1234/Test?param1=test将等同于直接调用example.com/Api/Example/Test?param1=test

到目前为止,我发现无法正确实现这一点,我接近的唯一方法是使用自定义路由:

app.UseMvc(routes => {
    routes.MapRoute(
                    name: "RedirectRoute",
                    template: "Api/VRedirect/{deviceId}/{*subAction}",
                    defaults: new { controller = "BaseApi", action = "VRedirect"});
);

带有重定向操作:

public IActionResult VRedirect(string deviceId, string subAction) {
        string controllerName = "Example"; // Database lookup based off deviceId
        return Redirect(string.Format("/Api/{0}/{1}", controllerName, subAction));
    }

这部分适用于GET请求,但根本不适用于POST,因为它会丢弃任何和所有POST数据。

有没有办法实现这样的东西?我怀疑我可能要写一个自定义路由器,但我不知道从哪里开始。

更新 我已经设法使用默认路由器完成所需的行为,只需在循环中为每个设备添加路由:

app.UseMvc(routes => {
    Dictionary<string, string> deviceRouteAssignments = new Dictionary<string, string>();
    // TODO: Get all these assignments from a database
    deviceRouteAssignments.Add("12345", "ExampleControllerName");
    foreach (var thisAssignment in deviceRouteAssignments) {
        routes.MapRoute(
            name: "DeviceRouteAssignment_" + thisAssignment.Key,
            template: "Api/VRedirect/" + thisAssignment.Key + "/{action}",
            defaults: new { controller = thisAssignment.Value });
        }
    }
}

然而,这有一些明显的限制,例如路由仅在应用程序启动时更新。大量路线的性能下降可能是一个问题,但我已经测试了10,000条路线未发现任何可察觉的减速。

4 个答案:

答案 0 :(得分:2)

首先,如果这些约束是静态的并且不会改变,或者不经常更改,我不会在每个请求上查找它们,而是在应用程序启动时查找它们,然后将数据缓存在HttpContext中。缓存或Redis或其他一些缓存机制,允许您绕过每个请求的外观。如果可以定期更新它们,请设置时间限制,并在缓存逐出时重新加载新的条目集。

请记住,您拥有的路由越多,您在最坏情况下的数据库查找次数就越多。因此,即使您需要查找每个请求,更好的解决方案是在每个请求上更新缓存。

但是,如果您必须在每个约束中的每个请求上执行此操作,那么您可以简单地执行此操作:

public void ConfigureServices(IServiceCollection services)
{
    services.AddEntityFramework(Configuration)
        .AddSqlServer()
        .AddDbContext<VRouterDbContextt>();
    //...
 }

// Note: I added the DbContext here (and yes, this does in fact work)...
public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
     ILoggerFactory loggerfactory, VRouterDbContext context)
{
    // ....

     app.UseMvc(routes =>
     {
         routes.MapRoute(
             name: "VRoute_" + "Example",
             template: "Api/VRouter/{deviceId}/{action}",
             defaults: new { controller = "Example"},
             constraints: new { deviceId = new VRouterConstraint(context, "Example")}
        });
}

public class VRouterConstraint : IRouteConstraint {
    public VRouterConstraint (VRouterDbContext context, string controllerId) {
       this.DbContext = context;
       this.ControllerId = controllerId;
    }

    private VRouterDbContext DbContext {get; set;}
    public string ControllerId{ get; set; }

    public bool Match(HttpContext httpContext, IRouter route, string routeKey, 
        IDictionary<string, object> values, RouteDirection routeDirection) {
        object deviceIdObject;
        if (!values.TryGetValue(routeKey, out deviceIdObject)) {
            return false;
        }

        string deviceId = deviceIdObject as string;
        if (deviceId == null) {
            return false;
        }

        bool match = DbContext.DeviceServiceAssociations
            .AsNoTracking()
            .Where(o => o.ControllerId == this.ControllerId)
            .Any(o => o.AssoicatedDeviceId == deviceId);
        return match;
    }
}

因此,这是为手动创建的RouteConstraints提供注入存储库的一种相当简单的方法。

然而,有一个小问题是DbContext必须在应用程序的生命周期中存在,并且DbContexts并不是真正的生活方式。除了处理上下文本身之外,DbContexts无法自行清理,因此随着时间的推移,它将基本上增长并增加其内存使用量。尽管在这种情况下,如果您总是查询相同的数据集,这可能会受到限制

这是因为您的路径约束是在应用启动时创建的,并且在应用程序的生命周期中存在,并且在创建约束时必须注入您的上下文(尽管有一些方法,但它们可能不会是最好的解决方案...例如你可以做一个优化,它会注入一个创建你的上下文的工厂,但现在你绕过了容器的生命周期管理。你也可以使用服务位置,有时你不会#39; t有很多选择..但我留下了最后的手段)。

这就是为什么在启动时查询数据库并缓存数据要比在每个请求上执行这些类型的查询要好得多。

但是,如果你对应用程序的生命周期(只有一个)生活很好,那么这是一个非常简单的解决方案。

此外,您确实应该使用接口隔离原则来减少依赖性。这仍然会对实际的VRouterDbContext产生依赖性,因此无法轻易进行模拟和测试......所以请添加一个接口。

答案 1 :(得分:1)

要使其适用于POST请求,您必须使用HttpClient之类的内容,并为所需资源创建发布请求。实际上,您也可以使用HttpClient来获取GET请求,并且它们将在100%的时间内正常工作。

但是如果你想调用外部API,这样做是有益的。如果您只想调用内部资源,最好使用其他模式。例如,您是否考虑过除BaseApiController以外的所有控制器?从设备收到请求并希望将处理委托给其他类后,它不必是控制器类。您可以使用Activator.CreateInstance(或者更好,使用DI容器来实例化类)来创建所需POCO类的实例,并调用它所需的方法。

答案 2 :(得分:1)

在进一步思考后,以下内容对您也有用:

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override Type GetControllerType(RequestContext requestContext, string controllerName)
    {
        var controllerToken = requestContext.RouteData.GetRequiredString("controller");
        var context = new DbContext();
        var mappedRoute = context.RouteMaps.FirstOrDefault(r => r.DeviceId == controllerToken);
        if(mappedRoute == null) return base.GetControllerType(requestContext, controllerName);

        requestContext.RouteData.Values["controller"] = mappedRoute.ControllerShortName; //Example: "Home";
        return Type.GetType(mappedRoute.FullyQualifiedName);  //Example: "Web.Controllers.HomeController"
    }
}

如您所见,您的数据库表至少包含三列,DeviceIdControllerShortNameFullyQualifiedName。因此,例如,如果您希望/ 1234 /关于由/ Home / About处理,您将指定&#34; Home&#34;以ControllerShortNameYourProject.Controllers.HomeController作为完全限定名称。请注意,如果控制器不在当前正在执行的程序集中,则必须添加程序集名称。

完成上述操作后,您只需在Global.asax注册:

ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));

答案 3 :(得分:0)

所以今天我有各种各样的顿悟,并意识到这实际上可以通过使用路线约束来实现。

注册了每个要使用的控制器的单一路径:

routes.MapRoute(
    name: "VRoute_" + "Example",
    template: "Api/VRouter/{deviceId}/{action}",
    defaults: new { controller = "Example"},
    constraints: new { deviceId = new VRouterConstraint("Example") }
);

上述代码对每个Controller重复一次,通过for循环或其他方法(在这种情况下只注册了ExampleController

注意为deviceId指定的路由约束。为了触发路由,VRouterConstraint必须在deviceId参数上注册匹配。

VRouterConstraint看起来像:

public class VRouterConstraint : IRouteConstraint {
    public VRouterConstraint (string controllerId) {
        this.ControllerId= controllerId;
    }

    public string ControllerId{ get; set; }

    public bool Match(HttpContext httpContext, IRouter route, string routeKey, IDictionary<string, object> values, RouteDirection routeDirection) {
        object deviceIdObject;
        if (!values.TryGetValue(routeKey, out deviceIdObject)) {
            return false;
        }

        string deviceId = deviceIdObject as string;
        if (deviceId == null) {
            return false;
        }

        bool match = false;

        using (VRouterDbContext vRouterDb = new VRouterDbContext ()) {
            match = vRouterDb.DeviceServiceAssociations
                .AsNoTracking()
                .Where(o => o.ControllerId == this.ControllerId)
                .Any(o => o.AssoicatedDeviceId == deviceId);
        }

        return match;
    }
}

因此,当设备转到地址Api/VRouter/ABC123/Test时,ABC123会被解析为deviceIdMatch()内的VRouterConstraint方法会被调用它。 Match()方法对数据库进行查找,以查看设备123ABC是否已注册到路由器链接到的Controller(在本例中为Example),如果是,则返回{ {1}}。