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实用构架设计(三)——EF-Code First(6):数据更新最佳实践
前言 最近在整理EntityFramework数据更新的代码,颇有体会,觉得有分享的价值,于是记录下来,让需要的人少走些弯路也是好的. 为方便起见,先创建一个控制台工程,使用using(var db ...
- NET 项目结构搭建
NET 项目结构搭建 我们头开始,从简单的单项目解决方案,逐步添加业务逻辑的约束,从应用逻辑和领域逻辑两方面考虑,从简单的单个项目逐步搭建一个多项目的解决方案.主要内容:(1)搭建应用逻辑和领域逻辑都 ...
- vue2项目结构搭建
vue2项目结构初搭建与项目基本流程 一.开始项目结构搭建的重要性 决定项目是否能够健康成长 决定了项目是否利于多人协作开发 决定了项目是否利于后期维护 决定了项目是否性能良好 决定了代码是否重用率降 ...
- SpringMVC+Spring+mybatis项目从零开始--分布式项目结构搭建
转载出处: SpringMVC+Spring+mybatis+Redis项目从零开始--分布式项目结构搭建 /** 本文为博主原创文章,如转载请附链接. **/ SSM框架web项目从零开始--分布式 ...
- 使用.NET 6开发TodoList应用(2)——项目结构搭建
为了不影响阅读的体验,我把系列导航放到文章最后了,有需要的小伙伴可以直接通过导航跳转到对应的文章 : P TodoList需求简介 首先明确一下我们即将开发的这个TodoList应用都需要完成什么功能 ...
- 架构系列:ASP.NET 项目结构搭建
我们头开始,从简单的单项目解决方案,逐步添加业务逻辑的约束,从应用逻辑和领域逻辑两方面考虑,从简单的单个项目逐步搭建一个多项目的解决方案.主要内容:(1)搭建应用逻辑和领域逻辑都简单的单项目 (2)为 ...
- vue2.0 仿手机新闻站(二)项目结构搭建 及 路由配置
1.项目结构 $ vue init webpack-simple news $ npm install vuex vue-router axios style-loader css-loader -D ...
- 25、Flask实战第25天:项目结构搭建
创建一个虚拟环境bbs,并安装flask框架 #cmd进入DOS窗口 mkvirtualenv bbs pip install flask 在本地磁盘D新建项目目录:bbs 打开pycharm,创建f ...
- 13: vue项目结构搭建与开发
vue其他篇 01: vue.js安装 02: vue.js常用指令 03: vuejs 事件.模板.过滤器 目录: 1.1 初始化项目 1.2 配置API接口,模拟后台数据 1.3 项目整体结构化开 ...
随机推荐
- [k8s]zookeeper集群在k8s的搭建(statefulset模式)-pod的调度
之前一直docker-compose跑zk集群,现在把它挪到k8s集群里. docker-compose跑zk集群 zk集群in k8s部署 参考: https://github.com/kubern ...
- Nginx的upstream目前支持5种分配方式
本文转自:http://mp.weixin.qq.com/s?__biz=MzI4OTU3ODk3NQ==&mid=2247484058&idx=1&sn=f4da816bfa ...
- 【iCore1S 双核心板_FPGA】例程十二:基于单口RAM的ARM+FPGA数据存取实验
实验现象: 核心代码: module single_port_ram( input CLK_12M, input WR, input RD, input CS0, inout [:]DB, input ...
- 【bootstrap组件】几个常用的好用bs组件
这次开发了个小TRS系统,虽然是很小,但是作为初心者,第一次用到了很多看起来洋气使用起来有相对简单的各种前端(主要是和bootstrap配合使用)组件.包括bootstrap-select2,boot ...
- Retrieve id of record just inserted into a Java DB (Derby) database
https://stackoverflow.com/questions/4894754/retrieve-id-of-record-just-inserted-into-a-java-db-derby ...
- 导出表结构sql语句
-- C:/dba必需是已经存在的目录 -- create or replace directory UTL_DIR as 'C:\dba'; --用sys用户登录给要访问的用户指定访问目录的权限gr ...
- Java知多少(1) 语言概述
Java语言是SUN(Stanford University Network,斯坦福大学网络公司)公司1995年推出的一门高级编程语言,起初主要应用在小型消费电子产品上,后来随着互联网的兴起,Java ...
- Tensorflow读写TFRecords文件
在使用slim之类的tensorflow自带框架的时候一般默认的数据格式就是TFRecords,在训练的时候使用TFRecords中数据的流程如下:使用input pipeline读取tfrecord ...
- puppet 用户和组资源管理
1. 用户和组资源的特性: 1.1 用户特性: allows_duplicates 支持含有相同UID的用户. manages_aix_lam ...
- 理解syslinux,SYSLINUX和PXELINUX
在研究网络装机的过程中,菜菜地被Syslinux.SYSLINUX和PXELINUX这些定义折磨了一下 它们有什么区别和联系?为什么配置PXELINUX要安装的是Syslinux而不是Pxelinux ...