MVC實用構(gòu)架實戰(zhàn):項目結(jié)構(gòu)搭建
一、前言
在本文中,將使用代碼的方式來一一解說各個層次。由于要搭建一個基本完整的結(jié)構(gòu),可能文章會比較長。另外,本系列主要出于實用的目的,因而并不會嚴(yán)格按照傳統(tǒng)的三層那樣進行非常明確的層次職能劃分。
二、需求說明
在本系列中,為方便大家理解,將以一個賬戶管理的小系統(tǒng)來進行解說,具體需求如下:
- 用戶信息分主要信息與擴展信息,一個用戶可以有(或沒有)一個用戶擴展信息。
- 記錄用戶的登錄記錄,一個用戶可以有多條登錄記錄,但登錄記錄所屬用戶唯一。
- 一個用戶可以有多個角色,一個角色也可以分配給多個用戶。
三、架構(gòu)基礎(chǔ)
(一) 功能返回值
對于一個操作性業(yè)務(wù)功能(比如添加,修改,刪除),通常我們處理返回值的做法是使用簡單類型,通常會有如下幾種方案:
- 直接返回void,即什么也不返回,在操作過程中拋出異常,只要沒有異常拋出,就認為是操作成功了
- 返回是否操作成功的bool類型的返回值
- 返回操作變更后的新數(shù)據(jù)信息
- 返回表示各種結(jié)果的狀態(tài)碼的返回值
- 返回一個自定義枚舉來表示操作的各種結(jié)果
- 如果要返回多個值,還要使用 out 來添加返回參數(shù)
這樣做有什么不妥之處呢,我們來逐一分析:
- 靠拋異常的方式來終止系統(tǒng)的運行,異常是沿調(diào)用堆棧逐層向上拋出的,會造成很大的性能問題
- bool值太死板,無法表示出業(yè)務(wù)操作中的各種情況
- 返回變更后的數(shù)據(jù),還要與原始數(shù)據(jù)來判斷才能得到是否操作成功
- 用狀態(tài)碼解決了2的問題,但各種狀態(tài)碼的維護成本也會非常高
- 用枚舉值一定程序上解決了翻譯的問題,但還是要把枚舉值翻譯成各種情況的文字描述
- !@#¥%……&
綜上,我們到底需要一個怎樣的業(yè)務(wù)操作結(jié)果呢?
- 要能表示操作的成功失?。◤U話)
- 要能快速表示各種操作場景(如參數(shù)錯誤,查詢數(shù)據(jù)不存在,數(shù)據(jù)狀態(tài)不滿足操作要求等)
- 能返回附加的返回信息(如更新成功后有后續(xù)操作,需要使用更新后的新值)
- 最好在調(diào)用方能使用統(tǒng)一的代碼進行返回值處理
- 最好能自定義返回的文字描述信息
- 最好能把返回給用戶的信息與日志記錄的信息分開
再綜上,顯然簡單類型的返回值滿足不了需求了,那就需要定義一個專門用來封裝返回值信息的返回值類,這里定義如下:
- /// <summary>
- /// 業(yè)務(wù)操作結(jié)果信息類,對操作結(jié)果進行封裝
- /// </summary>
- public class OperationResult
- {
- #region 構(gòu)造函數(shù)
- /// <summary>
- /// 初始化一個 業(yè)務(wù)操作結(jié)果信息類 的新實例
- /// </summary>
- /// <param name="resultType">業(yè)務(wù)操作結(jié)果類型</param>
- public OperationResult(OperationResultType resultType)
- {
- ResultType = resultType;
- }
- /// <summary>
- /// 初始化一個 定義返回消息的業(yè)務(wù)操作結(jié)果信息類 的新實例
- /// </summary>
- /// <param name="resultType">業(yè)務(wù)操作結(jié)果類型</param>
- /// <param name="message">業(yè)務(wù)返回消息</param>
- public OperationResult(OperationResultType resultType, string message)
- : this(resultType)
- {
- Message = message;
- }
- /// <summary>
- /// 初始化一個 定義返回消息與附加數(shù)據(jù)的業(yè)務(wù)操作結(jié)果信息類 的新實例
- /// </summary>
- /// <param name="resultType">業(yè)務(wù)操作結(jié)果類型</param>
- /// <param name="message">業(yè)務(wù)返回消息</param>
- /// <param name="appendData">業(yè)務(wù)返回數(shù)據(jù)</param>
- public OperationResult(OperationResultType resultType, string message, object appendData)
- : this(resultType, message)
- {
- AppendData = appendData;
- }
- /// <summary>
- /// 初始化一個 定義返回消息與日志消息的業(yè)務(wù)操作結(jié)果信息類 的新實例
- /// </summary>
- /// <param name="resultType">業(yè)務(wù)操作結(jié)果類型</param>
- /// <param name="message">業(yè)務(wù)返回消息</param>
- /// <param name="logMessage">業(yè)務(wù)日志記錄消息</param>
- public OperationResult(OperationResultType resultType, string message, string logMessage)
- : this(resultType, message)
- {
- LogMessage = logMessage;
- }
- /// <summary>
- /// 初始化一個 定義返回消息、日志消息與附加數(shù)據(jù)的業(yè)務(wù)操作結(jié)果信息類 的新實例
- /// </summary>
- /// <param name="resultType">業(yè)務(wù)操作結(jié)果類型</param>
- /// <param name="message">業(yè)務(wù)返回消息</param>
- /// <param name="logMessage">業(yè)務(wù)日志記錄消息</param>
- /// <param name="appendData">業(yè)務(wù)返回數(shù)據(jù)</param>
- public OperationResult(OperationResultType resultType, string message, string logMessage, object appendData)
- : this(resultType, message, logMessage)
- {
- AppendData = appendData;
- }
- #endregion
- #region 屬性
- /// <summary>
- /// 獲取或設(shè)置 操作結(jié)果類型
- /// </summary>
- public OperationResultType ResultType { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 操作返回信息
- /// </summary>
- public string Message { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 操作返回的日志消息,用于記錄日志
- /// </summary>
- public string LogMessage { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 操作結(jié)果附加信息
- /// </summary>
- public object AppendData { get; set; }
- #endregion
- }
再定義一個表示業(yè)務(wù)操作結(jié)果的枚舉,枚舉項上有一個DescriptionAttribute的特性,用來作為當(dāng)上面的Message為空時的返回結(jié)果描述。
- /// <summary>
- /// 表示業(yè)務(wù)操作結(jié)果的枚舉
- /// </summary>
- [Description("業(yè)務(wù)操作結(jié)果的枚舉")]
- public enum OperationResultType
- {
- /// <summary>
- /// 操作成功
- /// </summary>
- [Description("操作成功。")]
- Success,
- /// <summary>
- /// 操作取消或操作沒引發(fā)任何變化
- /// </summary>
- [Description("操作沒有引發(fā)任何變化,提交取消。")]
- NoChanged,
- /// <summary>
- /// 參數(shù)錯誤
- /// </summary>
- [Description("參數(shù)錯誤。")]
- ParamError,
- /// <summary>
- /// 指定參數(shù)的數(shù)據(jù)不存在
- /// </summary>
- [Description("指定參數(shù)的數(shù)據(jù)不存在。")]
- QueryNull,
- /// <summary>
- /// 權(quán)限不足
- /// </summary>
- [Description("當(dāng)前用戶權(quán)限不足,不能繼續(xù)操作。")]
- PurviewLack,
- /// <summary>
- /// 非法操作
- /// </summary>
- [Description("非法操作。")]
- IllegalOperation,
- /// <summary>
- /// 警告
- /// </summary>
- [Description("警告")]
- Warning,
- /// <summary>
- /// 操作引發(fā)錯誤
- /// </summary>
- [Description("操作引發(fā)錯誤。")]
- Error,
- }
#p#
(二) 實體基類
對于業(yè)務(wù)實體,有一些相同的且必要的信息,比如信息的創(chuàng)建時間,總是必要的;再比如想讓數(shù)據(jù)庫有一個“回收站”的功能,以給數(shù)據(jù)刪除做個緩沖,或者很多數(shù)據(jù)并非想從數(shù)據(jù)庫中徹底刪除掉,只是暫時的“禁用”一下,添加個邏輯刪除的標(biāo)記也是必要的。再有就是想給所有實體數(shù)據(jù)倉儲操作來個類型限定,以防止傳入了其他非實體類型?;谝陨侠碛桑陀辛讼旅孢@個實體基類:
- /// <summary>
- /// 可持久到數(shù)據(jù)庫的領(lǐng)域模型的基類。
- /// </summary>
- [Serializable]
- public abstract class Entity
- {
- #region 構(gòu)造函數(shù)
- /// <summary>
- /// 數(shù)據(jù)實體基類
- /// </summary>
- protected Entity()
- {
- IsDeleted = false;
- AddDate = DateTime.Now;
- }
- #endregion
- #region 屬性
- /// <summary>
- /// 獲取或設(shè)置 獲取或設(shè)置是否禁用,邏輯上的刪除,非物理刪除
- /// </summary>
- public bool IsDeleted { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 添加時間
- /// </summary>
- [DataType(DataType.DateTime)]
- public DateTime AddDate { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 版本控制標(biāo)識,用于處理并發(fā)
- /// </summary>
- [ConcurrencyCheck]
- [Timestamp]
- public byte[] Timestamp { get; set; }
- #endregion
- }
這里要補充一下,本來實體基類中是可以定義一個表示“實體編號”的Id屬性的,但有個問題,如果定義了,就限定了Id屬性的數(shù)據(jù)類型了,但實際需求中可能有些實體使用自增的int類型,有些實體使用的是易于數(shù)據(jù)合并的guid類型,因此為靈活方便,不在此限制住 Id的數(shù)據(jù)類型。
具體的架構(gòu)分層如下圖所示:
(一) 核心業(yè)務(wù)層
根據(jù) 需求說明 中定義的需求,簡單起見,這里只實現(xiàn)一個簡單的用戶登錄功能:
用戶信息實體:
- /// <summary>
- /// 實體類——用戶信息
- /// </summary>
- [Description("用戶信息")]
- public class Member : Entity
- {
- /// <summary>
- /// 獲取或設(shè)置 用戶編號
- /// </summary>
- public int Id { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 用戶名
- /// </summary>
- [Required]
- [StringLength(20)]
- public string UserName { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 密碼
- /// </summary>
- [Required]
- [StringLength(32)]
- public string Password { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 用戶昵稱
- /// </summary>
- [Required]
- [StringLength(20)]
- public string NickName { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 用戶郵箱
- /// </summary>
- [Required]
- [StringLength(50)]
- public string Email { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 用戶擴展信息
- /// </summary>
- public virtual MemberExtend Extend { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 用戶擁有的角色信息集合
- /// </summary>
- public virtual ICollection<Role> Roles { get; set; }
- /// <summary>
- /// 獲取或設(shè)置 用戶登錄記錄集合
- /// </summary>
- public virtual ICollection<LoginLog> LoginLogs { get; set; }
- }
核心業(yè)務(wù)契約:注意接口的返回值使用了上面定義的返回值類
- /// <summary>
- /// 賬戶模塊核心業(yè)務(wù)契約
- /// </summary>
- public interface IAccountContract
- {
- /// <summary>
- /// 用戶登錄
- /// </summary>
- /// <param name="loginInfo">登錄信息</param>
- /// <returns>業(yè)務(wù)操作結(jié)果</returns>
- OperationResult Login(LoginInfo loginInfo);
- }
核心業(yè)務(wù)實現(xiàn):核心業(yè)務(wù)實現(xiàn)類為抽象類,因沒有數(shù)據(jù)訪問功能,這里使用了一個Members字段來充當(dāng)數(shù)據(jù)源,業(yè)務(wù)功能的實現(xiàn)為虛方法,必要時可以在具體的客戶端(網(wǎng)站、桌面端,移動端)相應(yīng)的派生類中進行重寫。請注意具體實現(xiàn)中對于返回值的處理。這里登錄只負責(zé)最核心的登錄業(yè)務(wù)操作,不涉及比如Http上下文狀態(tài)的操作。
- /// <summary>
- /// 賬戶模塊核心業(yè)務(wù)實現(xiàn)
- /// </summary>
- public abstract class AccountService : IAccountContract
- {
- private static readonly Member[] Members = new[]
- {
- new Member { UserName = "admin", Password = "123456", Email = "admin@gmfcn.net", NickName = "管理員" },
- new Member { UserName = "gmfcn", Password = "123456", Email = "mf.guo@qq.com", NickName = "郭明鋒" }
- };
- private static readonly List<LoginLog> LoginLogs = new List<LoginLog>();
- /// <summary>
- /// 用戶登錄
- /// </summary>
- /// <param name="loginInfo">登錄信息</param>
- /// <returns>業(yè)務(wù)操作結(jié)果</returns>
- public virtual OperationResult Login(LoginInfo loginInfo)
- {
- PublicHelper.CheckArgument(loginInfo, "loginInfo");
- Member member = Members.SingleOrDefault(m => m.UserName == loginInfo.Access || m.Email == loginInfo.Access);
- if (member == null)
- {
- return new OperationResult(OperationResultType.QueryNull, "指定賬號的用戶不存在。");
- }
- if (member.Password != loginInfo.Password)
- {
- return new OperationResult(OperationResultType.Warning, "登錄密碼不正確。");
- }
- LoginLog loginLog = new LoginLog { IpAddress = loginInfo.IpAddress, Member = member };
- LoginLogs.Add(loginLog);
- return new OperationResult(OperationResultType.Success, "登錄成功。", member);
- }
- }
(二) 站點業(yè)務(wù)層
站點業(yè)務(wù)契約:站點業(yè)務(wù)契約繼承核心業(yè)務(wù)契約,即可擁有核心層定義的業(yè)務(wù)功能。站點登錄驗證使用了Forms的Cookie驗證,這里的退出不涉及核心層的操作,因而核心層沒有退出功能
- /// <summary>
- /// 賬戶模塊站點業(yè)務(wù)契約
- /// </summary>
- public interface IAccountSiteContract : IAccountContract
- {
- /// <summary>
- /// 用戶登錄
- /// </summary>
- /// <param name="model">登錄模型信息</param>
- /// <returns>業(yè)務(wù)操作結(jié)果</returns>
- OperationResult Login(LoginModel model);
- /// <summary>
- /// 用戶退出
- /// </summary>
- void Logout();
- }
站點業(yè)務(wù)實現(xiàn):站點業(yè)務(wù)實現(xiàn)繼承核心業(yè)務(wù)實現(xiàn)與站點業(yè)務(wù)契約,負責(zé)把從UI中接收到的視圖模型信息轉(zhuǎn)換為符合核心層定義的參數(shù),并處理與網(wǎng)站狀態(tài)相關(guān)的Session,Cookie等Http相關(guān)業(yè)務(wù)
- /// <summary>
- /// 賬戶模塊站點業(yè)務(wù)實現(xiàn)
- /// </summary>
- public class AccountSiteService : AccountService, IAccountSiteContract
- {
- /// <summary>
- /// 用戶登錄
- /// </summary>
- /// <param name="model">登錄模型信息</param>
- /// <returns>業(yè)務(wù)操作結(jié)果</returns>
- public OperationResult Login(LoginModel model)
- {
- PublicHelper.CheckArgument(model, "model");
- LoginInfo loginInfo = new LoginInfo
- {
- Access = model.Account,
- Password = model.Password,
- IpAddress = HttpContext.Current.Request.UserHostAddress
- };
- OperationResult result = base.Login(loginInfo);
- if (result.ResultType == OperationResultType.Success)
- {
- Member member = (Member)result.AppendData;
- DateTime expiration = model.IsRememberLogin
- ? DateTime.Now.AddDays(7)
- : DateTime.Now.Add(FormsAuthentication.Timeout);
- FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, member.UserName, DateTime.Now, expiration,
- true, member.NickName, FormsAuthentication.FormsCookiePath);
- HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
- if (model.IsRememberLogin)
- {
- cookie.Expires = DateTime.Now.AddDays(7);
- }
- HttpContext.Current.Response.Cookies.Set(cookie);
- result.AppendData = null;
- }
- return result;
- }
- /// <summary>
- /// 用戶退出
- /// </summary>
- public void Logout()
- {
- FormsAuthentication.SignOut();
- }
- }
#p#
(三) 站點展現(xiàn)層
MVC控制器:Action提供統(tǒng)一風(fēng)格的代碼來對業(yè)務(wù)操作結(jié)果OperationResult進行處理
- public class AccountController : Controller
- {
- public AccountController()
- {
- AccountContract = new AccountSiteService();
- }
- #region 屬性
- public IAccountSiteContract AccountContract { get; set; }
- #endregion
- #region 視圖功能
- public ActionResult Login()
- {
- string returnUrl = Request.Params["returnUrl"];
- returnUrl = returnUrl ?? Url.Action("Index", "Home", new { area = "" });
- LoginModel model = new LoginModel
- {
- ReturnUrl = returnUrl
- };
- return View(model);
- }
- [HttpPost]
- public ActionResult Login(LoginModel model)
- {
- try
- {
- OperationResult result = AccountContract.Login(model);
- string msg = result.Message ?? result.ResultType.ToDescription();
- if (result.ResultType == OperationResultType.Success)
- {
- return Redirect(model.ReturnUrl);
- }
- ModelState.AddModelError("", msg);
- return View(model);
- }
- catch (Exception e)
- {
- ModelState.AddModelError("", e.Message);
- return View(model);
- }
- }
- public ActionResult Logout( )
- {
- string returnUrl = Request.Params["returnUrl"];
- returnUrl = returnUrl ?? Url.Action("Index", "Home", new { area = "" });
- if (User.Identity.IsAuthenticated)
- {
- AccountContract.Logout();
- }
- return Redirect(returnUrl);
- }
- #endregion
- }
MVC 視圖:
- @model GMF.Demo.Site.Models.LoginModel
- @{
- ViewBag.Title = "Login";
- Layout = "~/Views/Shared/_Layout.cshtml";
- }
- <h2>Login</h2>
- @using (Html.BeginForm()) {
- @Html.AntiForgeryToken()
- @Html.ValidationSummary(true)
- <fieldset>
- <legend>LoginModel</legend>
- <div class="editor-label">
- @Html.LabelFor(model => model.Account)
- </div>
- <div class="editor-field">
- @Html.EditorFor(model => model.Account)
- @Html.ValidationMessageFor(model => model.Account)
- </div>
- <div class="editor-label">
- @Html.LabelFor(model => model.Password)
- </div>
- <div class="editor-field">
- @Html.EditorFor(model => model.Password)
- @Html.ValidationMessageFor(model => model.Password)
- </div>
- <div class="editor-label">
- @Html.LabelFor(model => model.IsRememberLogin)
- </div>
- <div class="editor-field">
- @Html.EditorFor(model => model.IsRememberLogin)
- @Html.ValidationMessageFor(model => model.IsRememberLogin)
- </div>
- @Html.HiddenFor(m => m.ReturnUrl)
- <p>
- <input type="submit" value="登錄" />
- </p>
- </fieldset>
- }
- <div>
- @Html.ActionLink("Back to List", "Index", "Home")
- </div>
- @section Scripts {
- @Scripts.Render("~/bundles/jqueryval")
- }
至此,整個項目構(gòu)架搭建完成,運行結(jié)果如下:
在本篇中,網(wǎng)站的Controller是依賴于站點業(yè)務(wù)實現(xiàn)與核心業(yè)務(wù)實現(xiàn)的,在下一篇中,將使用.net 4.0自帶的MEF作為IOC對層與層之間的依賴進行解耦。
原文鏈接:http://www.cnblogs.com/guomingfeng/archive/2013/05/20/mvc-build.html