Cambiar la recolección de ruta de MVC6 después del inicio

En MVC-5 pude editar la routetable después del inicio inicial accediendo a RouteTable.Routes . Deseo hacer lo mismo en MVC-6 para poder agregar / eliminar rutas durante el tiempo de ejecución (útil para CMS).

El código para hacerlo en MVC-5 es:

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

Pero no puedo encontrar RouteTable.Routes o algo similar en MVC-6. ¿Alguna idea de cómo puedo cambiar la colección de ruta durante el tiempo de ejecución?


Quiero utilizar este principio para agregar, por ejemplo, una url extra cuando se crea una página en el CMS.

Si tienes una clase como:

 public class Page { public int Id { get; set; } public string Url { get; set; } public string Html { get; set; } } 

Y un controlador como:

 public class CmsController : Controller { public ActionResult Index(int id) { var page = DbContext.Pages.Single(p => p.Id == id); return View("Layout", model: page.Html); } } 

Luego, cuando se agrega una página a la base de datos, recreo el conjunto de 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 } ); } 

De esta forma, puedo agregar páginas al CMS que no pertenecen a convenciones o plantillas estrictas. Puedo agregar una página con url /contact , pero también una página con url /help/faq/how-does-this-work .

La respuesta es que no hay una forma razonable de hacerlo, e incluso si encuentra una forma, no sería una buena práctica.

Un enfoque incorrecto al problema

Básicamente, la configuración de ruta de las versiones MVC pasadas debía actuar como una configuración DI, es decir, colocas todo allí en la raíz de la composición y luego usas esa configuración durante el tiempo de ejecución. El problema era que podía insertar objetos en la configuración en tiempo de ejecución (y muchas personas lo hicieron), lo cual no es el enfoque correcto.

Ahora que la configuración ha sido reemplazada por un verdadero contenedor DI, este enfoque ya no funcionará. El paso de registro ahora solo puede hacerse al inicio de la aplicación.

El enfoque correcto

El enfoque correcto para personalizar el enrutamiento mucho más allá de lo que la clase Route podría hacer en versiones anteriores de MVC era heredar RouteBase o Route.

MVC 6 tiene abstracciones similares, IRouter e INamedRouter que cumplen el mismo rol. Al igual que su predecesor, IRouter solo tiene dos métodos para implementar.

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

Esta interfaz es donde implementa la naturaleza bidireccional del enrutamiento: URL para enrutar valores y enrutar valores a URL.

Un ejemplo: CachedRoute

Aquí hay un ejemplo que rastrea y almacena en caché una asignación de 1-1 de la clave principal a la URL. Es genérico y he probado que funciona si la clave primaria es int o Guid .

Hay una pieza enchufable que se debe inyectar, ICachedRouteDataProvider , donde se puede implementar la consulta de la base de datos. También debe proporcionar el controlador y la acción, por lo que esta ruta es lo suficientemente genérica como para asignar múltiples consultas de bases de datos a múltiples métodos de acción mediante el uso de más de una instancia.

 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 : IRouter { private readonly string _controller; private readonly string _action; private readonly ICachedRouteDataProvider _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 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 pages, IDictionary 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(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 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 GetPageList() { IDictionary 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(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

Esta es la implementación del proveedor de datos que es básicamente lo que necesita hacer en su CMS.

 public interface ICachedRouteDataProvider { IDictionary GetPageToIdMap(); } public class CmsCachedRouteDataProvider : ICachedRouteDataProvider { public IDictionary GetPageToIdMap() { // Lookup the pages in DB return (from page in DbContext.Pages select new KeyValuePair( page.Url.TrimStart('/').TrimEnd('/'), page.Id) ).ToDictionary(pair => pair.Key, pair => pair.Value); } } 

Uso

Y aquí agregamos la ruta antes de la ruta predeterminada y configuramos sus opciones.

 // Add MVC to the request pipeline. app.UseMvc(routes => { routes.Routes.Add( new CachedRoute( controller: "Cms", action: "Index", dataProvider: new CmsCachedRouteDataProvider(), cache: routes.ServiceProvider.GetService(), 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?}"); }); 

Esa es la esencia de eso. Todavía podrías mejorar las cosas un poco.

Yo personalmente usaría un patrón de fábrica e inyectaría el repository en el constructor de CmsCachedRouteDataProvider lugar de codificar DbContext todas partes, por ejemplo.