启动后更改MVC6的路由集合 [英] Change route collection of MVC6 after startup

查看:24
本文介绍了启动后更改MVC6的路由集合的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 MVC-5 中,我可以在初始启动后通过访问 RouteTable.Routes 来编辑 routetable.我希望在 MVC-6 中做同样的事情,这样我就可以在运行时添加/删除路由(对 CMS 有用).

In MVC-5 I could edit the routetable after initial startup by accessing RouteTable.Routes. I wish to do the same in MVC-6 so I can add/delete routes during runtime (usefull for CMS).

在 MVC-5 中执行此操作的代码是:

The code to do it in MVC-5 is:

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

但我在 MVC-6 中找不到 RouteTable.Routes 或类似的东西.知道如何在运行时更改路由集合吗?

But I can't find RouteTable.Routes or something similar in MVC-6. Any idea how I can still change the route collection during runtime?

我想利用这个原理,例如在CMS中创建页面时添加一个额外的url.

I want to use this principle to add, for example, an extra url when a page is created in the CMS.

如果你有一个类:

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

还有一个控制器,例如:

And a controller like:

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:

Then when a page is added to the database I recreate the 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 的页面.

In this way I can add pages to the CMS that do not belong in conventions or strict templates. I can add a page with url /contact, but also a page with url /help/faq/how-does-this-work.

推荐答案

答案是没有合理的方法可以做到这一点,即使找到了方法也不是一个好的做法.

The answer is that there is no reasonable way to do this, and even if you find a way it would not be a good practice.

基本上,过去 MVC 版本的路由配置旨在充当 DI 配置 - 也就是说,您将所有内容都放在 组合根 然后在运行时使用该配置.问题在于您可以在运行时将对象推送到配置中(很多人都这样做了),这不是正确的方法.

Basically, the route configuration of MVC versions past was meant to act like a DI configuration - that is, you put everything there in the composition root and then use that configuration during runtime. The problem was that you could push objects into the configuration at runtime (and many people did), which is not the right approach.

现在配置已被真正的 DI 容器取代,这种方法将不再有效.注册步骤现在只能在应用程序启动时完成.

Now that the configuration has been replaced by a true DI container, this approach will no longer work. The registration step can now only be done at application startup.

在过去的 MVC 版本中,Route 类可以实现的自定义路由的正确方法是 继承RouteBase 或Route.

The correct approach to customizing routing well beyond what the Route class could do in MVC versions past was to inherit RouteBase or Route.

AspNetCore(以前称为 MVC 6)有类似的抽象,IRouterINamedRouter 填充相同的角色.与其前身非常相似,IRouter 只需实现两种方法.

AspNetCore (formerly known as MVC 6) has similar abstractions, IRouter and INamedRouter that fill the same role. Much like its predecessor, IRouter has just two methods to implement.

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

此接口是您实现路由的 2 路性质的地方 - URL 路由值和路由值到 URL.

This interface is where you implement the 2-way nature of routing - URL to route values and route values to URL.

这是一个跟踪和缓存主键到 URL 的 1-1 映射的示例.它是通用的,我已经测试过无论主键是 int 还是 Guid 都有效.

Here is an example that tracks and caches a 1-1 mapping of primary key to URL. It is generic and I have tested that it works whether the primary key is int or Guid.

必须注入一个可插入的部分,ICachedRouteDataProvider,可以在其中实现对数据库的查询.您还需要提供控制器和操作,因此该路由足够通用,可以使用多个实例将多个数据库查询映射到多个操作方法.

There is a pluggable piece that must be injected, ICachedRouteDataProvider where the query for the database can be implemented. You also need to supply the controller and action, so this route is generic enough to map multiple database queries to multiple action methods by using more than one instance.

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 中执行的操作.

CmsCachedRouteDataProvider

This is the implementation of the data provider that is basically what you need to do in your 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);
    }
}

用法

这里我们在默认路由之前添加路由,并配置它的选项.

Usage

And here we add the route before the default route, and configure its options.

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

这就是它的要点.你仍然可以改进一些东西.

That's the gist of it. You could still improve things a bit.

我个人会使用工厂模式并将存储库注入到 CmsCachedRouteDataProvider 的构造函数中,而不是在任何地方硬编码 DbContext,例如.

I would personally use a factory pattern and inject the repository into the constructor of CmsCachedRouteDataProvider rather than hard coding DbContext everywhere, for example.

这篇关于启动后更改MVC6的路由集合的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