Code First开发系列实战之使用EF搭建小型博客平台

返回《8天掌握EF的Code First开发》总目录


本篇目录

本系列的源码本人已托管于Coding上:点击查看,想要注册Coding的可以点击该连接注册
先附上codeplex上EF的源码:entityframework.codeplex.com,此外,本人的实验环境是VS 2013 Update 5,windows 10,MSSQL Server 2008/2012。

前面我们已经用7篇博客将EF的Code First知识系统地介绍完毕了,接下来就应该检验一下真理了。通过使用EF为一个完整的应用创建数据访问层,我们会在实践中看到所有的理论知识,这个应用是一个简单的博客平台,并且使用了EF进行数据访问。

理解应用需求

首先理解一下这个博客平台的需求,我们只是开发一个简单的、可用作博客框架的应用,只有一些常见的功能,不可能像博客园那么强大。下面列一下这个博客框架具有的一些功能:

  • 作者可以创建博客分类
  • 作者可以管理(添加和更新)分类
  • 作者可以创建新博客
  • 作者可以管理(更新或删除)博客
  • 首页会显示所有博客的列表,列表内容包括博客标题,发布时期,博客的前200个字符以及一个查看更多链接
  • 当点击首页博客列表的标题时,会显示整篇博客
  • 用户可以对博客发表评论
  • 用户可以删除评论

我们会以类库的形式开发一个简单的数据访问层,该类库会有助于这些功能的实现。因为我们创建了数据访问的类库,客户端应用就可以使用该类库了。

从博客框架的角度讲,这些并不是完整的功能集,但是这些功能足以用来描述EF相关的概念了。我会创建一个简单的ASP.NET MVC应用,ASP.NET MVC相关的问题本系列不涉及,但是我会展示一些应用运行的截图以证明数据访问层是正常工作的。

数据库设计

在创建POCO实体类之前,先来看一下要实现之前提到的功能,需要的可视化数据库设计。

首先,需要满足用户注册,验证和授权机制的表,只需要创建下面的三张表即可:

  • Users:这张表用于保存所有用户的信息,用于用户登录和管理账户。
  • Roles:用于跟踪应用中的角色,在这个博客应用中,只有两个角色:Authors(作者)和Users(用户)。作者可以发布和管理博客。用户只能对博客发表评论。
  • UserRoles:用于创建Users和Roles间的多对多关系。

基本的验证和授权表有了,再看看要实现上述功能还需要什么表:

  • Categories:用于存储作者创建的所有博客分类。
  • Blogs:用于存储作者创建的所有博客。
  • Comments:用于存储用户对博客发表的评论。

综上,可视化数据库设计图如下:

图片

创建实体数据模型

现在开始一个个创建POCO实体类,完成实体类的创建之后,我们再来实现它们之间的关系,也就是在实体类中添加导航属性。

创建实体类

User实体

首先,咱们创建保存用户明细的实体User,该实体对需要验证的用户数据负责,代码如下:

public partial class User
{
    public int Id { get; set; }
    public string UserName { get; set; }
    public string Email { get; set; }
    public bool EmailConfirmed { get; set; }
    public string PasswordHash { get; set; }
    public string SecurityStamp { get; set; }
}

EF会为Id生成自动增长的属性,其他属性都是用来认证用户的属性。

这里先假设User模型使用哈希和加盐技术存储用户的密码,PasswordHash包含了用户提供密码的哈希值,SecurityStamp包含了盐值。如果使用其他任何的密码存储机制,可以相应更改User模型。

Role实体

创建Role类的主要目的是为了授权,比如,为了识别当前登录系统的用户是一个author还是一个普通的user。代码如下:

public partial class Role
{
    public int Id { get; set; }

    [Required]
    [StringLength(256)]
    public string Name { get; set; }
}

Category实体

Category代表博客的所属类别,代码如下:

public partial class Category
{
    public int Id { get; set; }

