MVC執行流程

路由的擴展
我理解的路由作用有以下幾個
- Seo優化,用“/”分開的url爬蟲更愛吃
- 物理和邏輯文件分離,url不再按照文件路徑映射
- Controller,Action的選擇
MVC路由的擴展
實話實說MVC的路由我很少去做擴展,在MVC4時代,還會去重寫掉url的大小寫,而在MVC5之后,MVC自帶了配置去小寫化url。不過有一個配置還是必須要提一下那就是Area,在你的系統達到一定規模之后,Controllers通過Area來管理將會變得更容易。這里給出我的Area擴展,很簡單但是很重要,注意子類必須以AreaRegistration結尾,同樣遵循約定有限於配置的原則,當然你也可以重寫。
public abstract class AreaRegistrationBase : AreaRegistration
{
public override string AreaName
{
get {
var item = GetType().Name;
return item.Replace("AreaRegistration", "");
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapLowerCaseUrlRoute(
AreaName + "_default",
AreaName.ToLower() + "/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
GlobalConfiguration.Configuration.Routes.MapHttpRoute(
AreaName + "Api",
"api/" + AreaName + "/{controller}/{action}/{id}",
new { area = AreaName, id = RouteParameter.Optional, namespaceName = new[] { this.GetType().Namespace } }
);
}
}
WebApi路由的擴展
上面MVC的路由也注冊了Api的路由,當然還有更好的方法,由於WebApi,基本不需要Url.Action和HtmlAction,這種路由出棧的策略,不需要去通過路由生成Url,所以
我們只需要做到路由的解析即可,並且webapi並沒有提供自帶的area機制,所以我擴展了DefaultHttpControllerSelector,獲取到路由中area和controller參數
然后完成拼接,然后反射類型查找,在最開始時候預先緩存了所有controller,所以性能還不錯,由於原代碼是反編譯獲得,所以linq部分有點糟糕,等我回公司拿到源代碼再修改。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
namespace Coralcode.Webapi.Route
{
public class AreaHttpControllerSelector : DefaultHttpControllerSelector
{
public static string CoralControllerSuffix = "ApiController";
private readonly HttpConfiguration _configuration;
private readonly Lazy<ILookup<string, Type>> _apiControllerTypes;
private ILookup<string, Type> ApiControllerTypes
{
get
{
return this._apiControllerTypes.Value;
}
}
public AreaHttpControllerSelector(HttpConfiguration configuration)
: base(configuration)
{
this._configuration = configuration;
this._apiControllerTypes = new Lazy<ILookup<string, Type>>(new Func<ILookup<string, Type>>(this.GetApiControllerTypes));
}
private ILookup<string, Type> GetApiControllerTypes()
{
return Enumerable.ToLookup<Type, string, Type>((IEnumerable<Type>) ServicesExtensions.GetHttpControllerTypeResolver(this._configuration.Services).GetControllerTypes(ServicesExtensions.GetAssembliesResolver(this._configuration.Services)), (Func<Type, string>) (t => t.Name.ToLower().Substring(0, t.Name.Length - AreaHttpControllerSelector.CoralControllerSuffix.Length)), (Func<Type, Type>) (t => t));
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
string controllerName = this.GetControllerName(request);
if (!string.IsNullOrWhiteSpace(controllerName))
{
List<Type> list = Enumerable.ToList<Type>(this.ApiControllerTypes[controllerName.ToLower()]);
if (Enumerable.Any<Type>((IEnumerable<Type>) list))
{
IDictionary<string, object> values = HttpRequestMessageExtensions.GetRouteData(request).Values;
string endString;
if (values.Count > 1)
{
StringBuilder stringBuilder = new StringBuilder();
if (values.ContainsKey("area"))
{
stringBuilder.Append('.');
stringBuilder.Append(values["area"]);
stringBuilder.Append('.');
stringBuilder.Append("controllers");
}
if (values.ContainsKey("controller"))
{
stringBuilder.Append('.');
stringBuilder.Append(values["controller"]);
stringBuilder.Append(AreaHttpControllerSelector.CoralControllerSuffix);
}
endString = stringBuilder.ToString();
}
else
endString = string.Format(".{0}{1}", (object) controllerName, (object) AreaHttpControllerSelector.CoralControllerSuffix);
Type controllerType = Enumerable.FirstOrDefault<Type>((IEnumerable<Type>) Enumerable.OrderBy<Type, int>(Enumerable.Where<Type>((IEnumerable<Type>) list, (Func<Type, bool>) (t => t.FullName.EndsWith(endString, StringComparison.CurrentCultureIgnoreCase))), (Func<Type, int>) (t => Enumerable.Count<char>((IEnumerable<char>) t.FullName, (Func<char, bool>) (s => (int) s == 46)))));
if (controllerType != (Type) null)
return new HttpControllerDescriptor(this._configuration, controllerName, controllerType);
}
}
return base.SelectController(request);
}
}
}
Controller激活
Controller激活這部分是MVC和Ioc結合的核心,Ioc有三大部分
- Register 類型的發現和注冊,解決如何發現那些類型是需要注冊的和如何注冊,這里我采用Attribute的方式去發現,用Unity自帶的注冊方法注冊
- Resolve 類型的解析,如何知道某個類型和將其實例化
- LiftTime 如何控制實例的生命周期,例如是否使用單例,生命周期也采用Unity自帶的,主要用到單例和每次解析都一個實例
MVC需要和Ioc激活首先我們關注的就是哪里注冊和哪里去實例化,由於MVC使用的是約定優先於配置的方式,所有的Controller都是以“Controller”結尾,所以這里我直接加載
程序集,然后找到所有類型以Controller結尾的去注冊就好了,激活的話有以下三種方式,難易程度依次遞增,嵌套深度也依次遞增,這里我采用重寫
默認DefaultControllerFactory的方式去激活,這樣既可以用自己的也結合其他機制去實現,其他方式大家可以自行擴展。
- ControllerFactory
- IControllerActivetor
- IDependencyResolver
using Coralcode.Framework.Aspect.Unity;
using Coralcode.Mvc.Resources;
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace Coralcode.Mvc.ControllerFactory
{
public class UnityControllerFactory : DefaultControllerFactory
{
public override IController CreateController(RequestContext requestContext, string controllerName)
{
return base.CreateController(requestContext, controllerName);
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (controllerType == (Type) null)
throw new HttpException(404, Messages.MvcBase_NotFoundPage);
if (!UnityService.HasRegistered(controllerType))
return base.GetControllerInstance(requestContext, controllerType);
return (IController) UnityService.Resolve(controllerType);
}
}
}
//webapi 激活如下
using Coralcode.Framework.Aspect.Unity;
using System;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
namespace Coralcode.Webapi.ControllerFactory
{
public class UnityControllerActivator : IHttpControllerActivator
{
public IHttpController Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
{
IHttpController httpController = (IHttpController) UnityService.Resolve(controllerType);
HttpRequestMessageExtensions.RegisterForDispose(request, httpController as IDisposable);
return httpController;
}
}
}
Filter執行
LogFilter
這里基本上都是Mvc的源代碼,只是增加了一個日志功能而已,扒MVC源代碼大家一定去嘗試
using Coralcode.Framework.Log;
using System;
using System.Web;
using System.Web.Mvc;
namespace Coralcode.Mvc.Filters
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class LogExceptionAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
if (filterContext == null)
throw new ArgumentNullException("filterContext");
Exception exception = filterContext.Exception;
LoggerFactory.Instance.Error(exception.ToString());
if (filterContext.IsChildAction || filterContext.ExceptionHandled || (!filterContext.HttpContext.IsCustomErrorEnabled || new HttpException((string) null, exception).GetHttpCode() != 500) || !this.ExceptionType.IsInstanceOfType((object) exception))
return;
this.HandlerViewResultException(filterContext);
}
private void HandlerViewResultException(ExceptionContext filterContext)
{
string controllerName = (string) filterContext.RouteData.Values["controller"];
string actionName = (string) filterContext.RouteData.Values["action"];
HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
ExceptionContext exceptionContext = filterContext;
ViewResult viewResult1 = new ViewResult();
viewResult1.ViewName = this.View;
viewResult1.MasterName = this.Master;
viewResult1.ViewData = (ViewDataDictionary) new ViewDataDictionary<HandleErrorInfo>(model);
viewResult1.TempData = filterContext.Controller.TempData;
ViewResult viewResult2 = viewResult1;
exceptionContext.Result = (ActionResult) viewResult2;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
}
ResultFilter
這里統一處理了Ajax請求返回數據以ResultMessage返回,如果不是JsonResult選擇忽略,具體設計初衷可以看Controller設計那一節.
using Coralcode.Framework.Models;
using Coralcode.Mvc.ActionResults;
using System.Web.Mvc;
namespace Coralcode.Mvc.Filters
{
public class ResultMessageAttribute : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
JsonResult jsonResult = (JsonResult) (filterContext.Result as CustomJsonResult) ?? filterContext.Result as JsonResult;
if (jsonResult == null)
return;
jsonResult.Data = this.GetResultMessage(jsonResult.Data);
filterContext.Result = (ActionResult) jsonResult;
}
private object GetResultMessage(object data)
{
if (data is BaseMessage)
return data;
return (object) new ResultMessage(ResultState.Success, "Success", data);
}
}
}
Action執行
提高並發利器,最佳使用方式,這里詳細的介紹可以結合Artec的Controller同步和異步這一節,其中我個人推薦的就是采用返回Task<ActionResult>。
ValueProvider和ModelBinder
這里會將Url中的參數作為Model綁定的數據源。在做三級菜單的時候可以體現出用途,例如我首先在數據字典中添加如下數據
var newsType = new Glossary()
{
Key = "NewsType",
Value = "NewsType",
Description = "新聞",
Seq = 0,
Title = "新聞類型",
ParentId = -1,
};
_glossaryService.Add(newsType);
_glossaryService.Add(new Glossary()
{
Key = "RecentNews",
Value = "RecentNews",
Description = "新聞類型",
Seq = 1,
Title = "最新活動",
ParentId = newsType.Id,
});
_glossaryService.Add(new Glossary()
{
Key = "UserActivity",
Value = "UserActivity",
Description = "新聞類型",
Seq = 2,
Title = "會員活動",
ParentId = newsType.Id,
});
_glossaryService.Add(new Glossary()
{
Key = "OnlineMarket",
Value = "OnlineMarket",
Description = "新聞類型",
Seq = 3,
Title = "在線商城",
ParentId = newsType.Id,
});
_glossaryService.Add(new Glossary()
{
Key = "AboutUs",
Value = "AboutUs",
Description = "新聞類型",
Seq = 4,
Title = "關於我們",
ParentId = newsType.Id,
});
Repository.UnitOfWork.Commit();
然后從字段中讀出數據去添加菜單
var newsMenu = Regist("新聞管理", "/portal/home/handerindex?menuId=" + systemManagerMenu.Identify + "-NewsType",
systemManagerMenu.Identify, systemManagerMenu.Identify + "-NewsType");
//新聞管理
_glossaryService.GetFiltered("新聞類型").ForEach(item =>
{
Regist(item.Title,string.Format( "/portal/news/index?typeid={0}&type={1}" , item.Id,item.Title),
newsMenu.Identify, newsMenu.Identify + "-" + item.Id);
});
加載三級菜單
/// <summary>
/// 處理界面
/// </summary>
/// <returns></returns>
public ActionResult HanderIndex(string menuId)
{
ViewBag.Tree = string.Empty;
//TODO:請求兩次,待處理
if (menuId == null)
return View();
var items = _menuService.GetChildrenMenus(menuId);
ViewBag.Tree = JsonConvert.SerializeObject(items);
return View();
}
界面如圖

