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

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

问题描述

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

在code做的MVC-5是:

 使用(RouteTable.Routes.GetWriteLock())
{
    RouteTable.Routes.Clear();    RouteTable.Routes.IgnoreRoute({}资源个.axd / {*} PATHINFO);
    RouteTable.Routes.MapRoute(
        名称:默认,
        网址:{控制器} / {行动} / {ID}
        默认:新{控制器=家,行动=索引,ID = UrlParameter.Optional}
    );
}

但我无法找到 RouteTable.Routes 或类似的MVC-6的东西。任何想法如何我仍然可以在运行时改变路由集合?


我想用这个原则来补充,例如,当在CMS中创建一个页面一个额外的网址。

如果你有这样一个类:

 公共类页
{
    公众诠释标识{搞定;组; }
    公共字符串网址{搞定;组; }
    公共字符串的Html {搞定;组; }
}

而像控制器:

 公共类CmsController:控制器
{
    公众的ActionResult指数(INT ID)
    {
        VAR页= DbContext.Pages.Single(P => p.Id == ID);
        返回视图(布局,型号:page.html即可);
    }
}

:当一个页面被添加到我重新创建 routecollection 数据库

然后

  VAR路线= RouteTable.Routes;
使用(routes.GetWriteLock())
{
    routes.Clear();
    的foreach(在DbContext.Pages VAR页)
    {
        routes.MapRoute(
            名称:Guid.NewGuid()的ToString()
            网址:page.Url.TrimEnd('/'),
            默认:新{控制器=CMS,行动=索引,ID = page.Id}
        );
    }    VAR默认路由= routes.MapRoute(
        名称:默认,
        网址:{控制器} / {行动} / {ID}
        默认:新{控制器=家,行动=索引,ID = UrlParameter.Optional}
    );
}

在这样,我可以添加到页面中不约定或严格模板所属的CMS。我可以添加一个页面,网址 /联系人,也与网址的网页<​​code> /帮助/常见问题/如何 - 做 - 这工作。


解决方案

答案是,有这样做没有任何合理的方式,甚至如果你找到一个方式,它不会是一个很好的做法。

一种错误的方法的问题

基本上,MVC版本过去的路由配置是为了表现得像一个DI配置 - 也就是说,你把一切有中的成分根,然后在运行时使用该配置。问题是,你的可能的物品推入的配置在运行时(和许多人一样),这是不正确的方法。

现在的配置已经被换成了真正​​的DI容器,这种方式将不再起作用。注册步骤可现在只能在应用程序启动完成。

正确的做法

正确的方法来定制路由远远超出了路线类可以在MVC版本过去做的是<一个href=\"http://stackoverflow.com/questions/31934144/multiple-levels-in-mvc-custom-routing/31958586#31958586\">inherit RouteBase 或路线。

MVC 6也有类似的抽象, IRouter INamedRouter 填补相同的作用。就像它的predecessor, IRouter 刚刚两种方法来实现。

 命名空间Microsoft.AspNet.Routing
{
    公共接口IRouter
    {
        //派生从路线的值的列表的虚拟路径(URL)
        VirtualPathData GetVirtualPath(VirtualPathContext背景);        //填充基于路线数据(包括路由值)
        //请求
        任务RouteAsync(RouteContext背景);
    }
}

此接口是你实现路由的性质2路 - URL路由值和路由值网址

例子: CachedRoute&LT; TPrimaryKey&GT;

下面是跟踪和高速缓存主键URL的一对一映射的例子。它是通用的,我已经测试它工作的主键是 INT 的Guid

有一个可插拔的一块必须注入, ICachedRouteDataProvider ,其中数据库查询可以实现。您还需要提供控制器和动作,所以这条路线是通用使用多个实例多个数据库查询到多个动作方法映射。

 使用Microsoft.AspNet.Routing;