    [Required]
    [StringLength(200)]
    public string CategoryName { get; set; }
}

Blog实体

Blog实体用来存储博客数据。代码如下:

public partial class Blog
{
    public int Id { get; set; }

    [Required]
    [StringLength(500)]
    public string Title { get; set; }

    [Required]
    public string Body { get; set; }
    public DateTime CreationTime { get; set; }
    public DateTime UpdateTime { get; set; }
}

Comment实体

Comment实体表示用户对博客发表的评论。代码如下:

public partial class Comment
{
    public int Id { get; set; }
    public string Body { get; set; }
    public DateTime CreationTime { get; set; }
}

创建关系和导航属性

目前,我们只创建了独立的实体,这些实体之间还没有任何关系,现在就来实现这些实体间的关系。在使用代码实现之前,先来看看所有需要的关系:

  • User-Role:因为任何每个用户可以有多个角色,而每个角色也可以有多个用户,所以User和Role之间是多对多关系
  • Category-Blog:因为每个类别可以有多篇博客,而这里每篇博客属于一个类别,所以它们的关系是一对多关系
  • User-Blog:因为每个用户可以有多篇博客,而每篇博客属于一个用户,所以它们是一对多关系
  • Blog-Comment:因为每篇博客可以有多条评论,而每条评论属于一篇博客,所有它们是一对多关系
  • User-Comment:因为每个用户可以有多条评论,而每个评论属于一个用户,所以它们是一对多关系

下面就在实体类中实现这些关系(这里只列出新加的代码)。

User实体

从上面列出的关系可以看出,User实体需要实现以下关系:

  • 因为每个用户有多个角色,所以需要一个Roles集合属性作为导航属性
  • 因为每个用户有多篇博客,所以需要一个Blogs集合属性作为导航属性
  • 因为每个用户有条评论,所以需要一个Comments集合属性作为导航属性
public partial class User
{
    public User()
    {
        Blogs=new HashSet<Blog>();
        Comments=new HashSet<Comment>();
        Roles=new HashSet<Role>();
    }
    //省略部分属性
    public virtual ICollection<Blog> Blogs { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
    public virtual ICollection<Role> Roles { get; set; }
}

Role实体

因为每个角色有多个用户,所以需要添加一个Users集合属性作为导航属性

public partial class Role
{
    public Role()
    {
        Users=new HashSet<User>();
    }
    //省略部分属性
    public virtual ICollection<User> Users { get; set; }
}

Category实体

因为每个类别包含多篇博客,所以Category需要添加一个Blogs集合属性作为导航属性

public partial class Category
{
    public Category()
    {
        Blogs=new HashSet<Blog>();
    }

    //省略部分属性

    public virtual ICollection<Blog> Blogs { get; set; }
}

Blog实体

Blog需要实现的关系如下:

  • 因为每个Blog属于一个Category实体,所以需要添加一个属性实现和Category实体的外键关系,以及到Category实体的导航属性
  • 因为每个Blog属于一个用户实体,所以需要添加一个属性实现和User实体的外键关系,以及一个到User实体的导航属性
  • 因为每个Blog实体包含多条评论,所以需要添加Comments集合属性作为导航属性

代码如下:

public partial class Blog
{
    public Blog()
    {
        Comments=new HashSet<Comment>();
    }

    //省略部分属性
    public int CategoryId { get; set; }
    public int AuthorId { get; set; }

