启动后更改MVC6的路由收集
在MVC-5中,我可以通过访问RouteTable.Routes
在初始启动后编辑routetable
。 我希望在MVC-6中做同样的事情,这样我可以在运行时添加/删除路由(对CMS有用)。
MVC-5中的代码是:
using (RouteTable.Routes.GetWriteLock()) { RouteTable.Routes.Clear(); RouteTable.Routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); RouteTable.Routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
但是我无法在MVC-6中findRouteTable.Routes
或类似的东西。 任何想法如何我仍然可以在运行时更改路线集合?
我想要使用这个原则,例如,在CMS中创build页面时添加额外的URL。
如果你有一个类像:
public class Page { public int Id { get; set; } public string Url { get; set; } public string Html { get; set; } }
和一个控制器一样:
public class CmsController : Controller { public ActionResult Index(int id) { var page = DbContext.Pages.Single(p => p.Id == id); return View("Layout", model: page.Html); } }
然后,当一个页面被添加到数据库时,我重新创build了routecollection
:
var routes = RouteTable.Routes; using (routes.GetWriteLock()) { routes.Clear(); foreach(var page in DbContext.Pages) { routes.MapRoute( name: Guid.NewGuid().ToString(), url: page.Url.TrimEnd('/'), defaults: new { controller = "Cms", action = "Index", id = page.Id } ); } var defaultRoute = routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
通过这种方式,我可以将页面添加到不属于约定或严格模板的CMS中。 我可以添加一个页面的url/contact
,但也是一个url/help/faq/how-does-this-work
。
答案是没有合理的方法来做到这一点,即使你find了一个办法,这也不是一个好的做法。
错误的方法来解决问题
基本上,过去的MVC版本的路由configuration意味着像DIconfiguration – 就是说,您将所有内容放在组合根中 ,然后在运行时使用该configuration。 问题是你可以在运行时将对象推入configuration中(而且很多人都这样做),这不是正确的方法。
现在configuration已被真正的DI容器取代,这种方法将不再起作用。 注册步骤现在只能在应用程序启动时完成。
正确的方法
定制路由的正确方法远远超过了Route
类在过去的MVC版本中可以做的事,就是inheritanceRouteBase或Route。
MVC 6有相似的抽象, IRouter和INamedRouter填充相同的angular色。 就像它的前身一样, IRouter
只有两种实现方法。
namespace Microsoft.AspNet.Routing { public interface IRouter { // Derives a virtual path (URL) from a list of route values VirtualPathData GetVirtualPath(VirtualPathContext context); // Populates route data (including route values) based on the // request Task RouteAsync(RouteContext context); } }
这个接口是你实现路由的双向本质的地方 – URL来路由值和路由值到URL。
示例: CachedRoute<TPrimaryKey>
这是一个跟踪和caching主键到URL的1-1映射的例子。 它是通用的,我已经testing过,无论主键是int
还是Guid
,它都能正常工作。
有一个必须注入的可插入块, ICachedRouteDataProvider
可以实现对数据库的查询。 您还需要提供控制器和操作,所以此路由通用性足以通过使用多个实例将多个数据库查询映射到多个操作方法。
using Microsoft.AspNet.Routing; using Microsoft.Framework.Caching.Memory; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; public class CachedRoute<TPrimaryKey> : IRouter { private readonly string _controller; private readonly string _action; private readonly ICachedRouteDataProvider<TPrimaryKey> _dataProvider; private readonly IMemoryCache _cache; private readonly IRouter _target; private readonly string _cacheKey; private object _lock = new object(); public CachedRoute( string controller, string action, ICachedRouteDataProvider<TPrimaryKey> dataProvider, IMemoryCache cache, IRouter target) { if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException("controller"); if (string.IsNullOrWhiteSpace(action)) throw new ArgumentNullException("action"); if (dataProvider == null) throw new ArgumentNullException("dataProvider"); if (cache == null) throw new ArgumentNullException("cache"); if (target == null) throw new ArgumentNullException("target"); _controller = controller; _action = action; _dataProvider = dataProvider; _cache = cache; _target = target; // Set Defaults CacheTimeoutInSeconds = 900; _cacheKey = "__" + this.GetType().Name + "_GetPageList_" + _controller + "_" + _action; } public int CacheTimeoutInSeconds { get; set; } public async Task RouteAsync(RouteContext context) { var requestPath = context.HttpContext.Request.Path.Value; if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/') { // Trim the leading slash requestPath = requestPath.Substring(1); } // Get the page id that matches. TPrimaryKey id; //If this returns false, that means the URI did not match if (!GetPageList().TryGetValue(requestPath, out id)) { return; } //Invoke MVC controller/action var oldRouteData = context.RouteData; var newRouteData = new RouteData(oldRouteData); newRouteData.Routers.Add(_target); // TODO: You might want to use the page object (from the database) to // get both the controller and action, and possibly even an area. // Alternatively, you could create a route for each table and hard-code // this information. newRouteData.Values["controller"] = _controller; newRouteData.Values["action"] = _action; // This will be the primary key of the database row. // It might be an integer or a GUID. newRouteData.Values["id"] = id; try { context.RouteData = newRouteData; await _target.RouteAsync(context); } finally { // Restore the original values to prevent polluting the route data. if (!context.IsHandled) { context.RouteData = oldRouteData; } } } public VirtualPathData GetVirtualPath(VirtualPathContext context) { VirtualPathData result = null; string virtualPath; if (TryFindMatch(GetPageList(), context.Values, out virtualPath)) { result = new VirtualPathData(this, virtualPath); context.IsBound = true; } return result; } private bool TryFindMatch(IDictionary<string, TPrimaryKey> pages, IDictionary<string, object> values, out string virtualPath) { virtualPath = string.Empty; TPrimaryKey id; object idObj; object controller; object action; if (!values.TryGetValue("id", out idObj)) { return false; } id = SafeConvert<TPrimaryKey>(idObj); values.TryGetValue("controller", out controller); values.TryGetValue("action", out action); // The logic here should be the inverse of the logic in // RouteAsync(). So, we match the same controller, action, and id. // If we had additional route values there, we would take them all // into consideration during this step. if (action.Equals(_action) && controller.Equals(_controller)) { // The 'OrDefault' case returns the default value of the type you're // iterating over. For value types, it will be a new instance of that type. // Since KeyValuePair<TKey, TValue> is a value type (ie a struct), // the 'OrDefault' case will not result in a null-reference exception. // Since TKey here is string, the .Key of that new instance will be null. virtualPath = pages.FirstOrDefault(x => x.Value.Equals(id)).Key; if (!string.IsNullOrEmpty(virtualPath)) { return true; } } return false; } private IDictionary<string, TPrimaryKey> GetPageList() { IDictionary<string, TPrimaryKey> pages; if (!_cache.TryGetValue(_cacheKey, out pages)) { // Only allow one thread to poplate the data lock (_lock) { if (!_cache.TryGetValue(_cacheKey, out pages)) { pages = _dataProvider.GetPageToIdMap(); _cache.Set(_cacheKey, pages, new MemoryCacheEntryOptions() { Priority = CacheItemPriority.NeverRemove, AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(this.CacheTimeoutInSeconds) }); } } } return pages; } private static T SafeConvert<T>(object obj) { if (typeof(T).Equals(typeof(Guid))) { if (obj.GetType() == typeof(string)) { return (T)(object)new Guid(obj.ToString()); } return (T)(object)Guid.Empty; } return (T)Convert.ChangeType(obj, typeof(T)); } }
CmsCachedRouteDataProvider
这是数据提供者的实现,基本上你需要在你的CMS中做什么。
public interface ICachedRouteDataProvider<TPrimaryKey> { IDictionary<string, TPrimaryKey> GetPageToIdMap(); } public class CmsCachedRouteDataProvider : ICachedRouteDataProvider<int> { public IDictionary<string, int> GetPageToIdMap() { // Lookup the pages in DB return (from page in DbContext.Pages select new KeyValuePair<string, int>( page.Url.TrimStart('/').TrimEnd('/'), page.Id) ).ToDictionary(pair => pair.Key, pair => pair.Value); } }
用法
在这里,我们在默认路由之前添加路由,并configuration其选项。
// Add MVC to the request pipeline. app.UseMvc(routes => { routes.Routes.Add( new CachedRoute<int>( controller: "Cms", action: "Index", dataProvider: new CmsCachedRouteDataProvider(), cache: routes.ServiceProvider.GetService<IMemoryCache>(), target: routes.DefaultHandler) { CacheTimeoutInSeconds = 900 }); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); // Uncomment the following line to add a route for porting Web API 2 controllers. // routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}"); });
这是它的要点。 你仍然可以改进一点。
首先,我会亲自使用一个存储库模式,并将存储库注入到CmsCachedRouteDataProvider
的构造函数中,而不是在任何地方对CmsCachedRouteDataProvider
硬编码。
其次,您可以使用Microsoft.Framework.Runtime.Caching.ICache
,它接受caching依赖关系,并且每次更新Pages
表时都有可能导致caching失效。 然后,用户不必等待15分钟就可以看到更改。 不过,没有文档,我不能解决如何使用caching依赖关系。