使用Microsoft.Framework.Caching.Memory;
使用系统;
使用System.Collections.Generic;
使用System.Linq的;
使用的System.Reflection;
使用System.Threading.Tasks;公共类CachedRoute&LT; TPrimaryKey&GT; :IRouter
{
    私人只读字符串_controller;
    私人只读字符串_action;
    私人只读ICachedRouteDataProvider&LT; TPrimaryKey&GT; _dataProvider;
    私人只读IMemoryCache _cache;
    私人只读IRouter _target;
    私人只读字符串_cacheKey;
    私有对象_lock =新的对象();    公共CachedRoute(
        串控制器,
        串动,
        ICachedRouteDataProvider&LT; TPrimaryKey&GT; dataProvider中,
        IMemoryCache缓存,
        IRouter目标)
    {
        如果(string.IsNullOrWhiteSpace(控制器))
            抛出新的ArgumentNullException(控制器);
        如果(string.IsNullOrWhiteSpace(动作))
            抛出新的ArgumentNullException(行动);
        如果(dataProvider中== NULL)
            抛出新的ArgumentNullException(dataProvider中);
        如果(缓存== NULL)
            抛出新的ArgumentNullException(缓存);
        如果(目标== NULL)
            抛出新的ArgumentNullException(目标);        _controller =控制器;
        _action =行动;
        _dataProvider = dataProvider中;
        _cache =高速缓存;
        _target =目标;        //设置默认值
        CacheTimeoutInSeconds = 900;
        。_cacheKey =__+ this.GetType()名称+_GetPageList_+ _controller +_+ _action;
    }    公众诠释CacheTimeoutInSeconds {搞定;组; }    公共异步任务RouteAsync(RouteContext上下文)
    {
        VAR requestPath = context.HttpContext.Request.Path.Value;        如果(string.IsNullOrEmpty(requestPath)及!&放大器; requestPath [0] =='/')
        {
            //修剪前导斜杠
            requestPath = requestPath.Substring(1);
        }        //获取相匹配的页面ID。
        TPrimaryKey ID;        //如果返回false,这意味着URI不匹配
        如果(!GetPageList()。TryGetValue(requestPath,掉id))
        {
            返回;
        }        //调用MVC控制器/动作
        VAR oldRouteData = context.RouteData;
        VAR newRouteData =新的RouteData(oldRouteData);
        newRouteData.Routers.Add(_target);        // TODO:您可能希望使用页面对象(从数据库)来
        //取得两个控制器和动作,甚至可能的区域。
        //或者,您可以创建为每个表和硬code路线
        // 此信息。
        newRouteData.Values​​ [控制器] = _controller;
        newRouteData.Values​​ [行动] = _action;        //这将是数据库行的主键。
        //这可能是一个整数或GUID。
        newRouteData.Values​​ [ID] = ID;        尝试
        {
            context.RouteData = newRouteData;
            等待_target.RouteAsync(背景);
        }
        最后
        {
            //还原污染的路由数据的原始值以prevent。
            如果(!context.IsHandled)
            {
                context.RouteData = oldRouteData;
            }
        }
    }    公共VirtualPathData GetVirtualPath(VirtualPathContext上下文)
    {
        VirtualPathData结果= NULL;
        字符串virtualPath;        如果(TryFindMatch(GetPageList(),context.Values​​,出virtualPath))
        {
            结果=新VirtualPathData(这一点,virtualPath);
            context.IsBound = TRUE;
        }        返回结果;
    }    私人布尔TryFindMatch(IDictionary的&LT;字符串,TPrimaryKey&GT;页,IDictionary的&LT;字符串对象&gt;值,出字符串virtualPath)
    {
        virtualPath =的String.Empty;
        TPrimaryKey ID;
        反对idObj;
        对象控制器;
        对象操作;        如果(!values​​.TryGetValue(ID,出idObj))
        {
            返回false;
        }        ID = SafeConvert&LT; TPrimaryKey&GT;(idObj);
        values​​.TryGetValue(控制器,走出控制器);
        values​​.TryGetValue(行动,出于行动);        //这里的逻辑应的逻辑在逆
        // RouteAsync()。所以,我们匹配相同的控制器,操作和ID。
        //如果我们有更多的路由值在那里,我们将采取所有这些
        在该步骤期间//考虑。
        如果(action.Equals(_action)及&放大器; controller.Equals(_controller))
        {
            //在'OrDefault的情况下返回你的类型的默认值
            //迭代。对于值类型,这将是该类型的新实例。
            //由于KeyValuePair&LT; TKEY的,TValue&GT;是值类型(即一个结构)
            //将OrDefault情况下也不会导致空引用异常。
            //由于TKEY的这里是字符串,新实例的。重点将是空的。
            virtualPath = pages.FirstOrDefault(X =&GT; x.Value.Equals(ID))键。
            如果(!string.IsNullOrEmpty(virtualPath))
            {
                返回true;
            }
        }
        返回false;
    }    私人的IDictionary&LT;字符串,TPrimaryKey&GT; GetPageList()
    {
        IDictionary的&LT;字符串,TPrimaryKey&GT;页;        如果(!_cache.TryGetValue(_cacheKey,走出页))
        {
            //只允许一个线程poplate数据
            锁(_lock)
            {
                如果(!_cache.TryGetValue(_cacheKey,走出页))
                {
                    页= _dataProvider.GetPageToIdMap();                    _cache.Set(_cacheKey,页面,
                        新MemoryCacheEntryOptions()
                        {
                            优先级= CacheItemPriority.NeverRemove,
                            AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(this.CacheTimeoutInSeconds)
                        });
                }
            }
        }        返回页面;
    }    私有静态ŧSafeConvert&LT; T&GT;(obj对象)
    {
        如果(typeof运算(T).Equals(typeof运算(GUID)))
        {
            如果(obj.GetType()== typeof运算(字符串))
            {
                回报(T)(对象)新的GUID(obj.ToString());
            }
            返回(T)(对象)Guid.Empty;
        }
        回报(T)Convert.ChangeType(OBJ的typeof(T));
    }
}

