学编程,来源栈;先学习,再交钱
当前系列: ASP.NET 修改讲义
复习:三层架构,已学会EF

分层架构实现

实际开发中,实现的方式方法五花八门:

  • 有非常硬核的,进行物理分隔:UI层一台服务器,BLL层一台服务器……
  • 有非常随意的,在Project中一层建一个文件夹就Over了
我们选择利用项目来进行分层:

演示:注册/登录/NeedLogOn的实现

  • 新建solution folder和项目
  • 同步虚拟的solution folder和分层文件夹
  • 添加项目间引用:Alt+Enter智能提示 & Add References

注意:project无法双向引用,无法穿透引用

#理解#:.sln和.csproj的作用复习

BLL

DDD/面向对象的核心。包含:

Entities

独立到一个project,只是被其他项目引用,不会引用其他任何项目。

采用充血模式,完成大量业务逻辑

public void Register()
{
    InviteCode = new Random().Next(9999).ToString();   //邀请码

    if (InvitedBy != null)    //积分奖励
    {
        InvitedBy.Credit += 10;
    }//else nothing
}

Repositories

使用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文件
  1. IIS只会直接运行ASP.NET项目,所有的第三方组件只会从ASP.NET项目的bin文件夹中获取,所以EF组件要(通过nuget)添加到ASP.NET项目引用中
  2. 17bang”对应的连接字符串(connectionStrings),以及EF配置(configSections/entityFramework,见App.config)都要同步到的web.config中,配置信息只能从这里读取!

演示:没有这样做会报的错误。

SRV

ProdService

同时引用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);
}

ViewModels

贫血模式的DTO:

  • 在UI层和SRV之间作为数据容器使用
  • 在MVC中作为Model使用
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命名风格)的异同。

UI

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();

演示:能够将用户名和密码存入数据库。

#理解#:各个项目之间的调用关系。



重构Repository

继承和泛型

首先entity是可以抽象的:所有的entity都是可以用Id标识的,所以我们可以有一个BaseEntity类:

