c# - net - routes maproute parameters
Cambiar la colección de rutas de MVC6 después del inicio (1)
La respuesta es que no hay una forma razonable de hacer esto, e incluso si encuentra una manera, no sería una buena práctica.
Un enfoque incorrecto del problema
Básicamente, la configuración de ruta de las versiones anteriores de MVC debía actuar como una configuración DI, es decir, coloca todo allí en la raíz de composición y luego usa 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 se puede realizar 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
podía hacer en versiones anteriores de MVC era
heredar RouteBase
o Route.
AspNetCore (anteriormente conocido como MVC 6) tiene abstracciones similares,
IRouter
e
INamedRouter
que
INamedRouter
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 a valores de ruta y valores de ruta a URL.
Un ejemplo:
CachedRoute<TPrimaryKey>
Aquí hay un ejemplo que rastrea y almacena en caché una asignación 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 conectable que debe inyectarse,
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<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));
}
}
CmsCachedRouteDataProvider
Esta es la implementación del proveedor de datos que es básicamente lo que necesita hacer en su 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);
}
}
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<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?}");
});
Esa es la esencia de esto. Aún podrías mejorar un poco las cosas.
Yo personalmente usaría un patrón de fábrica e inyectaría el repositorio en el constructor de
CmsCachedRouteDataProvider
lugar de codificar
DbContext
todas partes, por ejemplo.
En MVC-5 podría 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 rutas durante el tiempo de ejecución?
Quiero utilizar este principio para agregar, por ejemplo, una URL adicional 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, vuelvo a crear la
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 manera 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
.