MVC实用构架实战(一)——项目结构搭建
前言
在《上篇》中,已经把项目整体结构规划做了个大概的规划。在本文中,将使用代码的方式来一一解说各个层次。由于要搭建一个基本完整的结构,可能文章会比较长。另外,本系列主要出于实用的目的,因而并不会严格按照传统的三层那样进行非常明确的层次职能划分。
需求说明
在本系列中,为方便大家理解,将以一个账户管理的小系统来进行解说,具体需求如下:
- 用户信息分主要信息与扩展信息,一个用户可以有(或没有)一个用户扩展信息。
- 记录用户的登录记录,一个用户可以有多条登录记录,但登录记录所属用户唯一。
- 一个用户可以有多个角色,一个角色也可以分配给多个用户。
架构基础
功能返回值
对于一个操作性业务功能(比如添加,修改,删除),通常我们处理返回值的做法是使用简单类型,通常会有如下几种方案:
- 直接返回void,即什么也不返回,在操作过程中抛出异常,只要没有异常抛出,就认为是操作成功了
- 返回是否操作成功的bool类型的返回值
- 返回操作变更后的新数据信息
- 返回表示各种结果的状态码的返回值
- 返回一个自定义枚举来表示操作的各种结果
- 如果要返回多个值,还要使用 out 来添加返回参数
这样做有什么不妥之处呢,我们来逐一分析:
- 靠抛异常的方式来终止系统的运行,异常是沿调用堆栈逐层向上抛出的,会造成很大的性能问题
- bool值太死板,无法表示出业务操作中的各种情况
- 返回变更后的数据,还要与原始数据来判断才能得到是否操作成功
- 用状态码解决了2的问题,但各种状态码的维护成本也会非常高
- 用枚举值一定程序上解决了翻译的问题,但还是要把枚举值翻译成各种情况的文字描述
- !@#¥%……&
综上,我们到底需要一个怎样的业务操作结果呢?
- 要能表示操作的成功失败(废话)
- 要能快速表示各种操作场景(如参数错误,查询数据不存在,数据状态不满足操作要求等)
- 能返回附加的返回信息(如更新成功后有后续操作,需要使用更新后的新值)
- 最好在调用方能使用统一的代码进行返回值处理
- 最好能自定义返回的文字描述信息
- 最好能把返回给用户的信息与日志记录的信息分开
再综上,显然简单类型的返回值满足不了需求了,那就需要定义一个专门用来封装返回值信息的返回值类,这里定义如下:
1 /// <summary> 2 /// 业务操作结果信息类,对操作结果进行封装 3 /// </summary> 4 public class OperationResult 5 { 6 #region 构造函数 7 8 /// <summary> 9 /// 初始化一个 业务操作结果信息类 的新实例 10 /// </summary> 11 /// <param name="resultType">业务操作结果类型</param> 12 public OperationResult(OperationResultType resultType) 13 { 14 ResultType = resultType; 15 } 16 17 /// <summary> 18 /// 初始化一个 定义返回消息的业务操作结果信息类 的新实例 19 /// </summary> 20 /// <param name="resultType">业务操作结果类型</param> 21 /// <param name="message">业务返回消息</param> 22 public OperationResult(OperationResultType resultType, string message) 23 : this(resultType) 24 { 25 Message = message; 26 } 27 28 /// <summary> 29 /// 初始化一个 定义返回消息与附加数据的业务操作结果信息类 的新实例 30 /// </summary> 31 /// <param name="resultType">业务操作结果类型</param> 32 /// <param name="message">业务返回消息</param> 33 /// <param name="appendData">业务返回数据</param> 34 public OperationResult(OperationResultType resultType, string message, object appendData) 35 : this(resultType, message) 36 { 37 AppendData = appendData; 38 } 39 40 /// <summary> 41 /// 初始化一个 定义返回消息与日志消息的业务操作结果信息类 的新实例 42 /// </summary> 43 /// <param name="resultType">业务操作结果类型</param> 44 /// <param name="message">业务返回消息</param> 45 /// <param name="logMessage">业务日志记录消息</param> 46 public OperationResult(OperationResultType resultType, string message, string logMessage) 47 : this(resultType, message) 48 { 49 LogMessage = logMessage; 50 } 51 52 /// <summary> 53 /// 初始化一个 定义返回消息、日志消息与附加数据的业务操作结果信息类 的新实例 54 /// </summary> 55 /// <param name="resultType">业务操作结果类型</param> 56 /// <param name="message">业务返回消息</param> 57 /// <param name="logMessage">业务日志记录消息</param> 58 /// <param name="appendData">业务返回数据</param> 59 public OperationResult(OperationResultType resultType, string message, string logMessage, object appendData) 60 : this(resultType, message, logMessage) 61 { 62 AppendData = appendData; 63 } 64 65 #endregion 66 67 #region 属性 68 69 /// <summary> 70 /// 获取或设置 操作结果类型 71 /// </summary> 72 public OperationResultType ResultType { get; set; } 73 74 /// <summary> 75 /// 获取或设置 操作返回信息 76 /// </summary> 77 public string Message { get; set; } 78 79 /// <summary> 80 /// 获取或设置 操作返回的日志消息,用于记录日志 81 /// </summary> 82 public string LogMessage { get; set; } 83 84 /// <summary> 85 /// 获取或设置 操作结果附加信息 86 /// </summary> 87 public object AppendData { get; set; } 88 89 #endregion 90 }
再定义一个表示业务操作结果的枚举,枚举项上有一个DescriptionAttribute的特性,用来作为当上面的Message为空时的返回结果描述。
1 /// <summary> 2 /// 表示业务操作结果的枚举 3 /// </summary> 4 [Description("业务操作结果的枚举")] 5 public enum OperationResultType 6 { 7 /// <summary> 8 /// 操作成功 9 /// </summary> 10 [Description("操作成功。")] 11 Success, 12 13 /// <summary> 14 /// 操作取消或操作没引发任何变化 15 /// </summary> 16 [Description("操作没有引发任何变化,提交取消。")] 17 NoChanged, 18 19 /// <summary> 20 /// 参数错误 21 /// </summary> 22 [Description("参数错误。")] 23 ParamError, 24 25 /// <summary> 26 /// 指定参数的数据不存在 27 /// </summary> 28 [Description("指定参数的数据不存在。")] 29 QueryNull, 30 31 /// <summary> 32 /// 权限不足 33 /// </summary> 34 [Description("当前用户权限不足,不能继续操作。")] 35 PurviewLack, 36 37 /// <summary> 38 /// 非法操作 39 /// </summary> 40 [Description("非法操作。")] 41 IllegalOperation, 42 43 /// <summary> 44 /// 警告 45 /// </summary> 46 [Description("警告")] 47 Warning, 48 49 /// <summary> 50 /// 操作引发错误 51 /// </summary> 52 [Description("操作引发错误。")] 53 Error, 54 }
实体基类
对于业务实体,有一些相同的且必要的信息,比如信息的创建时间,总是必要的;再比如想让数据库有一个“回收站”的功能,以给数据删除做个缓冲,或者很多数据并非想从数据库中彻底删除掉,只是暂时的“禁用”一下,添加个逻辑删除的标记也是必要的。再有就是想给所有实体数据仓储操作来个类型限定,以防止传入了其他非实体类型。基于以上理由,就有了下面这个实体基类:
1 /// <summary> 2 /// 可持久到数据库的领域模型的基类。 3 /// </summary> 4 [Serializable] 5 public abstract class Entity 6 { 7 #region 构造函数 8 9 /// <summary> 10 /// 数据实体基类 11 /// </summary> 12 protected Entity() 13 { 14 IsDeleted = false; 15 AddDate = DateTime.Now; 16 } 17 18 #endregion 19 20 #region 属性 21 22 /// <summary> 23 /// 获取或设置 获取或设置是否禁用,逻辑上的删除,非物理删除 24 /// </summary> 25 public bool IsDeleted { get; set; } 26 27 /// <summary> 28 /// 获取或设置 添加时间 29 /// </summary> 30 [DataType(DataType.DateTime)] 31 public DateTime AddDate { get; set; } 32 33 /// <summary> 34 /// 获取或设置 版本控制标识,用于处理并发 35 /// </summary> 36 [ConcurrencyCheck] 37 [Timestamp] 38 public byte[] Timestamp { get; set; } 39 40 #endregion 41 }
这里要补充一下,本来实体基类中是可以定义一个表示“实体编号”的Id属性的,但有个问题,如果定义了,就限定了Id属性的数据类型了,但实际需求中可能有些实体使用自增的int类型,有些实体使用的是易于数据合并的guid类型,因此为灵活方便,不在此限制住 Id的数据类型。
架构分层
具体的架构分层如下图所示:
核心业务层
根据 需求说明 中定义的需求,简单起见,这里只实现一个简单的用户登录功能:
用户信息实体:
1 /// <summary> 2 /// 实体类——用户信息 3 /// </summary> 4 [Description("用户信息")] 5 public class Member : Entity 6 { 7 /// <summary> 8 /// 获取或设置 用户编号 9 /// </summary> 10 public int Id { get; set; } 11 12 /// <summary> 13 /// 获取或设置 用户名 14 /// </summary> 15 [Required] 16 [StringLength(20)] 17 public string UserName { get; set; } 18 19 /// <summary> 20 /// 获取或设置 密码 21 /// </summary> 22 [Required] 23 [StringLength(32)] 24 public string Password { get; set; } 25 26 /// <summary> 27 /// 获取或设置 用户昵称 28 /// </summary> 29 [Required] 30 [StringLength(20)] 31 public string NickName { get; set; } 32 33 /// <summary> 34 /// 获取或设置 用户邮箱 35 /// </summary> 36 [Required] 37 [StringLength(50)] 38 public string Email { get; set; } 39 40 /// <summary> 41 /// 获取或设置 用户扩展信息 42 /// </summary> 43 public virtual MemberExtend Extend { get; set; } 44 45 /// <summary> 46 /// 获取或设置 用户拥有的角色信息集合 47 /// </summary> 48 public virtual ICollection<Role> Roles { get; set; } 49 50 /// <summary> 51 /// 获取或设置 用户登录记录集合 52 /// </summary> 53 public virtual ICollection<LoginLog> LoginLogs { get; set; } 54 }
核心业务契约:注意接口的返回值使用了上面定义的返回值类
1 /// <summary> 2 /// 账户模块核心业务契约 3 /// </summary> 4 public interface IAccountContract 5 { 6 /// <summary> 7 /// 用户登录 8 /// </summary> 9 /// <param name="loginInfo">登录信息</param> 10 /// <returns>业务操作结果</returns> 11 OperationResult Login(LoginInfo loginInfo); 12 }
核心业务实现:核心业务实现类为抽象类,因没有数据访问功能,这里使用了一个Members字段来充当数据源,业务功能的实现为虚方法,必要时可以在具体的客户端(网站、桌面端,移动端)相应的派生类中进行重写。请注意具体实现中对于返回值的处理。这里登录只负责最核心的登录业务操作,不涉及比如Http上下文状态的操作。
1 /// <summary> 2 /// 账户模块核心业务实现 3 /// </summary> 4 public abstract class AccountService : IAccountContract 5 { 6 private static readonly Member[] Members = new[] 7 { 8 new Member { UserName = "admin", Password = "123456", Email = "admin@gmfcn.net", NickName = "管理员" }, 9 new Member { UserName = "gmfcn", Password = "123456", Email = "mf.guo@qq.com", NickName = "郭明锋" } 10 }; 11 12 private static readonly List<LoginLog> LoginLogs = new List<LoginLog>(); 13 14 /// <summary> 15 /// 用户登录 16 /// </summary> 17 /// <param name="loginInfo">登录信息</param> 18 /// <returns>业务操作结果</returns> 19 public virtual OperationResult Login(LoginInfo loginInfo) 20 { 21 PublicHelper.CheckArgument(loginInfo, "loginInfo"); 22 Member member = Members.SingleOrDefault(m => m.UserName == loginInfo.Access || m.Email == loginInfo.Access); 23 if (member == null) 24 { 25 return new OperationResult(OperationResultType.QueryNull, "指定账号的用户不存在。"); 26 } 27 if (member.Password != loginInfo.Password) 28 { 29 return new OperationResult(OperationResultType.Warning, "登录密码不正确。"); 30 } 31 LoginLog loginLog = new LoginLog { IpAddress = loginInfo.IpAddress, Member = member }; 32 LoginLogs.Add(loginLog); 33 return new OperationResult(OperationResultType.Success, "登录成功。", member); 34 } 35 }
站点业务层
站点业务契约:站点业务契约继承核心业务契约,即可拥有核心层定义的业务功能。站点登录验证使用了Forms的Cookie验证,这里的退出不涉及核心层的操作,因而核心层没有退出功能
1 /// <summary> 2 /// 账户模块站点业务契约 3 /// </summary> 4 public interface IAccountSiteContract : IAccountContract 5 { 6 /// <summary> 7 /// 用户登录 8 /// </summary> 9 /// <param name="model">登录模型信息</param> 10 /// <returns>业务操作结果</returns> 11 OperationResult Login(LoginModel model); 12 13 /// <summary> 14 /// 用户退出 15 /// </summary> 16 void Logout(); 17 }
站点业务实现:站点业务实现继承核心业务实现与站点业务契约,负责把从UI中接收到的视图模型信息转换为符合核心层定义的参数,并处理与网站状态相关的Session,Cookie等Http相关业务。
在这里需要注意的是,目前的项目中并没有加入IOC组件来对层与层之间进行解耦,在上层调用下层的时候,我们仍然以如下方式来进行实例化:
1 IAccountSiteContract accountContract = new AccountSiteService();
这会造成层与层之间紧耦合,在后面的文章中,会加入.NET自带的MEF组件进行层之间的解耦,到时层对象实现化的工作将由MEF来完成,就需要把 AccountSiteService 类的可访问性由 public 修改为 internal,以防止出现上面的实例化代码出现。
1 /// <summary> 2 /// 账户模块站点业务实现 3 /// </summary> 4 public class AccountSiteService : AccountService, IAccountSiteContract 5 { 6 /// <summary> 7 /// 用户登录 8 /// </summary> 9 /// <param name="model">登录模型信息</param> 10 /// <returns>业务操作结果</returns> 11 public OperationResult Login(LoginModel model) 12 { 13 PublicHelper.CheckArgument(model, "model"); 14 LoginInfo loginInfo = new LoginInfo 15 { 16 Access = model.Account, 17 Password = model.Password, 18 IpAddress = HttpContext.Current.Request.UserHostAddress 19 }; 20 OperationResult result = base.Login(loginInfo); 21 if (result.ResultType == OperationResultType.Success) 22 { 23 Member member = (Member)result.AppendData; 24 DateTime expiration = model.IsRememberLogin 25 ? DateTime.Now.AddDays(7) 26 : DateTime.Now.Add(FormsAuthentication.Timeout); 27 FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, member.UserName, DateTime.Now, expiration, 28 true, member.NickName, FormsAuthentication.FormsCookiePath); 29 HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket)); 30 if (model.IsRememberLogin) 31 { 32 cookie.Expires = DateTime.Now.AddDays(7); 33 } 34 HttpContext.Current.Response.Cookies.Set(cookie); 35 result.AppendData = null; 36 } 37 return result; 38 } 39 40 /// <summary> 41 /// 用户退出 42 /// </summary> 43 public void Logout() 44 { 45 FormsAuthentication.SignOut(); 46 } 47 }
站点展现层
MVC控制器:Action提供统一风格的代码来对业务操作结果OperationResult进行处理
1 public class AccountController : Controller 2 { 3 public AccountController() 4 { 5 AccountContract = new AccountSiteService(); 6 } 7 8 #region 属性 9 10 public IAccountSiteContract AccountContract { get; set; } 11 12 #endregion 13 14 #region 视图功能 15 16 public ActionResult Login() 17 { 18 string returnUrl = Request.Params["returnUrl"]; 19 returnUrl = returnUrl ?? Url.Action("Index", "Home", new { area = "" }); 20 LoginModel model = new LoginModel 21 { 22 ReturnUrl = returnUrl 23 }; 24 return View(model); 25 } 26 27 [HttpPost] 28 public ActionResult Login(LoginModel model) 29 { 30 try 31 { 32 OperationResult result = AccountContract.Login(model); 33 string msg = result.Message ?? result.ResultType.ToDescription(); 34 if (result.ResultType == OperationResultType.Success) 35 { 36 return Redirect(model.ReturnUrl); 37 } 38 ModelState.AddModelError("", msg); 39 return View(model); 40 } 41 catch (Exception e) 42 { 43 ModelState.AddModelError("", e.Message); 44 return View(model); 45 } 46 } 47 48 public ActionResult Logout( ) 49 { 50 string returnUrl = Request.Params["returnUrl"]; 51 returnUrl = returnUrl ?? Url.Action("Index", "Home", new { area = "" }); 52 if (User.Identity.IsAuthenticated) 53 { 54 AccountContract.Logout(); 55 } 56 return Redirect(returnUrl); 57 } 58 59 #endregion 60 }
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") }
至此,整个项目构架搭建完成,运行结果如下:
在本篇中,网站的Controller是依赖于站点业务实现与核心业务实现的,在下一篇中,将使用.net 4.0自带的MEF作为IOC对层与层之间的依赖进行解耦。
源码下载
为了让大家能第一时间获取到本架构的最新代码,也为了方便我对代码的管理,本系列的源码已加入微软的开源项目网站 http://www.codeplex.com,地址为:
https://gmframework.codeplex.com/
可以通过下列途径获取到最新代码:
- 如果你是本项目的参与者,可以通过VS自带的团队TFS直接连接到 https://tfs.codeplex.com:443/tfs/TFS17 获取最新代码
- 如果你安装有SVN客户端(亲测TortoiseSVN 1.6.7可用),可以连接到 https://gmframework.svn.codeplex.com/svn 获取最新代码
- 如果以上条件都不满足,你可以进入页面 https://gmframework.codeplex.com/SourceControl/latest 查看最新代码,也可以点击页面上的 Download 链接进行压缩包的下载,你还可以点击页面上的 History 链接获取到历史版本的源代码
- 如果你想和大家一起学习MVC,学习EF,欢迎加入Q群:5008599(群发言仅限技术讨论,拒绝闲聊,拒绝酱油,拒绝广告)
- 如果你想与我共同来完成这个开源项目,可以随时联系我。
系列导航
- MVC实用架构设计(〇)——总体设计
- MVC实用架构设计(一)——项目结构搭建
- MVC实用架构设计(二)——使用MEF应用IOC
- MVC实用架构设计(三)——EF-Code First(1):Repository,UnitOfWork,DbContext
- MVC实用架构设计(三)——EF-Code First(2):实体映射、数据迁移,重构
- MVC实用架构设计(三)——EF-Code First(3):使用T4模板生成相似代码
- MVC实用架构设计(三)——EF-Code First(4):数据查询
- MVC实用架构设计(三)——EF-Code First(5):二级缓存
- MVC实体架构设计(三)——EF-Code First(6):数据更新
- 未完待续。。。
作者:郭明锋
Q群:MVC EF技术交流(5008599) OSharp开发框架交流(85895249)
出处:https://www.cnblogs.com/guomingfeng
声明:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。