学习Nop中Routes的使用
1. 映射路由
大型MVC项目为了扩展性,可维护性不能向一般项目在Global中RegisterRoutes的方法里面映射路由。这里学习一下Nop是如何做的。
Global.cs . 通过IOC容器取得IRoutePublisher实例
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("favicon.ico"); routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); //register custom routes (plugins, etc) var routePublisher = EngineContext.Current.Resolve<IRoutePublisher>(); routePublisher.RegisterRoutes(routes); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "Nop.Web.Controllers" } ); }
IRoutePublisher.cs 只有一个方法
public interface IRoutePublisher { void RegisterRoutes(RouteCollection routeCollection); }
实现类
public class RoutePublisher : IRoutePublisher { private readonly ITypeFinder _typeFinder; public RoutePublisher(ITypeFinder typeFinder) { this._typeFinder = typeFinder; } public void RegisterRoutes(RouteCollection routes) { var routeProviderTypes = _typeFinder.FindClassesOfType<IRouteProvider>(); var routeProviders = new List<IRouteProvider>(); foreach (var providerType in routeProviderTypes) { var provider = Activator.CreateInstance(providerType) as IRouteProvider; routeProviders.Add(provider); } routeProviders = routeProviders.OrderByDescending(rp => rp.Priority).ToList(); routeProviders.ForEach(rp => rp.RegisterRoutes(routes)); } }
ITypeFinder之前已经介绍过。点击查看 ,搜索项目中所有的IRouteProvider实现。然后根据优先级排序,最后调用RegisterRoutes依次映射
竟然有这么多,大多都在Plugin程序集中为插件映射路由。随便打开一个。
public partial class RouteProvider : IRouteProvider { public void RegisterRoutes(RouteCollection routes) { routes.MapRoute("Plugin.Payments.AuthorizeNet.Configure", "Plugins/PaymentAuthorizeNet/Configure", new { controller = "PaymentAuthorizeNet", action = "Configure" }, new[] { "Nop.Plugin.Payments.AuthorizeNet.Controllers" } ); routes.MapRoute("Plugin.Payments.AuthorizeNet.PaymentInfo", "Plugins/PaymentAuthorizeNet/PaymentInfo", new { controller = "PaymentAuthorizeNet", action = "PaymentInfo" }, new[] { "Nop.Plugin.Payments.AuthorizeNet.Controllers" } ); } public int Priority { get { return 0; } } }
好处不用说了。模块化方便复用。
2.自定义Route
LocalizedRoute.cs
using System.Web; using System.Web.Routing; using Nop.Core.Data; using Nop.Core.Domain.Localization; using Nop.Core.Infrastructure; namespace Nop.Web.Framework.Localization { /// <summary> /// Provides properties and methods for defining a localized route, and for getting information about the localized route. /// </summary> public class LocalizedRoute : Route { #region Fields private bool? _seoFriendlyUrlsForLanguagesEnabled; #endregion #region Constructors /// <summary> /// Initializes a new instance of the System.Web.Routing.Route class, using the specified URL pattern and handler class. /// </summary> /// <param name="url">The URL pattern for the route.</param> /// <param name="routeHandler">The object that processes requests for the route.</param> public LocalizedRoute(string url, IRouteHandler routeHandler) : base(url, routeHandler) { } /// <summary> /// Initializes a new instance of the System.Web.Routing.Route class, using the specified URL pattern, handler class and default parameter values. /// </summary> /// <param name="url">The URL pattern for the route.</param> /// <param name="defaults">The values to use if the URL does not contain all the parameters.</param> /// <param name="routeHandler">The object that processes requests for the route.</param> public LocalizedRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler) : base(url, defaults, routeHandler) { } /// <summary> /// Initializes a new instance of the System.Web.Routing.Route class, using the specified URL pattern, handler class, default parameter values and constraints. /// </summary> /// <param name="url">The URL pattern for the route.</param> /// <param name="defaults">The values to use if the URL does not contain all the parameters.</param> /// <param name="constraints">A regular expression that specifies valid values for a URL parameter.</param> /// <param name="routeHandler">The object that processes requests for the route.</param> public LocalizedRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler) : base(url, defaults, constraints, routeHandler) { } /// <summary> /// Initializes a new instance of the System.Web.Routing.Route class, using the specified URL pattern, handler class, default parameter values, /// constraints,and custom values. /// </summary> /// <param name="url">The URL pattern for the route.</param> /// <param name="defaults">The values to use if the URL does not contain all the parameters.</param> /// <param name="constraints">A regular expression that specifies valid values for a URL parameter.</param> /// <param name="dataTokens">Custom values that are passed to the route handler, but which are not used to determine whether the route matches a specific URL pattern. The route handler might need these values to process the request.</param> /// <param name="routeHandler">The object that processes requests for the route.</param> public LocalizedRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler) : base(url, defaults, constraints, dataTokens, routeHandler) { } #endregion #region Methods /// <summary> /// Returns information about the requested route. /// </summary> /// <param name="httpContext">An object that encapsulates information about the HTTP request.</param> /// <returns> /// An object that contains the values from the route definition. /// </returns> public override RouteData GetRouteData(HttpContextBase httpContext) { if (DataSettingsHelper.DatabaseIsInstalled() && this.SeoFriendlyUrlsForLanguagesEnabled) { string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath; string applicationPath = httpContext.Request.ApplicationPath; if (virtualPath.IsLocalizedUrl(applicationPath, false)) { //In ASP.NET Development Server, an URL like "http://localhost/Blog.aspx/Categories/BabyFrog" will return //"~/Blog.aspx/Categories/BabyFrog" as AppRelativeCurrentExecutionFilePath. //However, in II6, the AppRelativeCurrentExecutionFilePath is "~/Blog.aspx" //It seems that IIS6 think we're process Blog.aspx page. //So, I'll use RawUrl to re-create an AppRelativeCurrentExecutionFilePath like ASP.NET Development Server. //Question: should we do path rewriting right here? string rawUrl = httpContext.Request.RawUrl; var newVirtualPath = rawUrl.RemoveLocalizedPathFromRawUrl(applicationPath); if (string.IsNullOrEmpty(newVirtualPath)) newVirtualPath = "/"; newVirtualPath = newVirtualPath.RemoveApplicationPathFromRawUrl(applicationPath); newVirtualPath = "~" + newVirtualPath; httpContext.RewritePath(newVirtualPath, true); } } RouteData data = base.GetRouteData(httpContext); return data; } /// <summary> /// Returns information about the URL that is associated with the route. /// </summary> /// <param name="requestContext">An object that encapsulates information about the requested route.</param> /// <param name="values">An object that contains the parameters for a route.</param> /// <returns> /// An object that contains information about the URL that is associated with the route. /// </returns> public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { VirtualPathData data = base.GetVirtualPath(requestContext, values); if (DataSettingsHelper.DatabaseIsInstalled() && this.SeoFriendlyUrlsForLanguagesEnabled) { if (data != null) { string rawUrl = requestContext.HttpContext.Request.RawUrl; string applicationPath = requestContext.HttpContext.Request.ApplicationPath; if (rawUrl.IsLocalizedUrl(applicationPath, true)) { data.VirtualPath = string.Concat(rawUrl.GetLanguageSeoCodeFromUrl(applicationPath, true), "/", data.VirtualPath); } } } return data; } public virtual void ClearSeoFriendlyUrlsCachedValue() { _seoFriendlyUrlsForLanguagesEnabled = null; } #endregion #region Properties protected bool SeoFriendlyUrlsForLanguagesEnabled { get { if (!_seoFriendlyUrlsForLanguagesEnabled.HasValue) _seoFriendlyUrlsForLanguagesEnabled = EngineContext.Current.Resolve<LocalizationSettings>().SeoFriendlyUrlsForLanguagesEnabled; return _seoFriendlyUrlsForLanguagesEnabled.Value; } } #endregion } }
继承Route并且实现GetRouteData, GetVirtualPath这2个方法。从返回的参数可以知道一个是解析URL,一个是生成URL。是一个双向的过程。
注意 if (DataSettingsHelper.DatabaseIsInstalled() && this.SeoFriendlyUrlsForLanguagesEnabled) 这个判断条件,启用了地区化友好SEO。才进行URL转换。
简单说就是 http://localhost:2619/en 《=》http://localhost:2619/ 的双向转换。 en,ch之类的就是网站设置的语言了。
2。MVC路由测试
先看单元测试
映射路由
new Nop.Web.Infrastructure.RouteProvider().RegisterRoutes(RouteTable.Routes);
[Test] public void Boards_routes() { "~/boards/".ShouldMapTo<BoardsController>(c => c.Index()); //TODO add support for optional parameters in 'ShouldMapTo' method (such as in ~/boards/activediscussions/ or ~/boards/topic/11/). The same is about issue is in the other route test methods //"~/boards/activediscussions/".ShouldMapTo<BoardsController>(c => c.ActiveDiscussions(0)); //"~/boards/activediscussionsrss/".ShouldMapTo<BoardsController>(c => c.ActiveDiscussionsRss(0)); "~/boards/postedit/1".ShouldMapTo<BoardsController>(c => c.PostEdit(1)); "~/boards/postdelete/2".ShouldMapTo<BoardsController>(c => c.PostDelete(2)); "~/boards/postcreate/3".ShouldMapTo<BoardsController>(c => c.PostCreate(3, null)); "~/boards/postcreate/4/5".ShouldMapTo<BoardsController>(c => c.PostCreate(4, 5)); "~/boards/topicedit/6".ShouldMapTo<BoardsController>(c => c.TopicEdit(6)); "~/boards/topicdelete/7".ShouldMapTo<BoardsController>(c => c.TopicDelete(7)); "~/boards/topiccreate/8".ShouldMapTo<BoardsController>(c => c.TopicCreate(8)); "~/boards/topicmove/9".ShouldMapTo<BoardsController>(c => c.TopicMove(9)); "~/boards/topicwatch/10".ShouldMapTo<BoardsController>(c => c.TopicWatch(10)); //"~/boards/topic/11/".ShouldMapTo<BoardsController>(c => c.Topic(11, 1)); //"~/boards/topic/11/test-topic-slug".ShouldMapTo<BoardsController>(c => c.Topic(11, 1)); "~/boards/topic/11/test-topic-slug/page/2".ShouldMapTo<BoardsController>(c => c.Topic(11, 2)); "~/boards/forumwatch/12".ShouldMapTo<BoardsController>(c => c.ForumWatch(12)); "~/boards/forumrss/13".ShouldMapTo<BoardsController>(c => c.ForumRss(13)); //"~/boards/forum/14/".ShouldMapTo<BoardsController>(c => c.Forum(14, 1)); //"~/boards/forum/14/test-forum-slug".ShouldMapTo<BoardsController>(c => c.Forum(14, 1)); "~/boards/forum/14/test-forum-slug/page/2".ShouldMapTo<BoardsController>(c => c.Forum(14, 2)); "~/boards/forumgroup/15/".ShouldMapTo<BoardsController>(c => c.ForumGroup(15)); "~/boards/forumgroup/15/test-forumgroup-slug/".ShouldMapTo<BoardsController>(c => c.ForumGroup(15)); //"~/boards/search/".ShouldMapTo<BoardsController>(c => c.Search(null, null, null, null, null, 1)); }
看来这里的代码。膜拜作者吧。。之前一直不知道如何测试Route,最多是装个Routedebug什么的
接下来进行解析匹配
/// <summary> /// Asserts that the route matches the expression specified. Checks controller, action, and any method arguments /// into the action as route values. /// </summary> /// <typeparam name="TController">The controller.</typeparam> /// <param name="routeData">The routeData to check</param> /// <param name="action">The action to call on TController.</param> public static RouteData ShouldMapTo<TController>(this RouteData routeData, Expression<Func<TController, ActionResult>> action) where TController : Controller { routeData.ShouldNotBeNull("The URL did not match any route"); //check controller routeData.ShouldMapTo<TController>(); //check action var methodCall = (MethodCallExpression)action.Body; string actualAction = routeData.Values.GetValue("action").ToString(); string expectedAction = methodCall.Method.ActionName(); actualAction.AssertSameStringAs(expectedAction); //check parameters for (int i = 0; i < methodCall.Arguments.Count; i++) { ParameterInfo param = methodCall.Method.GetParameters()[i]; bool isReferenceType = !param.ParameterType.IsValueType; bool isNullable = isReferenceType || (param.ParameterType.UnderlyingSystemType.IsGenericType && param.ParameterType.UnderlyingSystemType.GetGenericTypeDefinition() == typeof(Nullable<>)); string controllerParameterName = param.Name; bool routeDataContainsValueForParameterName = routeData.Values.ContainsKey(controllerParameterName); object actualValue = routeData.Values.GetValue(controllerParameterName); object expectedValue = null; Expression expressionToEvaluate = methodCall.Arguments[i]; // If the parameter is nullable and the expression is a Convert UnaryExpression, // we actually want to test against the value of the expression's operand. if (expressionToEvaluate.NodeType == ExpressionType.Convert && expressionToEvaluate is UnaryExpression) { expressionToEvaluate = ((UnaryExpression)expressionToEvaluate).Operand; } switch (expressionToEvaluate.NodeType) { case ExpressionType.Constant: expectedValue = ((ConstantExpression)expressionToEvaluate).Value; break; case ExpressionType.New: case ExpressionType.MemberAccess: expectedValue = Expression.Lambda(expressionToEvaluate).Compile().DynamicInvoke(); break; } if (isNullable && (string)actualValue == String.Empty && expectedValue == null) { // The parameter is nullable so an expected value of '' is equivalent to null; continue; } // HACK: this is only sufficient while System.Web.Mvc.UrlParameter has only a single value. if (actualValue == UrlParameter.Optional || (actualValue != null && actualValue.ToString().Equals("System.Web.Mvc.UrlParameter"))) { actualValue = null; } if (expectedValue is DateTime) { actualValue = Convert.ToDateTime(actualValue); } else { expectedValue = (expectedValue == null ? expectedValue : expectedValue.ToString()); } string errorMsgFmt = "Value for parameter '{0}' did not match: expected '{1}' but was '{2}'"; if (routeDataContainsValueForParameterName) { errorMsgFmt += "."; } else { errorMsgFmt += "; no value found in the route context action parameter named '{0}' - does your matching route contain a token called '{0}'?"; } actualValue.ShouldEqual(expectedValue, String.Format(errorMsgFmt, controllerParameterName, expectedValue, actualValue)); } return routeData; }
这里又看到了表达式树强大的地方。关于表达式树具体使用请搜索博客园, 这里通过routeData和解析action来判断请求和action是否一致。
总结:通过学习Nop中路由的一些使用。掌握了一些很有用的Route的使用。这些模块今后也能根据需要加入到自己的网站中。
参考:
两篇关于自定义路由:http://www.cnblogs.com/john-connor/archive/2012/05/03/2478821.html
http://www.cnblogs.com/ldp615/archive/2011/12/05/asp-net-mvc-elegant-route.html