CmsCachedRouteDataProvider

这是数据提供程序的实施,基本上是你需要在你的CMS做什么。

 公共接口ICachedRouteDataProvider&LT; TPrimaryKey&GT;
{
    IDictionary的&LT;字符串,TPrimaryKey&GT; GetPageToIdMap();
}公共类CmsCachedRouteDataProvider:ICachedRouteDataProvider&LT; INT&GT;
{
    公众的IDictionary&LT;字符串,整数&GT; GetPageToIdMap()
    {
        //查询数据库中的网页
        返回(从DbContext.Pages页
                选择新KeyValuePair&LT;字符串,整数&GT;(
                    page.Url.TrimStart('/')。TrimEnd('/'),
                    page.Id)
                ).ToDictionary(双=&GT; pair.Key,对= GT; pair.Value);
    }
}

用法

在这里,我们添加默认路由该路由,并配置其选项。

  //添加到MVC的请求管道。
app.UseMvc(路线=&GT;
{
    routes.Routes.Add(
        新CachedRoute&LT; INT&GT;(
            控制器:CMS
            动作:索引,
            dataProvider中:新CmsCachedRouteDataProvider()
            缓存:routes.ServiceProvider.GetService&LT; IMemoryCache&GT;()
            目标:routes.DefaultHandler)
        {
            CacheTimeoutInSeconds = 900
        });    routes.MapRoute(
        名称:默认,
        模板:{控制器= HOME} / {行动=指数} / {?ID});    //取消注释以下行添加路由用于移植的Web API 2控制器。
    // routes.MapWebApiRoute(DefaultApi,API / {控制器} / {ID}?);
});

这是它的要点。你仍然可以提高事情有点。

首先,我会亲自使用存储库模式,注入储备库到 CmsCachedRouteDataProvider 的构造函数,而不是硬编码的DbContext 无处不在。

其次,你可以使用 Microsoft.Framework.Runtime.Caching.ICache ,它接受缓存依赖,并可能有缓存每次无效表被更新。然后,用户就不必等待长达15分钟才能看到的变化。没有证件,我无法工作,如何使用缓存依赖,虽然。

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).

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

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?


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

If you have a class like:

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

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

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.

An Incorrect Approach to the Problem

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.

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.

The Correct Approach

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

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

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

An Example: CachedRoute<TPrimaryKey>

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.

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.AspNet.Routing;
using Microsoft.Framework.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 oldRouteData = context.RouteData;
        var newRouteData = new RouteData(oldRouteData);
        newRouteData.Routers.Add(_target);

        // 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.
        newRouteData.Values["controller"] = _controller;
        newRouteData.Values["action"] = _action;

        // This will be the primary key of the database row.
        // It might be an integer or a GUID.
        newRouteData.Values["id"] = id;

        try
        {
            context.RouteData = newRouteData;
            await _target.RouteAsync(context);
        }
        finally
        {
            // Restore the original values to prevent polluting the route data.
            if (!context.IsHandled)
            {
                context.RouteData = oldRouteData;
            }
        }
    }

    public VirtualPathData GetVirtualPath(VirtualPathContext context)
    {
        VirtualPathData result = null;
        string virtualPath;

        if (TryFindMatch(GetPageList(), context.Values, out virtualPath))
        {
            result = new VirtualPathData(this, virtualPath);
            context.IsBound = true;
        }

        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

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.

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

Secondly, you could use Microsoft.Framework.Runtime.Caching.ICache, which accepts cache dependencies and potentially have the cache invalidate every time the Pages table is updated. Then the users wouldn't have to wait up to 15 minutes to see the changes. Without documentation, I couldn't work out how to use cache dependencies, though.

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

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