然后在Search和ViewModel中有一個字段是TypeId,這樣在List,PageSearch,AddOrEdit中就可以自動綁定值了。
public class NewsSearch:SearchBase
{
public long? TypeId { get; set; }
public string Title { get; set; }
}
View發現和ActionResult執行
MVC默認的系統比較弱,當controller和view比較多的時候一個文件下面內容會非常多,我這里做了一個模塊化處理.
using System.Web.Mvc;
namespace Coralcode.Mvc.ViewEngines
{
public class ThemesRazorViewEngine : RazorViewEngine
{
public ThemesRazorViewEngine()
{
this.AreaViewLocationFormats = new string[3]
{
"~/Themes/{2}/{1}/{0}.cshtml",
"~/Themes/Shared/{0}.cshtml",
"~/Themes/{2}/Shared/{0}.cshtml"
};
this.AreaMasterLocationFormats = new string[1]
{
"~/Themes/Shared/{0}.cshtml"
};
this.AreaPartialViewLocationFormats = new string[4]
{
"~/Themes/{2}/{1}/{0}.cshtml",
"~/Themes/{2}/Shared/{0}.cshtml",
"~/Themes/Shared/{0}.cshtml",
"~/Themes/Shared/Control/{0}.cshtml"
};
this.ViewLocationFormats = new string[2]
{
"~/Themes/{1}/{0}.cshtml",
"~/Themes/Shared/{0}.cshtml"
};
this.MasterLocationFormats = new string[1]
{
"~/Themes/Shared/{0}.cshtml"
};
this.PartialViewLocationFormats = new string[2]
{
"~/Themes/{1}/{0}.cshtml",
"~/Themes/Shared/{0}.cshtml"
};
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (controllerContext.RouteData.Values.ContainsKey("area") && !controllerContext.RouteData.DataTokens.ContainsKey("area"))
controllerContext.RouteData.DataTokens.Add("area", controllerContext.RouteData.Values["area"]);
return base.FindView(controllerContext, viewName, masterName, useCache);
}
}
項目目錄結構如下圖

這樣子發布后的目錄非常的干凈,如圖:

JsonResult執行和MetaData(模型元數據提供機制)
這部分在Controller設計那一節有詳細說明,請往前一步參考
總結
- 大家一定要看看MVC源代碼,
- 腦袋里要能把MVC執行流程串起來
- 主體設計已經講完了,源代碼整理中,工作比較忙,見諒,
- 喜歡請關注,有問題請留言,頭疼得一筆,睡覺,晚安
