【问题标题】:How can I do a virtual route/redirect based on the path given in ASP.NET VNEXT MVC6?如何根据 ASP.NET VNEXT MVC6 中给出的路径进行虚拟路由/重定向?
【发布时间】:2015-03-18 05:30:52
【问题描述】:

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

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

我的计划是使用虚拟重定向之类的东西,所有设备都会调用一个固定的 URL,例如 example.com/Api/VRedirect/{deviceId}/MethodName?param1=test

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

例如,如果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 条路由,没有发现任何明显的减速。

【问题讨论】:

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


    【解决方案1】:

    首先,如果这些约束是静态的并且不会更改,或者不经常更改,我不会在每个请求时查找它们,而是在应用程序启动时查找它们,然后将数据缓存在 HttpContext.Cache 或 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 没有自行清理的设施,因此它基本上会随着时间的推移而增长和增长其内存使用量。尽管在这种情况下,如果您总是查询相同的数据集,这可能会受到限制.

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

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

    但是,如果您对应用程序生命周期中的上下文(只有一个)感到满意,那么这是一个非常简单的解决方案。

    另外,你真的应该使用接口隔离原则来减少依赖。这仍然会创建对实际 VRouterDbContext 的依赖,因此不能轻松地对其进行模拟和测试...所以改为添加一个接口。

    【讨论】:

    • 非常感谢,这很好用。路由确实很少更新,但它们确实需要在运行时更新而无需重新启动应用程序,因此我可能会按照您的建议实现每 x 分钟到期的缓存。 DbContext 的长寿命实例有点令人担忧,但考虑到它只读取很少更改的数据,这应该不是问题。
    • @Ryan - 新的基于约定的配置的巧妙之处之一是您可以像这样添加新参数,它会为您注入它们。对于标准操作方法,它不会这样做,但是哦。但是,即使它没有提供这个不错的功能,我们仍然可以使用服务位置......我很喜欢在这样的配置节点中使用服务位置,因为我仍然认为它是组合根的模糊部分(虽然我'肯定马克·西曼会强烈反对)。
    • @Ryan - 如果长期存在的上下文成为问题,请告诉我,我们可以找到解决方案。
    • 在我的笔记本电脑上使用 IIS 进行测试时,它工作正常,但是当我使用 Kestrel Web 服务器在慢速 Linux 机器上测试它时,我会收到一个数据库错误(调用堆栈越来越丢失所以我无法得到实际的错误,但我认为这与并发有关)。我认为来自不同线程的两个请求同时在约束中调用 Match ,它同时访问相同的 DbContext 。我似乎通过添加一个锁定对象并锁定 DbContext 访问来修复错误。
    • @Ryan - InMemoryStore 仅用于单元测试,您真的不应该将它用于应用程序目的。它没有任何普通数据库的并发支持。
    【解决方案2】:

    为此,您必须使用HttpClient 之类的东西,然后为您想要的资源创建一个发布请求。事实上,您也可以将HttpClient 用于 GET 请求,它们将在 100% 的时间内工作。

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

    【讨论】:

    • 我考虑过将所有东西都移到一个控制器中,但是每个现有的控制器都使用不同的 DbContexts,这些 DbContexts 被注入到控制器中,我希望将它们分隔开来。 HttpClient 可能是要走的路,但它是完成解决方案的迂回方式。我认为最终处理这些路径的自定义 IRouter 可能是要走的路,但我目前不知道如何实现。
    【解决方案3】:

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

    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。因此,例如,如果您希望 /Home/About 处理 /1234/About,您可以将“Home”指定为 ControllerShortName 并将 YourProject.Controllers.HomeController 指定为完全限定名称。请注意,如果控制器不在当前执行的程序集中,则必须添加程序集名称。

    完成以上操作后,只需在Global.asax注册即可:

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

    【讨论】:

    • 感谢您的回答。不幸的是,我不能在 Global.asax 中使用 ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));,因为 MVC6 应用程序不再使用 Global.asax 文件,但我已经接受了您的回答,直到我可以根据它最终确定自己的解决方案。
    【解决方案4】:

    所以今天我有了某种顿悟,并意识到这实际上可以通过使用路线约束相当简单地完成。

    为每个要使用的控制器注册一个路由:

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

    上面的代码对每个控制器重复一次,通过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 被解析为deviceId,并针对它调用VRouterConstraint 中的Match() 方法。 Match() 方法在数据库中查找设备123ABC 是否已注册到路由链接到的控制器(在本例中为Example),如果是,则返回True

    【讨论】:

    • 您的路线是静态的吗?或者他们可以在运行时改变?意思是,你能假设当应用程序启动时,所有路由都已定义,所有 deviceid 都存在,并且这些不会改变吗?或者,您是否可以假设它们不会频繁更改,因此可以缓存一段时间并每 x 分钟/小时刷新一次,等等。
    猜你喜欢
    • 2017-03-17
    • 1970-01-01
    • 1970-01-01
    • 2011-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-29
    相关资源
    最近更新 更多