public class BaseEntity
{
    public int Id { get; set; }
public class User : BaseEntity
public class Article : BaseEntity
于是,repository也可以相应的抽象,比如,所有的repository都可以:
  1. 根据id获取某个entity
  2. 保存某个entity,返回它的id
  3. 删除某个entity
  4. ……

这首先需要一个泛型基类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实现呢?必须依赖

DbContext

在基类中声明并实例化:

private SqlContext context;
public BaseRepository()
{
    context = new SqlContext();


于是,在其子类中可以直接利用base:
public UserRepository(SqlContext context): base(context)

这样隐藏无参构造函数,使得开发人员初始化repository时不得不传入DbContext参数,避免遗忘疏忽。

DbSet<T>

直接复制子类的内容既不合理(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>();

#小技巧#:

  • SqlContext context;设为private,避免子类利用context获取其他DbSet,比如:
        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>();
  • DbSet<T> dbSet设为protected,子类可以直接使用,比如:
    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()……

但是,注意,注意,注意(重要的事说三遍)!要“为了重用”,仅仅是因为他们有相同的属性,就把他们“归为一类”,比如:复习:桌子和动物都有腿……

  • 有Name的归为一类,于是可以GetByName():User和Keyword都可以有Name,他们能是一类么?
  • 有User的归为一类,于是可以GetByUser():Article和Message都有User,他们能是一类么?

PS:所以,我一直强调,尽量不要用类名做属性名,应该是Article.Author和Message.Sender,而不是Article.User和Message.User

#再次体会#:为什么所有的entity都是BaseEntity的基类?选择:

  1. 因为他们都有一个Id属性
  2. 因为他们都可以通过Id标识

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,




Context冲突:文章发布

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)到数据库中。

解决的办法就是:

共用DbContext

但怎么让这个两个Repository共用一个DbContext呢?

让DbContext从外部传入:

public ArticleRepository(SqlContext context)
{
    this.context = context;
public ArticleService()
{
    SqlContext context = new SqlContext();
    articleRepository = new ArticleRepository(context);
    userRepository = new UserRepository(context);




基类控制

在Repository基类中设置一个有参构造函数:
private SqlContext context;
public BaseRepository(SqlContext context)
{
    this.context = context;


这样子类就必须:

public ArticleRepository(SqlContext context) 
    : base(context)



CurrentUser

需求

项目中太多地方(发布/点赞/评论/消息……)都需要获取到“当前用户”,所以需要一个可以快速获取它的封装。

问题是这个封装放在哪里?

UI层

因为当前用户需要从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);
//...

显得非常累赘!

SRV层

当前用户信息不需要由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

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];

GLB

cookie中要使用的“键”,应该用Const,但现在不仅仅是UI,SRV层以及其他层都可能要使用它,怎么办?

引入一个GLB项目文件夹,里面添加一个Global的类库项目,放这里面:

以后“全局性的”代码都可以放这里。

异常抛不抛?

然后

  1. 从cookie中取id,
    if (cookie == null)
    {
        return null;
    }
    string userId = cookie.Values[Cookie.UserId];
  2. 根据id通过userRepository取user对象,
    User user =  userRepository.Find(Convert.ToInt32(userId));
  3. 比对cookie中的密码,
    if (user.Password != cookie.Values[Cookie.Password])

都类似于之前LogonStatus的代码

但是,中途如果从了“意外”,比如:

  • cookie中没有id,或者id不是有效数字,或者没有id对应的User(这种“有可能”出现的场景不能抛异常,可以删除掉该cookie)
  • 从cookie中获取的pwd和数据库中存放的pwd不合,

怎么办?抛异常:可以。但是注意抛了异常就不会处理cookie,哪怕这样:

//把处理(删除)cookie的代码写在抛异常之前
HttpContext.Current.Response.Cookies.Add(new HttpCookie(Cookie.User) {  });
throw new Exception("");   

因为抛异常之后,这里的response无法正常到达客户端。

#体会#:从严格意义上讲,让SRV层牵涉cookie/System.Web,破坏了UI/SRV层之间的职责划分。但是,综合权衡利弊,……

使用GetCurrentUser()

LogonStatus

关键是在UserService中声明:    

public _LogOnStatusModel GetLogonStatus()
{
    User current = GetCurrentUser();
    if (current == null)

NeedLogOn

关键是在OnAuthorization()中调用:

if (new UserService().GetLogonStatus() == null)   //此处暂时借用GetLogonStatus()

文章发布

在ArticleService.Publish()方法中:

Article article = new Article
{
    Title = model.Title,
    Author = GetCurrentUser()

但是,这样又会出现GetCurrentUser()和当前方法使用不同DbContext的问题!


ContextPerRequest

解决DbContext冲突的终极方案。复习

HttpContext容器

第一个要解决的问题:每一次用到的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实例。比如:

  • ArticleService:
    public ArticleService()
    {
        articleRepository = new ArticleRepository(currentDbContext);
  • SharedService:
    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()
   -- ActionExecuted()
   -- ResultExecuting()
   @ Index.cshtml       
     @Html.Action("_LogonStatus")
        --ActionExecuting()
                ChildAction()
        --ActionExecuted()
        --ResultExecuting()
        --ResultExecuted()
   -- ResultExecuted()

如果一旦Action执行完成,就进行提交,一次请求会要求多次DbContext,产生多次提交!

所以我们选择在IResultFilter的OnResultExecuted()中提交事务。

IsChildAction

为了避免多次提交的问题,我们利用MVC在FilterContext中,为我们提供IsChildAction属性:

public void OnResultExecuted(ResultExecutedContext filterContext)
{
    if (!filterContext.IsChildAction)
    {
        try
        {
            BaseService.Commit();

这样就能保证一次请求只有一个commit。

最后,不要忘了对ContextPerRequest进行全局注册:

filters.Add(new ContextPerRequest());

其他

不要使用ResultExecutedContext的filterContext的Exception属性来确定是否应该回滚:
if (filterContext.Exception == null)

因为Action中的异常不会被放在这(Result)里面。(演示)


作业

使用分层架构,完成文章(或者建议/求助,只需要包含:标题、正文和作者)

  1. 发布:/Article/New
  2. 单页显示:/Article/{id}
  3. 修改:/Article/Edit/{id}
    1. 只有当前用户就是作者的时候,才在单页显示该修改链接
    2. 防御式编程,在Service中检查:当前用户是不是文章作者。@想一想@:如果不是的话,如何处理?

注意:

  • 1和3页面,只有登录用户才能访问
  • 可以糅合之前的安全措施:过滤和防CSFR
学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ASP.NET 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码