Modifica la raccolta del percorso di MVC6 dopo l’avvio

In MVC-5 ho potuto modificare la routetable dopo l’avvio iniziale accedendo a RouteTable.Routes . Desidero fare lo stesso in MVC-6 in modo da poter aggiungere / eliminare percorsi durante il runtime (utile per CMS).

Il codice per farlo in 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 } ); } 

Ma non riesco a trovare RouteTable.Routes o qualcosa di simile in MVC-6. Qualche idea su come posso ancora cambiare la collezione del percorso durante il runtime?


Voglio usare questo principio per aggiungere, per esempio, un url extra quando viene creata una pagina nel CMS.

Se hai una class come:

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

E un controller come:

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

Quindi, quando una pagina viene aggiunta al database, ricrei 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 } ); } 

In questo modo posso aggiungere pagine al CMS che non appartengono a convenzioni o modelli rigidi. Posso aggiungere una pagina con url /contact , ma anche una pagina con url /help/faq/how-does-this-work .

La risposta è che non esiste un modo ragionevole per farlo, e anche se trovi un modo non sarebbe una buona pratica.

Un approccio errato al problema

Fondamentalmente, la configurazione del percorso delle versioni MVC passate doveva agire come una configurazione DI: cioè, si inserisce tutto nella composizione root e quindi si utilizza tale configurazione durante il runtime. Il problema era che si potevano spingere oggetti nella configurazione in fase di esecuzione (e molte persone lo facevano), che non è l’approccio giusto.

Ora che la configurazione è stata sostituita da un vero contenitore DI, questo approccio non funzionerà più. La fase di registrazione ora può essere eseguita solo all’avvio dell’applicazione.

L’approccio corretto

L’approccio corretto alla personalizzazione del routing ben oltre ciò che la class Route poteva fare nelle versioni MVC passate era di ereditare RouteBase o Route.

MVC 6 ha astrazioni simili, IRouter e INamedRouter che riempiono lo stesso ruolo. Proprio come il suo predecessore, IRouter ha solo due metodi da implementare.

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

Questa interfaccia è dove implementate la natura bidirezionale del routing: l’URL per instradare i valori e indirizzare i valori all’URL.

Un esempio: CachedRoute

Ecco un esempio che tiene traccia e memorizza nella cache una mapping 1-1 della chiave primaria all’URL. È generico e ho verificato che funzioni sia che la chiave primaria sia int o Guid .

C’è un componente collegabile che deve essere iniettato, ICachedRouteDataProvider dove è ansible implementare la query per il database. È inoltre necessario fornire il controller e l’azione, pertanto questa route è sufficientemente generica per mappare più query di database a più metodi di azione utilizzando più di un’istanza.

 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

Questa è l’implementazione del fornitore di dati che fondamentalmente è ciò che devi fare nel tuo 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

E qui aggiungiamo il percorso prima del percorso predefinito e ne configuriamo le opzioni.

 // 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?}"); }); 

Questo è il succo di ciò. Potresti ancora migliorare un po ‘le cose.

Personalmente utilizzerei un modello factory e inserisco il repository nel costruttore di CmsCachedRouteDataProvider piuttosto che nel codice DbContext , ovunque, ad esempio.