Подтвердить что ты не робот

Измените коллекцию маршрутов 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.

4b9b3361

Ответ 1

Ответ в том, что нет разумного способа сделать это, и даже если вы найдете способ, это не будет хорошей практикой.

Неверный подход к проблеме

По сути, конфигурация маршрута прошлых версий MVC должна была действовать как конфигурация DI - то есть вы помещаете все туда в корень композиции, а затем используете эту конфигурацию во время выполнения. Проблема заключалась в том, что вы могли помещать объекты в конфигурацию во время выполнения (и многие это делали), что не является правильным подходом.

Теперь, когда конфигурация заменена настоящим контейнером DI, этот подход больше не будет работать. Шаг регистрации теперь можно выполнить только при запуске приложения.

Правильный подход

Правильный подход к настройке маршрутизации, выходящий далеко за пределы того, что класс Route мог делать в прошлых версиях MVC, заключался в наследовании RouteBase или Route.

AspNetCore имеет похожие абстракции, 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 репозиторий в конструктор CmsCachedRouteDataProvider а не везде, например, жестко программировал DbContext.