在MVC-5中,我可以routetable通过访问来编辑初始启动后的内容RouteTable.Routes。我希望在MVC-6中执行相同的操作,因此我可以在运行时添加/删除路由(对于CMS来说,usefull)。
routetable
RouteTable.Routes
在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 } ); }
但是我RouteTable.Routes在MVC-6中找不到或类似的东西。知道我如何仍可以在运行时更改路由集合吗?
我想使用此原理在CMS中创建页面时添加例如额外的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); } }
然后,当页面添加到数据库时,我重新创建了routecollection:
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。
/contact
/help/faq/how-does-this-work
答案是没有合理的方法可以做到这一点,即使您找到一种方法也不是一个好习惯。
基本上,过去的MVC版本的路由配置旨在像DI配置一样工作- 也就是说,您将所有内容放在组合根目录中,然后在运行时使用该配置。问题是您 可以 在运行时将对象推送到配置中(很多人这样做),这不是正确的方法。
现在,配置已由真正的DI容器替换,此方法将不再起作用。现在只能在应用程序启动时完成注册步骤。
超越Route类在过去的MVC版本中所做的工作来自定义路由的正确方法是继承RouteBase或Route。
Route
AspNetCore(以前称为MVC 6)具有类似的抽象,IRouter和INamedRouter扮演相同的角色。与其前身很像,IRouter只有两种方法可以实现。
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); } }
在此接口中,您可以实现路由的2向性质-用于路由值的URL和用于URL的路由值。
CachedRoute<TPrimaryKey>
这是一个跟踪和缓存主键到URL的1-1映射的示例。它是通用的,我已经测试它工作的主键是否int还是Guid。
int
Guid
有一个必须插入的可插入部分,可以ICachedRouteDataProvider在其中实现对数据库的查询。您还需要提供控制器和操作,因此此路由足够通用,可以通过使用多个实例将多个数据库查询映射到多个操作方法。
ICachedRouteDataProvider
using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.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 routeData = context.RouteData; // 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. routeData.Values["controller"] = _controller; routeData.Values["action"] = _action; // This will be the primary key of the database row. // It might be an integer or a GUID. routeData.Values["id"] = id; await _target.RouteAsync(context); } public VirtualPathData GetVirtualPath(VirtualPathContext context) { VirtualPathData result = null; string virtualPath; if (TryFindMatch(GetPageList(), context.Values, out virtualPath)) { result = new VirtualPathData(this, virtualPath); } 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 (i.e. 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)); } }
这就是数据提供程序的实现,基本上是您需要在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); } }
在这里,我们在默认路由之前添加路由,并配置其选项。
// 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而不是DbContext在各处进行硬编码。
CmsCachedRouteDataProvider
DbContext