Изменить маршрутную коллекцию MVC6 после запуска
В MVC-5 я мог редактировать routetable
после первоначального запуска путем доступа RouteTable.Routes
, Я хочу сделать то же самое в 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 }
);
}
Но я не могу найти RouteTable.Routes
или что-то подобное в MVC-6. Есть идеи, как я могу изменить коллекцию маршрутов во время выполнения?
Я хочу использовать этот принцип для добавления, например, дополнительного URL при создании страницы в CMS.
Если у вас есть класс, как:
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
:
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
,
1 ответ
Ответ в том, что нет разумного способа сделать это, и даже если вы найдете способ, это не будет хорошей практикой.
Неправильный подход к проблеме
По сути, конфигурация маршрута прошлых версий MVC должна была действовать как конфигурация DI - то есть вы помещаете все туда в корень композиции, а затем используете эту конфигурацию во время выполнения. Проблема заключалась в том, что вы могли помещать объекты в конфигурацию во время выполнения (и многие это делали), что не является правильным подходом.
Теперь, когда конфигурация заменена настоящим контейнером DI, этот подход больше не будет работать. Шаг регистрации теперь можно выполнить только при запуске приложения.
Правильный подход
Правильный подход к настройке маршрутизации далеко за пределы Route
Класс мог сделать в версиях MVC, прошлое должно было наследовать RouteBase или Route.
MVC 6 имеет похожие абстракции, IRouter и INamedRouter, которые выполняют одну и ту же роль. Как и его предшественник, 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>
Вот пример, который отслеживает и кэширует 1-1 сопоставление первичного ключа с URL. Это универсальный, и я проверил, работает ли первичный ключ int
или же Guid
,
Есть вставляемый элемент, который необходимо ввести, 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));
}
}
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);
}
}
использование
И здесь мы добавляем маршрут перед маршрутом по умолчанию и настраиваем его параметры.
// 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
везде, например.
Один простой способ: если произошла ошибка 404, вы проверяете эту квоту:
Если URL-адрес существует в списке маршрутизации, перенаправьте на этот
образец ядра.net перейдите в Startup.cs(корень проекта), а затем в методе Configure добавьте нижний код:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env){
app.UseStatusCodePages(async context =>
{
var redirctPage = pageToRedirect(context);
context.HttpContext.Response.Redirect(redirctPage);
}
...
}
private string pageToRedirect(StatusCodeContext context)
{
var def = "";
if(context.HttpContext.Response.StatusCode==404){
if (context.HttpContext.Request.Path.ToString().ToLower().Contains("/product/"))
{
def = "/Home/Product";
def += context.HttpContext.Request.QueryString;
}
else if (context.HttpContext.Request.Path.ToString().ToLower()=="/news")//or you can call class that load info from DB to redirect
{
def = "/Home/News";
def += context.HttpContext.Request.QueryString;
}
else//404 error page
def = "/Home/Error?statusCode=" + context.HttpContext.Response.StatusCode;
}else //other errors code
def = "/Home/Error?statusCode=" + context.HttpContext.Response.StatusCode;
return def;
}