    public virtual Category Category { get; set; }
    public virtual User User { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

上面的代码中,CategoryId属性用于创建和Category实体的外键关系,AuthorId用于创建和User实体的外键关系。

Comment实体

  • 因为每条评论属于一篇博客,所以需要添加一个属性实现和Blog实体的外键关系,以及到Blog实体的导航属性
  • 因为每条评论属于一个用户,所以需要添加一个属性实现和User实体的外键关系,以及到User实体的导航属性

代码如下:

public partial class Comment
{
    //省略部分属性
    public int? BlogId { get; set; }
    public int? PosterId { get; set; }
    public virtual Blog Blog { get; set; }
    public virtual User User { get; set; }
}

BolgId用于创建和Blog实体之间的外键关系,PosterId用于创建和User实体之间的外键关系。

实现DbContext类

至此,所有的实体类已经创建好了,现在是时候创建DbContext类了。DbContext类包含了我们创建的实体的DbSet对象,我们需要重写OnModelCreating方法来指定实体间下面的关系:

  • Category和Blog之间的一对多关系
  • Blog和Comment之间的一对多关系
  • User和Blog之间的一对多关系
  • User和Comment之间的一对多关系
  • User和Role之间的多对多关系

代码如下:

public class BlogAppContext:DbContext
{
    public BlogAppContext() : base("name=BlogAppConn")
    {
    }

    public virtual DbSet<Blog> Blogs { get; set; }
    public virtual DbSet<Category> Categories { get; set; }
    public virtual DbSet<Comment> Comments { get; set; }
    public virtual DbSet<Role> Roles { get; set; }
    public virtual DbSet<User> Users { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //Category和Blog之间的一对多关系
        modelBuilder.Entity<Category>()
            .HasMany(c=>c.Blogs)
            .WithRequired(b=>b.Category)
            .HasForeignKey(b=>b.CategoryId)
            .WillCascadeOnDelete(false);

        //Blog和Comment之间的一对多关系
        modelBuilder.Entity<Blog>()
            .HasMany(b=>b.Comments)
            .WithRequired(c=>c.Blog)
            .HasForeignKey(c=>c.BlogId)
            .WillCascadeOnDelete(false);

        //User和Blog之间的一对多关系
        modelBuilder.Entity<User>()
            .HasMany(u=>u.Blogs)
            .WithRequired(b=>b.User)
            .HasForeignKey(b=>b.AuthorId)
            .WillCascadeOnDelete(false);

        //User和Comment之间的一对多关系
        modelBuilder.Entity<User>()
            .HasMany(u => u.Comments)
            .WithOptional(c => c.User)
            .HasForeignKey(c => c.PosterId);

        //User和Role之间的多对多关系
        modelBuilder.Entity<User>()
            .HasMany(u => u.Roles)
            .WithMany(r => r.Users)
            .Map(act => act.ToTable("UserRoles")
            .MapLeftKey("UserId")
            .MapRightKey("RoleId"));

        base.OnModelCreating(modelBuilder);
    }
}

上面,我们创建了DbContext类——BlogAppContext。构造函数使用了配置文件中名为BlogAppConn的连接字符串,然后重写了 OnModelCreating方法来实现实体间的多种关系。注意最后一个多对多关系的配置,这种配置会告诉EF生成一个连接表。

执行数据访问

现在需要实现应用程序的数据访问层了,需要做的第一件事就是创建一个类库,然后将所有的实体类和上下文类拷贝到类库中,截图如下:

图片

要实现可扩展的数据访问层,我们要使用仓储和工作单元模式。

理解仓储模式

当我们使用不同的数据模型和领域模型时,仓储模式特别有用。仓储可以充当数据模型和领域模型之间的中介。在内部,仓储以数据模型的形式和数据库交互,然后给数据访问层之上的应用层返回领域模型。

在我们这个例子中,因为使用了数据模型作为领域模型,因此,也会返回相同的模型。如果想要使用不同的数据模型和领域模型,那么需要将数据模型的值映射到领域模型或使用任何映射库执行映射。

现在定义仓储接口IRepository如下:

public interface IRepository<T> where T:class
{
    IEnumerable<T> GetAllList(Expression<Func<T,bool>> predicate=null);
    T Get(Expression<Func<T, bool>> predicate);
    void Insert(T entity);
    void Delete(T entity);
    void Update(T entity);
    long Count();
}

上面的几个方法都是常见的CRUD操作,就不解释了,下面看看如何为实体实现具体的仓储类:
以下是Category类的仓储实现:

public class CategoryRepository:IRepository<Category>
{
    private BlogAppContext _context = null;

    public CategoryRepository(BlogAppContext context)
    {
        _context = context;
    }

    public IEnumerable<Category> GetAllList(System.Linq.Expressions.Expression<Func<Category, bool>> predicate = null)
    {
        if (predicate==null)
        {
            return _context.Categories.ToList();
        }
        return _context.Categories.Where(predicate).ToList();
    }

    public Category Get(System.Linq.Expressions.Expression<Func<Category, bool>> predicate)
    {
        return _context.Categories.SingleOrDefault(predicate);
    }

    public void Insert(Category entity)
    {
        _context.Categories.Add(entity);
    }

    public void Delete(Category entity)
    {
        _context.Categories.Remove(entity);
    }

    public void Update(Category entity)
    {
        _context.Categories.Attach(entity);
        _context.Entry(entity).State=EntityState.Modified;
    }

    public long Count()
    {
       return _context.Categories.LongCount();
    }
}

在上面的代码中,我们使用了BlogAppContext类执行了Category实体上的所有CRUD操作,并实现了IRepository接口,同样的道理,我们可以实现其他的仓储类如 BlogRepository和CommentRepository。然而,所有的仓储如果真的一个个写完的话, 除了使用了上下文各自的属性外,其他的所有代码都是相同的。因此,我们不用为每个实体创建一个单独的仓储类,而是采用对所有实体类都有效的泛型仓储。

泛型仓储类代码如下:

public class Repository<T>:IRepository<T> where T:class
{
    private readonly BlogAppContext _context = null;
    private readonly DbSet<T> _dbSet; 
    public Repository(BlogAppContext context)
    {
        _context = context;
        _dbSet = _context.Set<T>();
    }

    public IEnumerable<T> GetAllList(System.Linq.Expressions.Expression<Func<T, bool>> predicate = null)
    {
        if (predicate==null)
        {
            return _dbSet.AsEnumerable();
        }
        return _dbSet.Where(predicate).ToList();
    }

    public T Get(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
    {
        return _dbSet.SingleOrDefault(predicate);
    }

    public void Insert(T entity)
    {
        _dbSet.Add(entity);
    }

    public void Delete(T entity)
    {
        _dbSet.Remove(entity);
    }

    public void Update(T entity)
    {
        _dbSet.Attach(entity);
        _context.Entry(entity).State=EntityState.Modified;
    }

    public long Count()
    {
        return _dbSet.LongCount();
    }
}

这个泛型仓储类很有用,因为一个类就可以对所有的实体类执行CRUD操作。

实现泛型仓储是很好的做法,因为一个应用可能包含上百个实体,如果每个实体都编写仓储的话,那么要写太多乏味的代码。

理解工作单元

我们已经知道,DbContext默认支持事务,当实例化一个新的DbContext对象时,就会创建一个新的事务,当调用SaveChanges方法时,事务会提交。问题是,如果我们使用相同的DbContext对象把多个代码模块的操作放到一个单独的事务中,该怎么办呢?答案就是工作单元(Unit of Work)。

工作单元本质是一个类,它可以在一个事务中跟踪所有的操作,然后将所有的操作作为原子单元执行。看一下仓储类,可以看到DbContext对象是从外面传给它们的。此外,所有的仓储类都没有调用SaveChanges方法,原因在于,我们在创建工作单元时会将DbContext对象传给每个仓储。当想保存修改时,就可以在工作单元上调用SaveChanges方法,也就在DbContext类上调用了SaveChanges方法。这样就会使得涉及多个仓储的所有操作成为单个事务的一部分。

这里定义我们的工作单元类如下:

public class UnitOfWork:IDisposable
{
    private readonly BlogAppContext _context = null;
    private Repository<Blog> _blogRepository = null;
    private Repository<Category> _categoryRepository;
    private Repository<Comment> _commentRepository = null;
    private Repository<Role> _roleRepository = null;
    private Repository<User> _userRepository = null;

    public UnitOfWork(Repository<Blog> blogRepository, Repository<Category> categoryRepository, Repository<Comment> commentRepository, Repository<Role> roleRepository, Repository<User> userRepository)
    {
        _blogRepository = blogRepository;
        _categoryRepository = categoryRepository;
        _commentRepository = commentRepository;
        _roleRepository = roleRepository;
        _userRepository = userRepository;
        _context=new BlogAppContext();
    }

    public Repository<Blog> BlogRepository
    {
        get { return _blogRepository ?? (_blogRepository = new Repository<Blog>(_context)); }
    }

    public Repository<Category> CategoryRepository
    {
        get { return _categoryRepository ?? (_categoryRepository = new Repository<Category>(_context)); }
    }

    public Repository<Comment> CommentRepository
    {
        get { return _commentRepository ?? (_commentRepository = new Repository<Comment>(_context)); }
    }

    public Repository<Role> RoleRepository
    {
        get { return _roleRepository ?? (_roleRepository = new Repository<Role>(_context)); }
    }

    public Repository<User> UserRepository
    {
        get { return _userRepository ?? (_userRepository = new Repository<User>(_context)); }
    }

    public void SaveChanges()
    {
        _context.SaveChanges();
    }


    public void Dispose()
    {
        throw new NotImplementedException();
    }
}

上面就是我们定义的工作单元类,它会创建实际的仓储类,然后将DbContext的相同实例传给这些仓储。当处理完所有的操作时,然后调用这个类的SaveChanges方法,也就是调用了DbContext类的SaveChanges方法,从而将数据保存到数据库中。

现在,事务和这个UnitOfWork类联系起来了,每个工作单元都有自己的事务,每个事务都可以使用相应的仓储类处理多个实体,当调用了UnitOfWork类的SaveChanges方法时,事务就会提交。

UnitOfWork类也可以以泛型的方式实现,这样就不需要为每个仓储类添加一个属性了。


这里进行数据库迁移,生成特定的数据库,至于连接字符串、数据库迁移等的配置,这里不再赘述,请查看该系列前面的章节。生成数据库后,然后插入测试数据进行测试。

给出我这里的测试数据:

/*Categories表数据*/
INSERT dbo.Categories( CategoryName )VALUES  ( N'ABP理论基础')
INSERT dbo.Categories( CategoryName )VALUES  ( N'ABP理论高级')
INSERT dbo.Categories( CategoryName )VALUES  ( N'ABP实践基础')
INSERT dbo.Categories( CategoryName )VALUES  ( N'ABP实践高级')
INSERT dbo.Categories( CategoryName )VALUES  ( N'ASP.NET')
INSERT dbo.Categories( CategoryName )VALUES  ( N'EF')
INSERT dbo.Categories( CategoryName )VALUES  ( N'JavaScript')
INSERT dbo.Categories( CategoryName )VALUES  ( N'TypeScript')

管理分类

数据访问层的类库已经创建好了,现在看看如何对Category实体执行各种各样的操作。

显示分类列表

要列出所有的分类,只需要创建UnitOfWork对象,然后使用CategoryRepository检索数据库中的所有分类即可:

public ActionResult Index()
{
    using (var uow=new UnitOfWork())
    {
         var categories = uow.CategoryRepository.GetAll().ToList();
         return View(categories);
    }
}

效果截图如下:

图片

虽然我们创建的是ASP.NET MVC项目,但是我们的重点还是EF Code First的实践,因此,很多相关的MVC代码大家自己可以查看源码,只需要关注数据的读取和写入即可。

添加分类

要添加一个新的分类,需要创建一个Category模型,然后调用CategoryRepository的Insert方法,首先来看一下视图:

图片

控制器Action代码:

[HttpPost]
public ActionResult Create(Category model)
{
    try
    {
        using (var uow = new UnitOfWork())
        {
            uow.CategoryRepository.Insert(model);
            uow.SaveChanges();
        }

        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}

更新分类

更新分类时,首先要从数据库中检索到该分类,然后更新必填的属性,再调用CategoryRepository的Update方法。看一下更新时的视图:

图片

看一下打开编辑页面的逻辑控制Action代码:

public ActionResult Edit(int id)
{
    using (var uow=new UnitOfWork())
    {
        var category = uow.CategoryRepository.Get(c => c.Id == id);
        return View(category);
    }
}

上面的Edit方法,使用了id值来检索分类,然后再将检索到的模型传递到视图上,这样就可以更新该模型了。用户在该模型的原始值的基础上进行修改,点击Save按钮之后,该模型又会传给控制器,下面看一下如何将更新后的模型更新到数据库中:

[HttpPost]
public ActionResult Edit(Category model)
{
    try
    {
        using (var uow=new UnitOfWork())
        {
            uow.CategoryRepository.Update(model);
            uow.SaveChanges();
        }

        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}

删除分类

要删除分类,首先要确保数据库中存在该分类,所以,先要检索到该分类,然后调用CategoryRepository的Delete方法。下面看一下控制器的Action方法如何通过分类的Id删除分类:

public ActionResult Delete(int id)
{
    try
    {
        using (var uow=new UnitOfWork())
        {
            var category = uow.CategoryRepository.Get(c => c.Id == id);
            uow.CategoryRepository.Delete(category);
            uow.SaveChanges();
        }

        return RedirectToAction("Index");
    }
    catch(Exception ex)
    {
        throw ex;
    }
}


删除时,我们先要通过Id检索到指定的分类,然后才调用仓储的Delete方法进行删除。

管理博客

和之前的分类管理功能类似,很多话就不再重复说了,直接贴代码!

添加新的博客

视图效果:

图片

这个界面,写过博客的人应该很熟悉了,对应的各个字段就不一一解释了。当点击Create按钮时,会创建一个Blog模型,然后该模型传给控制器的action方法,action方法使用UnitOfWork和Repository类将该Blog模型对象添加到数据库:

[HttpPost]
public ActionResult Create(Blog blog)
{
    try
    {
        using (var uow = new UnitOfWork())
        {
            blog.CreationTime = DateTime.Now;
            blog.UpdateTime = DateTime.Now;
            //blog.AuthorId = uow.UserRepository.Get(u => u.UserName == User.Identity.Name).Id;//以后加入Identity技术时使用
            blog.AuthorId = 1;
            uow.SaveChanges();
        }

        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}

为了简化,这里使用了textarea标签作为博客正文的输入,作为支持Markdown语法的网站来说,textarea也足够用了!但是对于不熟悉Markdown语法的用户来说,最好使用富文本编辑器,如CKEditor等等。

更新博客

就拿博客园来说,比如发表了一篇博客,如果发现有错别字或者哪里说的不对等等,这些情况下,我们都可以对发布的博客进行更新。视图效果:

图片

更新展示页面对应的Action方法如下:

public ActionResult Edit(int id)
{
    using (var uow = new UnitOfWork())
    {
        var categories = uow.CategoryRepository.GetAll().ToList();
        ViewData["categories"] = new SelectList(categories, "Id", "CategoryName");
        var blog = uow.BlogRepository.Get(b => b.Id == id);
        return View(blog);
    }
}

点击Save按钮后,更新视图中的模型会传回给Action方法:

[HttpPost]
public ActionResult Edit(Blog blog)
{
    try
    {
        using (var uow=new UnitOfWork())
        {
            blog.UpdateTime=DateTime.Now;
            uow.BlogRepository.Update(blog);
            uow.SaveChanges();
        }
        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}

以上展示了更新一个Blog实体的Action方法,视图代码等其他没展示的部分请在源码中查看。有些Action方法后期也会有所改变,也请以最终的源代码为准。

删除博客

public ActionResult Delete(int id)
{
    try
    {
        using (var uow=new UnitOfWork())
        {
            var blog = uow.BlogRepository.Get(b => b.Id == id);
            uow.BlogRepository.Delete(blog);
            uow.SaveChanges();
        }

        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}


博客的删除和更新操作应该只有该博客的作者可以进行操作。因此,只有登录的用户是该博客的作者时,操作链接才应该显示出来。因此,应该在视图页面中添加下面的代码:

if(User.Identity.Name == Model.User.UserName)
{
  //操作链接
}

在主页显示博客列表

这个稍微有点棘手,因为要实现以下功能:

  • 在主页上只显示前10篇博客
  • 每篇博客可以看到标题和前200个字符
  • 每篇博客应该显示评论数量
  • 每篇博客应该显示推送日期和更新日期
  • 每篇博客应该显示作者名字

要实现这些功能,我们要定义一个视图模型ViewModel,然后使用Linq To Entities的投影从数据库中检索出所有信息。ViewModel定义如下:

// GET: /Home/
public ActionResult Index(int page=0)
{
    using (var uow=new UnitOfWork())
    {
        var blogs = uow.BlogRepository.GetAll()
            .Include(b=>b.User).Include(b=>b.Comments)
            .OrderByDescending(b => b.CreationTime)
            .Skip(page*10)
            .Take(10);

        var blogSummaryList = blogs.Select(b => new BlogViewModel
        {
            AuthorName = b.User.UserName,
            CommentsCount = b.Comments.Count,
            CreationTime = b.CreationTime,
            Id = b.Id,
            Overview = b.Body.Length>200?b.Body.Substring(0,200):b.Body,
            Title = b.Title,
            UpdateTime = b.UpdateTime
        }).ToList();

        return View(blogSummaryList);
    }
}

上面的代码对于EF新手来说可能有些难度,但是如果你学习了之前的7篇博客,恐怕这些代码也是很简单的!
稍作解释,blogs是Blog实体的分页结果集,因为ViewModel用到了AuthorName和CommentsCount,所以必须通过预先加载的方式加载Users表和Comments表。Skip和Take是两个重要的分页函数,但在分页之前,记得先要进行排序,否则会编译出错。blogSummaryList是对blogs进行投影,将实体集合转换成对应的视图模型集合。

最后看一下效果图:

图片

展示单条博客

当用户点击阅读全文时,应该展示以下信息:

  • 博客标题、正文、博主用户名
  • 发布日期和更新日期
  • 该博客的所有评论
  • 发表评论的超链接

下面看一下实现这个目的的Action方法:

public ActionResult Details(int id)
{
    using (var uow = new UnitOfWork())
    {
        var blog = uow.BlogRepository.Get(b => b.Id == id);
        var blogDetailViewModel=new BlogDetailViewModel
        {
            AuthorName = blog.User.UserName,
            Body = blog.Body,
            CreationTime = blog.CreationTime,
            Id = blog.Id,
            Title = blog.Title,
            UpdateTime = blog.UpdateTime,
            CategoryName = blog.Category.CategoryName
        };
        return View(blogDetailViewModel);
    }
}

其中,BlogDetailViewModel是一个视图模型类,它的定义如下:

public class BlogDetailViewModel
{
    public int Id { get; set; }
    [DisplayName("标题")]
    public string Title { get; set; }
    [DisplayName("正文")]
    public string Body { get; set; }
    [DisplayName("博主")]
    public string AuthorName { get; set; }
    [DisplayName("所属分类")]
    public string CategoryName { get; set; }
    [DisplayName("发布日期")]
    public DateTime CreationTime { get; set; }
    [DisplayName("更新日期")]
    public DateTime UpdateTime { get; set; }

}

最终效果图如下:

图片

至于发表评论功能,在下面的管理评论一节完成。

管理评论

添加评论

点击“Comment”超链接会显示一个评论区域和一个发表按钮,如下:

图片

填写完评论后,点击发表评论按钮,数据提交到下面的Action方法:

[HttpPost]
public ContentResult Create(Comment comment)
{
    try
    {
        using (var uow=new UnitOfWork())
        {
            comment.PosterId = 2;//这里直接写入Id,本该写入当前评论者的Id
            comment.CreationTime=DateTime.Now;
            uow.CommentRepository.Insert(comment);
            uow.SaveChanges();
        }
        return Content(JsonConvert.SerializeObject(comment));
    }
    catch(Exception ex)
    {
        throw ex;
    }
}

最终效果如下:

图片

显示评论列表

因为我们已经在Blog实体中有了Comments导航属性,所以,要在Blog明细的页面上显示评论列表,只需要循环该集合即可展示所有评论。

修改展示博客明细页面所对应的Action方法的代码如下:

public ActionResult Details(int id)
{
    using (var uow = new UnitOfWork())
    {
        var blog = uow.BlogRepository.Get(b => b.Id == id);
        var blogDetailViewModel=new BlogDetailViewModel
        {
            AuthorName = blog.User.UserName,
            Body = blog.Body,
            CreationTime = blog.CreationTime,
            Id = blog.Id,
            Title = blog.Title,
            UpdateTime = blog.UpdateTime,
            CategoryName = blog.Category.CategoryName
        };
        List<CommentViewModel> commentList= blog.Comments.Select(comment => new CommentViewModel
        {
            PosterName = comment.User.UserName, Message = comment.Body, CreationTime = comment.CreationTime
        }).ToList();

        ViewData["Comments"] = commentList;
        return View(blogDetailViewModel);
    }
}

视图上对应的代码:

<h3>评论列表</h3>

@{
    if (ViewData["Comments"] != null)
    {
        var comments = ViewData["Comments"] as List<CommentViewModel>;
        <div id="CommentContainer">
            @foreach (var comment in comments)
            {

                <hr />
                <span><strong>@comment.PosterName</strong></span> <span>评论于 @comment.CreationTime </span><br />
                <p>@comment.Message</p>

            }
        </div>
    }

}

删除评论

public ContentResult Delete(int id)
{
    try
    {
        using (var uow=new UnitOfWork())
        {
            var comment= uow.CommentRepository.Get(c => c.Id == id);
            uow.CommentRepository.Delete(comment);
            uow.SaveChanges();
        }

        return Content("ok");
    }
    catch (Exception ex)
    {
        return Content(ex.Message);
    }
}

Action方法很简单,前端代码请查看源代码。

总结

这一篇,我们基本上使用了之前7篇博文的所有知识点完成了一个小型博客平台半成品(User,Role等没有实现),至此,我们也就有能力使用EF的Code First方式开发以数据为中心的应用。

与现实中的应用相比,这个简单的应用知识冰山一角。现实中的应用远比这个应用规模更大,且更复杂。但是本篇的初衷是让开发者熟悉如何使用EF的Code First方式着手应用开发,以及如何开发端到端的功能。

到这里,这个《8天掌握EF的Code First开发》就结束了。当然,EF是一个说不尽的话题,老话说,“知易行难”,因此关于EF,我们还有很多东西要学,这需要我们每天不停地操练积累经验。同时,还有很多优秀的第三方ORM工具,比如Dapper、NHibernate等等,但我相信,好好掌握了一个ORM,其他的都不在话下。学习完了这个系列,我觉得你应该完全准备好在任何应用中使用EF了!大家加油吧!

posted @ 2016-06-22 17:00 tkbSimplest 阅读(...) 评论(...) 编辑 收藏