实际开发中,实现的方式方法五花八门:
演示:注册/登录/NeedLogOn的实现
注意:project无法双向引用,无法穿透引用
#理解#:.sln和.csproj的作用(复习)
独立到一个project,只是被其他项目引用,不会引用其他任何项目。
采用充血模式,完成大量业务逻辑。
public void Register() { InviteCode = new Random().Next(9999).ToString(); //邀请码 if (InvitedBy != null) //积分奖励 { InvitedBy.Credit += 10; }//else nothing }
使用EF连接数据库,引用entitis,实现仓储模式。
public int Save(User user) { using (SqlContext context = new SqlContext()) { context.Users.Add(user); context.SaveChanges();SqlContext需要被声明(同时引入EF组件):
public class SqlContext : DbContext { public DbSet<User> Users { get; set; } public SqlContext() : base("17bang")注意:因为最后所有的其他项目,都会被编译成被ASP.NET MVC项目调用的.dll文件 ,
演示:没有这样做会报的错误。
同时引用Entities和Repositories,为UI层提供一个简洁的调用接口:
public int Save(IndexModel model) { User user = new User { UserName = model.UserName, Password = model.Password, //已经在UI层(Controller)里加密 }; user.Register(); return userRepository.Save(user); }
贫血模式的DTO:
public class IndexModel : Shared.CaptchaModel { [Required(ErrorMessage = "* 用户名不能为空")] public string UserName { get; set; } [Required(ErrorMessage = "* 密码不能为空")] public string Password { get; set; } [Required(ErrorMessage = "* 确认密码不能为空")] [Compare(nameof(Password), ErrorMessage = "确认密码和密码不一致")] public string ComfirmPassword { get; set; } public string InvitedByName { get; set; } public string InvitedByCode { get; set; }对比:Entities.User和Register.IndexModel(PerViewPerModel命名风格)的异同。
ASP.NET MVC框架,负责接受用户的输入和页面的呈现等。除此以外,传入Model,直接调用Service:
[HttpPost] public ActionResult Index(IndexModel model) { //加密过程也可以移到SRV或BLL model.Password = model.Password.MD5Encrypt(); int id = userService.Save(model);
字段userService在构造函数中实例化:
private UserService userService; public RegisterController() { userService = new UserService();
演示:能够将用户名和密码存入数据库。
#理解#:各个项目之间的调用关系。
首先entity是可以抽象的:所有的entity都是可以用Id标识的,所以我们可以有一个BaseEntity类:
public class BaseEntity { public int Id { get; set; }
public class User : BaseEntity public class Article : BaseEntity于是,repository也可以相应的抽象,比如,所有的repository都可以:
这首先需要一个泛型基类BaseRepository<T>:
public class BaseRepository<T> where T : BaseEntity
@想一想@:为什么要添加泛型约束(where)?
public T Find(int id) public int Save(T entity)
子类继承的时候提供不同的泛型参数,自然就可以存入User/Article等entity
public class UserRepository : BaseRepository<User>{ }
但Find()/Save()等方法如何利用EF实现呢?必须依赖
在基类中声明并实例化:
private SqlContext context; public BaseRepository() { context = new SqlContext();
public UserRepository(SqlContext context): base(context)
这样隐藏无参构造函数,使得开发人员初始化repository时不得不传入DbContext参数,避免遗忘疏忽。
直接复制子类的内容既不合理(entity不仅仅是User呀),也没法通过编译:
检查context.Users,发现它的类型是DbSet<User>;而context.Articles,它的类型是DbSet<Article>:所以……
能不能:(#仔细体会,^_^#)
protected DbSet<T> dbSet; //注意这里的protected! public int Save(T entity) { dbSet.Add(entity);
但这样声明的DbSet还没有和DbContext“建立联系”,会是null值,所以我们需要在构造函数中:
dbSet = context.Set<T>();
#小技巧#:
public class CommentRepository : BaseRepository<Comment> { void SomeFunc() { //调用了本应只属于ArticleRepository的DbSet sqlContext.Articles.Where(/*……*/); } }这不是“便利”,而是“职责混乱”。甚至更严格的做法:移除掉SqlDbContext中的DbSet属性,把映射放到 OnModelCreating()中
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<User>(); modelBuilder.Entity<Article>();
public class UserRepository : BaseRepository<User> { public User GetByName(string name) { return dbSet.Where(u => u.UserName == name).SingleOrDefault();
entity的继承层次还可以进n步的扩展。比如还可以从Article/Suggest/Problem中抽象出Content,他们都有Body/Author,可以Publish()……
但是,注意,注意,注意(重要的事说三遍)!不要“为了重用”,仅仅是因为他们有相同的属性,就把他们“归为一类”,比如:(复习:桌子和动物都有腿……)
PS:所以,我一直强调,尽量不要用类名做属性名,应该是Article.Author和Message.Sender,而不是Article.User和Message.User
#再次体会#:为什么所有的entity都是BaseEntity的基类?选择:
1和2表述的差别在哪?
UserRepository中添加方法:
public User GetByName(string name) { return context.Users.Where(u => u.UserName == name).SingleOrDefault();
Service中根据邀请人用户名获得邀请人对象:
public int Save(IndexModel model) { User invitedBy = null; if (!string.IsNullOrEmpty(model.InvitedByName)) { invitedBy = userRepository.GetByName(model.InvitedByName); } User user = new User { InvitedBy = invitedBy,
Repository应该和Entity对应,一个Entity一个Repository,所以这里还可以有ArticleRepository:
public class ArticleRepository { private SqlContext context; public ArticleRepository() { context = new SqlContext();
文章的发布,至少会涉及到这两个Repository:一个获取当前用户(作者),一个发布文章。
public int Publish(NewModel model) { int userId = 1; //实际上应从cookie中获取 User author = userRepository.Get(userId); Article article = new Article { Title = model.Title, Body = model.Body, Author = author }; articleRepository.Save(article);
运行之后,检查数据库,额外生成了一个新的邀请人!@想一想@:为什么?
因为我们使用了两个不同的DbContext,进行SaveChanges()的DbContext没有对其他DbContext加载的邀请人对象的追踪(复习:ChangeTracker),所以会将其视为transient(一个新的entity)添加(INSERT)到数据库中。
解决的办法就是:
但怎么让这个两个Repository共用一个DbContext呢?
让DbContext从外部传入:
public ArticleRepository(SqlContext context) { this.context = context;
public ArticleService() { SqlContext context = new SqlContext(); articleRepository = new ArticleRepository(context); userRepository = new UserRepository(context);
private SqlContext context; public BaseRepository(SqlContext context) { this.context = context;
这样子类就必须:
public ArticleRepository(SqlContext context) : base(context)
项目中太多地方(发布/点赞/评论/消息……)都需要获取到“当前用户”,所以需要一个可以快速获取它的封装。
问题是这个封装放在哪里?
因为当前用户需要从cookie中获取,所以这是最直观的想法。
可以引入一个BaseController,作为其他Controller的基类,用于存放抽象出来的共用方法等。
public class BaseController : Controller
在BaseController中添加一个CurrentUserId属性:
public int GetCurrentUser(){ /*从cookie中取值等*/ }
然后让所有Controller继承自BaseController:
public class ArticleController : BaseController但这样做的问题是:太多太多的Service方法都要额外添加一个currentUserId参数,比如:
int Publish(NewModel model, int authorId); int Edit(NewModel model, int authorId); void Agree(int articleId, int voterId); //...
显得非常累赘!
当前用户信息不需要由UI层向SRV传递,直接由SRV获取。这样,上述Service方法就可以没有currentUserId参数:
int Publish(NewModel model); int Edit(NewModel model); void Agree(int articleId); //...
同样的,我们也把CurrentUserId属性放置在BaseService中:
public class BaseService { public User GetCurrentUser()但是,要怎么才能在SRV中获得cookie?
需要引入:
System.Web.dll是.NET Framework自带的Assembly,不需要NuGet管理,(如果不直接Alt+Enter智能提示引入的话)
直接在References上右键Add Reference,弹出窗口中选择Assemblies,找到:
然后就可以使用HttpContext.Current可以获得当前Http上下文,这里面就能取到Cookie
HttpCookie cookie = HttpContext.Current.Request.Cookies[Cookie.User];
cookie中要使用的“键”,应该用Const,但现在不仅仅是UI,SRV层以及其他层都可能要使用它,怎么办?
引入一个GLB项目文件夹,里面添加一个Global的类库项目,放这里面:
以后“全局性的”代码都可以放这里。
然后
if (cookie == null) { return null; } string userId = cookie.Values[Cookie.UserId];
User user = userRepository.Find(Convert.ToInt32(userId));
if (user.Password != cookie.Values[Cookie.Password])
都类似于之前LogonStatus的代码。
但是,中途如果从了“意外”,比如:
怎么办?抛异常:可以。但是注意抛了异常就不会处理cookie,哪怕这样:
//把处理(删除)cookie的代码写在抛异常之前 HttpContext.Current.Response.Cookies.Add(new HttpCookie(Cookie.User) { }); throw new Exception("");
因为抛异常之后,这里的response无法正常到达客户端。
#体会#:从严格意义上讲,让SRV层牵涉cookie/System.Web,破坏了UI/SRV层之间的职责划分。但是,综合权衡利弊,……
关键是在UserService中声明:
public _LogOnStatusModel GetLogonStatus() { User current = GetCurrentUser(); if (current == null)
关键是在OnAuthorization()中调用:
if (new UserService().GetLogonStatus() == null) //此处暂时借用GetLogonStatus()
在ArticleService.Publish()方法中:
Article article = new Article { Title = model.Title, Author = GetCurrentUser()
但是,这样又会出现GetCurrentUser()和当前方法使用不同DbContext的问题!
解决DbContext冲突的终极方案。(复习)
第一个要解决的问题:每一次用到的DbContext,存放在哪里?
ASP.NET为我们提供了一个Dictionary容器:HttpContext.Current.Items,它存放在当前Http请求的上下文环境中,即:如果Http请求结束,Items也消失;一个Http请求一个Items——这是一个绝佳的存放DbContext的容器。
为了便于所有Service获取DbContext实例,我们在BaseService中添加一个dbContext只读属性:
protected static SqlContext currentDbContext //static与否随意 { get {然后,用我们已经非常熟悉的方式处理:
const string dbContext = "DbContext"; //这里不用public的Const了,^_^ if (HttpContext.Current.Items[dbContext] == null) { HttpContext.Current.Items[dbContext] = new SqlContext(); } return (SqlContext)HttpContext.Current.Items[dbContext];
这样就能保证一个Http请求中,通过currentDbContext始终只能获得一个SqlContext实例。比如:
public ArticleService() { articleRepository = new ArticleRepository(currentDbContext);
public BaseService() { userRepository = new UserRepository(currentDbContext);
在实例化SqlContext时直接启动:
SqlContext context = new SqlContext(); context.Database.BeginTransaction(); HttpContext.Current.Items[dbContext] = context;注意这里需要引入EntityFramework组件!@想一想@:为什么?
同时在BaseService中我们封装以下几个静态方法(不要把currentDbContext暴露出去):
public static void Commit() { currentDbContext.SaveChanges(); DbContextTransaction transaction = currentDbContext.Database.CurrentTransaction; if (transaction != null) //可选 { transaction.Commit();注意SaveChanges()的调用,这样其他很多地方就可以省略其调用——除非是在持久化新new出来的entity的时候。
public static void Rollback() { DbContextTransaction transaction = currentDbContext.Database.CurrentTransaction; transaction.Rollback();
public static void Dispose() { currentDbContext.Dispose(); HttpContext.Current.Items.Remove(dbContext);
在有异常抛出的时候执行。所以我们利用HandleErrorAttribute:
public class YzHandleErrorAttribute : HandleErrorAttribute { public override void OnException(ExceptionContext filterContext) { try { BaseService.Rollback(); } finally //保证Dispose的执行 { BaseService.Dispose(); } base.OnException(filterContext); //不要忘了这句代码声明一个YzHandleErrorAttribute,在它的OnException()中回滚。然后在FilterConfig中:
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { //filters.Add(new HandleErrorAttribute()); filters.Add(new YzHandleErrorAttribute());
仍然是利用Filter。但是,是IActionFilter呢,还是IResultFilter。
从尽早释放DbContext的角度出发,我们肯定希望能在IActionFilter的OnActionExecuted()中就提交事务。
但是,MVC中的Filter是会影响ChildAction
要结合ChildAction的执行顺序明白Filter的执行顺序:
Index()如果一旦Action执行完成,就进行提交,一次请求会要求多次DbContext,产生多次提交!
所以我们选择在IResultFilter的OnResultExecuted()中提交事务。
为了避免多次提交的问题,我们利用MVC在FilterContext中,为我们提供IsChildAction属性:
public void OnResultExecuted(ResultExecutedContext filterContext) { if (!filterContext.IsChildAction) { try { BaseService.Commit();
这样就能保证一次请求只有一个commit。
最后,不要忘了对ContextPerRequest进行全局注册:
filters.Add(new ContextPerRequest());
if (filterContext.Exception == null)
因为Action中的异常不会被放在这(Result)里面。(演示)
使用分层架构,完成文章(或者建议/求助,只需要包含:标题、正文和作者)
注意:
多快好省!前端后端,线上线下,名师精讲
更多了解 加: