第28章 实战项目一:博客系统
章节摘要
本章通过构建一个完整的博客系统,综合运用前面所学的 ASP.NET Core、Entity Framework Core、身份认证、授权等技术,实现用户注册登录、文章发布、评论互动、分类标签、搜索等功能,掌握真实项目的开发流程和最佳实践。
本章目录
- 28.1 项目需求与架构设计
- 28.2 数据访问层实现
- 28.3 业务逻辑层实现
- 28.4 API 控制器层实现
- 28.5 Program.cs 配置与启动
- 28.6 单元测试与集成测试
- 28.7 部署配置与常见误区
28.1 项目需求与架构设计
项目背景
构建一个现代化的个人博客系统,支持多用户协作、文章管理、评论互动、分类标签等功能。
目标用户:
- 博主:发布和管理文章
- 读者:浏览文章、发表评论、搜索内容
- 管理员:管理用户、审核内容
功能需求
核心功能:
1. 用户系统
- 用户注册与登录(本地账号 + OAuth)
- 用户角色(Admin、Author、Reader)
- 用户资料管理
- 密码修改与重置
2. 文章系统
- 发布文章(支持 Markdown)
- 编辑和删除文章
- 文章草稿保存
- 文章分类和标签
- 文章搜索
- 文章分页浏览
- 文章阅读统计
3. 评论系统
- 发表评论
- 评论回复(嵌套评论)
- 评论点赞
- 评论审核(管理员)
4. 分类与标签
- 创建和管理分类
- 创建和管理标签
- 按分类/标签筛选文章
5. 管理后台
- 用户管理
- 文章管理
- 评论管理
- 系统统计
非功能需求:
- 响应式设计(支持移动端)
- SEO 优化
- 性能优化(缓存、分页)
- 安全性(XSS、CSRF、SQL注入防护)
- 代码可维护性
技术选型
后端技术栈:
- ASP.NET Core 8.0 Web API
- Entity Framework Core 8.0
- SQL Server / PostgreSQL
- ASP.NET Core Identity(用户认证)
- JWT Bearer 认证
- AutoMapper(对象映射)
- FluentValidation(数据验证)
- Serilog(日志记录)
前端技术栈(可选):
- Blazor / React / Vue.js
- Bootstrap / Tailwind CSS
- Markdown 编辑器
项目架构
采用分层架构(Clean Architecture):
BlogSystem/
├── BlogSystem.Api/ # API 层(控制器、中间件)
│ ├── Controllers/
│ │ ├── AuthController.cs
│ │ ├── PostsController.cs
│ │ ├── CommentsController.cs
│ │ ├── CategoriesController.cs
│ │ └── TagsController.cs
│ ├── Middlewares/
│ └── Program.cs
├── BlogSystem.Core/ # 核心层(领域实体、接口)
│ ├── Entities/
│ │ ├── User.cs
│ │ ├── Post.cs
│ │ ├── Comment.cs
│ │ ├── Category.cs
│ │ └── Tag.cs
│ ├── Interfaces/
│ │ ├── IRepository.cs
│ │ ├── IPostService.cs
│ │ └── ICommentService.cs
│ └── DTOs/
├── BlogSystem.Infrastructure/ # 基础设施层(数据访问、外部服务)
│ ├── Data/
│ │ ├── BlogDbContext.cs
│ │ └── Repositories/
│ ├── Services/
│ └── Extensions/
└── BlogSystem.Tests/ # 测试层
├── UnitTests/
└── IntegrationTests/
架构说明:
- API 层:处理 HTTP 请求,调用服务层
- Core 层:领域模型、业务接口、DTO
- Infrastructure 层:数据访问、第三方服务
- Tests 层:单元测试和集成测试
依赖关系:
Api → Core ← Infrastructure
↑
Tests
数据库设计
ER 图概览:
┌─────────────┐ ┌─────────────┐
│ Users │ │ Posts │
├─────────────┤ ├─────────────┤
│ Id (PK) │────────<│ AuthorId(FK)│
│ UserName │ 1:N │ Title │
│ Email │ │ Content │
│ PasswordHash│ │ Slug │
│ Role │ │ CategoryId │
│ CreatedAt │ │ Status │
└─────────────┘ │ ViewCount │
│ CreatedAt │
│ PublishedAt │
└─────────────┘
│ M:N
│
┌─────────────┐
│ PostTags │
├─────────────┤
│ PostId (FK) │
│ TagId (FK) │
└─────────────┘
│
│ N:1
┌─────────────┐
│ Tags │
├─────────────┤
│ Id (PK) │
│ Name │
│ Slug │
└─────────────┘
┌─────────────┐ ┌─────────────┐
│ Categories │ │ Comments │
├─────────────┤ ├─────────────┤
│ Id (PK) │────────<│ PostId (FK) │
│ Name │ 1:N │ AuthorId(FK)│
│ Slug │ │ Content │
│ Description │ │ ParentId(FK)│
└─────────────┘ │ IsApproved │
│ CreatedAt │
└─────────────┘
表设计详情:
1. Users(用户表)
CREATE TABLE Users (
Id NVARCHAR(450) PRIMARY KEY,
UserName NVARCHAR(256) NOT NULL UNIQUE,
Email NVARCHAR(256) NOT NULL UNIQUE,
PasswordHash NVARCHAR(MAX) NOT NULL,
FullName NVARCHAR(100),
Bio NVARCHAR(500),
AvatarUrl NVARCHAR(500),
Role NVARCHAR(50) NOT NULL DEFAULT 'Reader',
EmailConfirmed BIT NOT NULL DEFAULT 0,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
UpdatedAt DATETIME2
);
2. Categories(分类表)
CREATE TABLE Categories (
Id INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(100) NOT NULL UNIQUE,
Slug NVARCHAR(100) NOT NULL UNIQUE,
Description NVARCHAR(500),
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
3. Posts(文章表)
CREATE TABLE Posts (
Id INT PRIMARY KEY IDENTITY(1,1),
Title NVARCHAR(200) NOT NULL,
Slug NVARCHAR(200) NOT NULL UNIQUE,
Content NVARCHAR(MAX) NOT NULL,
Excerpt NVARCHAR(500),
FeaturedImageUrl NVARCHAR(500),
AuthorId NVARCHAR(450) NOT NULL,
CategoryId INT NOT NULL,
Status NVARCHAR(20) NOT NULL DEFAULT 'Draft', — Draft, Published, Archived
ViewCount INT NOT NULL DEFAULT 0,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
UpdatedAt DATETIME2,
PublishedAt DATETIME2,
FOREIGN KEY (AuthorId) REFERENCES Users(Id),
FOREIGN KEY (CategoryId) REFERENCES Categories(Id)
);
CREATE INDEX IX_Posts_AuthorId ON Posts(AuthorId);
CREATE INDEX IX_Posts_CategoryId ON Posts(CategoryId);
CREATE INDEX IX_Posts_Status ON Posts(Status);
CREATE INDEX IX_Posts_PublishedAt ON Posts(PublishedAt DESC);
4. Tags(标签表)
CREATE TABLE Tags (
Id INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(50) NOT NULL UNIQUE,
Slug NVARCHAR(50) NOT NULL UNIQUE,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE()
);
5. PostTags(文章-标签关联表)
CREATE TABLE PostTags (
PostId INT NOT NULL,
TagId INT NOT NULL,
PRIMARY KEY (PostId, TagId),
FOREIGN KEY (PostId) REFERENCES Posts(Id) ON DELETE CASCADE,
FOREIGN KEY (TagId) REFERENCES Tags(Id) ON DELETE CASCADE
);
CREATE INDEX IX_PostTags_TagId ON PostTags(TagId);
6. Comments(评论表)
CREATE TABLE Comments (
Id INT PRIMARY KEY IDENTITY(1,1),
PostId INT NOT NULL,
AuthorId NVARCHAR(450) NOT NULL,
ParentCommentId INT NULL, — 用于嵌套评论
Content NVARCHAR(1000) NOT NULL,
IsApproved BIT NOT NULL DEFAULT 0,
LikeCount INT NOT NULL DEFAULT 0,
CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
UpdatedAt DATETIME2,
FOREIGN KEY (PostId) REFERENCES Posts(Id) ON DELETE CASCADE,
FOREIGN KEY (AuthorId) REFERENCES Users(Id),
FOREIGN KEY (ParentCommentId) REFERENCES Comments(Id)
);
CREATE INDEX IX_Comments_PostId ON Comments(PostId);
CREATE INDEX IX_Comments_AuthorId ON Comments(AuthorId);
CREATE INDEX IX_Comments_ParentCommentId ON Comments(ParentCommentId);
CREATE INDEX IX_Comments_CreatedAt ON Comments(CreatedAt DESC);
实体类定义
Core/Entities/User.cs:
using Microsoft.AspNetCore.Identity;
namespace BlogSystem.Core.Entities
{
public class User : IdentityUser
{
public string FullName { get; set; } = "";
public string? Bio { get; set; }
public string? AvatarUrl { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
// 导航属性
public ICollection<Post> Posts { get; set; } = new List<Post>();
public ICollection<Comment> Comments { get; set; } = new List<Comment>();
}
}
Core/Entities/Category.cs:
namespace BlogSystem.Core.Entities
{
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Slug { get; set; } = "";
public string? Description { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// 导航属性
public ICollection<Post> Posts { get; set; } = new List<Post>();
}
}
Core/Entities/Post.cs:
namespace BlogSystem.Core.Entities
{
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public string Slug { get; set; } = "";
public string Content { get; set; } = "";
public string? Excerpt { get; set; }
public string? FeaturedImageUrl { get; set; }
// 外键
public string AuthorId { get; set; } = "";
public int CategoryId { get; set; }
// 状态
public PostStatus Status { get; set; } = PostStatus.Draft;
public int ViewCount { get; set; } = 0;
// 时间戳
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
// 导航属性
public User Author { get; set; } = null!;
public Category Category { get; set; } = null!;
public ICollection<Tag> Tags { get; set; } = new List<Tag>();
public ICollection<Comment> Comments { get; set; } = new List<Comment>();
}
public enum PostStatus
{
Draft, // 草稿
Published, // 已发布
Archived // 已归档
}
}
Core/Entities/Tag.cs:
namespace BlogSystem.Core.Entities
{
public class Tag
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Slug { get; set; } = "";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// 导航属性
public ICollection<Post> Posts { get; set; } = new List<Post>();
}
}
Core/Entities/Comment.cs:
namespace BlogSystem.Core.Entities
{
public class Comment
{
public int Id { get; set; }
public int PostId { get; set; }
public string AuthorId { get; set; } = "";
public int? ParentCommentId { get; set; } // 父评论ID(用于嵌套评论)
public string Content { get; set; } = "";
public bool IsApproved { get; set; } = false;
public int LikeCount { get; set; } = 0;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
// 导航属性
public Post Post { get; set; } = null!;
public User Author { get; set; } = null!;
public Comment? ParentComment { get; set; }
public ICollection<Comment> Replies { get; set; } = new List<Comment>();
}
}
DTOs 定义
Core/DTOs/PostDTOs.cs:
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Core.DTOs
{
// 创建文章请求
public class CreatePostRequest
{
[Required(ErrorMessage = "标题不能为空")]
[StringLength(200, ErrorMessage = "标题长度不能超过200字符")]
public string Title { get; set; } = "";
[Required(ErrorMessage = "内容不能为空")]
public string Content { get; set; } = "";
[StringLength(500, ErrorMessage = "摘要长度不能超过500字符")]
public string? Excerpt { get; set; }
public string? FeaturedImageUrl { get; set; }
[Required(ErrorMessage = "分类不能为空")]
public int CategoryId { get; set; }
public List<string> Tags { get; set; } = new List<string>();
public PostStatus Status { get; set; } = PostStatus.Draft;
}
// 更新文章请求
public class UpdatePostRequest
{
[Required]
[StringLength(200)]
public string Title { get; set; } = "";
[Required]
public string Content { get; set; } = "";
[StringLength(500)]
public string? Excerpt { get; set; }
public string? FeaturedImageUrl { get; set; }
[Required]
public int CategoryId { get; set; }
public List<string> Tags { get; set; } = new List<string>();
public PostStatus Status { get; set; }
}
// 文章响应
public class PostResponse
{
public int Id { get; set; }
public string Title { get; set; } = "";
public string Slug { get; set; } = "";
public string Content { get; set; } = "";
public string? Excerpt { get; set; }
public string? FeaturedImageUrl { get; set; }
public string Status { get; set; } = "";
public int ViewCount { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
// 作者信息
public string AuthorId { get; set; } = "";
public string AuthorName { get; set; } = "";
public string? AuthorAvatarUrl { get; set; }
// 分类信息
public int CategoryId { get; set; }
public string CategoryName { get; set; } = "";
// 标签
public List<TagResponse> Tags { get; set; } = new List<TagResponse>();
// 评论数
public int CommentCount { get; set; }
}
// 文章列表响应(简化版)
public class PostListResponse
{
public int Id { get; set; }
public string Title { get; set; } = "";
public string Slug { get; set; } = "";
public string? Excerpt { get; set; }
public string? FeaturedImageUrl { get; set; }
public int ViewCount { get; set; }
public DateTime PublishedAt { get; set; }
public string AuthorName { get; set; } = "";
public string CategoryName { get; set; } = "";
public List<string> Tags { get; set; } = new List<string>();
public int CommentCount { get; set; }
}
}
Core/DTOs/CommentDTOs.cs:
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Core.DTOs
{
// 创建评论请求
public class CreateCommentRequest
{
[Required(ErrorMessage = "评论内容不能为空")]
[StringLength(1000, MinimumLength = 1, ErrorMessage = "评论长度必须在1-1000字符之间")]
public string Content { get; set; } = "";
public int? ParentCommentId { get; set; }
}
// 评论响应
public class CommentResponse
{
public int Id { get; set; }
public int PostId { get; set; }
public string Content { get; set; } = "";
public bool IsApproved { get; set; }
public int LikeCount { get; set; }
public DateTime CreatedAt { get; set; }
// 作者信息
public string AuthorId { get; set; } = "";
public string AuthorName { get; set; } = "";
public string? AuthorAvatarUrl { get; set; }
// 父评论ID(用于嵌套显示)
public int? ParentCommentId { get; set; }
// 回复列表
public List<CommentResponse> Replies { get; set; } = new List<CommentResponse>();
}
}
Core/DTOs/CategoryDTOs.cs:
using System.ComponentModel.DataAnnotations;
namespace BlogSystem.Core.DTOs
{
public class CreateCategoryRequest
{
[Required(ErrorMessage = "分类名称不能为空")]
[StringLength(100, ErrorMessage = "分类名称长度不能超过100字符")]
public string Name { get; set; } = "";
[StringLength(500, ErrorMessage = "描述长度不能超过500字符")]
public string? Description { get; set; }
}
public class CategoryResponse
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Slug { get; set; } = "";
public string? Description { get; set; }
public int PostCount { get; set; } // 该分类下的文章数
}
}
Core/DTOs/TagResponse.cs:
namespace BlogSystem.Core.DTOs
{
public class TagResponse
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string Slug { get; set; } = "";
public int PostCount { get; set; } // 该标签下的文章数
}
}
28.2 数据访问层实现
DbContext 配置
Infrastructure/Data/BlogDbContext.cs:
using BlogSystem.Core.Entities;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace BlogSystem.Infrastructure.Data
{
public class BlogDbContext : IdentityDbContext<User>
{
public BlogDbContext(DbContextOptions<BlogDbContext> options)
: base(options)
{
}
public DbSet<Post> Posts { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<Comment> Comments { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// 配置 User
builder.Entity<User>(entity =>
{
entity.Property(e => e.FullName).HasMaxLength(100);
entity.Property(e => e.Bio).HasMaxLength(500);
entity.Property(e => e.AvatarUrl).HasMaxLength(500);
entity.HasIndex(e => e.Email).IsUnique();
});
// 配置 Category
builder.Entity<Category>(entity =>
{
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Slug).IsRequired().HasMaxLength(100);
entity.Property(e => e.Description).HasMaxLength(500);
entity.HasIndex(e => e.Name).IsUnique();
entity.HasIndex(e => e.Slug).IsUnique();
});
// 配置 Post
builder.Entity<Post>(entity =>
{
entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
entity.Property(e => e.Slug).IsRequired().HasMaxLength(200);
entity.Property(e => e.Content).IsRequired();
entity.Property(e => e.Excerpt).HasMaxLength(500);
entity.Property(e => e.FeaturedImageUrl).HasMaxLength(500);
entity.Property(e => e.Status).HasConversion<string>();
entity.HasIndex(e => e.Slug).IsUnique();
entity.HasIndex(e => e.AuthorId);
entity.HasIndex(e => e.CategoryId);
entity.HasIndex(e => e.Status);
entity.HasIndex(e => e.PublishedAt);
entity.HasOne(e => e.Author)
.WithMany(u => u.Posts)
.HasForeignKey(e => e.AuthorId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(e => e.Category)
.WithMany(c => c.Posts)
.HasForeignKey(e => e.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
// 配置多对多关系(Post <-> Tag)
entity.HasMany(e => e.Tags)
.WithMany(t => t.Posts)
.UsingEntity<Dictionary<string, object>>(
"PostTags",
j => j.HasOne<Tag>().WithMany().HasForeignKey("TagId"),
j => j.HasOne<Post>().WithMany().HasForeignKey("PostId"));
});
// 配置 Tag
builder.Entity<Tag>(entity =>
{
entity.Property(e => e.Name).IsRequired().HasMaxLength(50);
entity.Property(e => e.Slug).IsRequired().HasMaxLength(50);
entity.HasIndex(e => e.Name).IsUnique();
entity.HasIndex(e => e.Slug).IsUnique();
});
// 配置 Comment
builder.Entity<Comment>(entity =>
{
entity.Property(e => e.Content).IsRequired().HasMaxLength(1000);
entity.HasIndex(e => e.PostId);
entity.HasIndex(e => e.AuthorId);
entity.HasIndex(e => e.ParentCommentId);
entity.HasIndex(e => e.CreatedAt);
entity.HasOne(e => e.Post)
.WithMany(p => p.Comments)
.HasForeignKey(e => e.PostId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Author)
.WithMany(u => u.Comments)
.HasForeignKey(e => e.AuthorId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(e => e.ParentComment)
.WithMany(c => c.Replies)
.HasForeignKey(e => e.ParentCommentId)
.OnDelete(DeleteBehavior.Restrict);
});
// 添加种子数据
SeedData(builder);
}
private void SeedData(ModelBuilder builder)
{
// 添加默认分类
builder.Entity<Category>().HasData(
new Category { Id = 1, Name = "技术", Slug = "tech", Description = "技术相关文章" },
new Category { Id = 2, Name = "生活", Slug = "life", Description = "生活随笔" },
new Category { Id = 3, Name = "教程", Slug = "tutorial", Description = "教程文章" }
);
// 添加默认标签
builder.Entity<Tag>().HasData(
new Tag { Id = 1, Name = "C#", Slug = "csharp" },
new Tag { Id = 2, Name = "ASP.NET Core", Slug = "aspnetcore" },
new Tag { Id = 3, Name = "EF Core", Slug = "efcore" }
);
}
}
}
Repository 接口定义
Core/Interfaces/IRepository.cs:
using System.Linq.Expressions;
namespace BlogSystem.Core.Interfaces
{
public interface IRepository<T> where T : class
{
// 查询
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
// 分页查询
Task<(IEnumerable<T> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
Expression<Func<T, bool>>? filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null);
// 添加
Task<T> AddAsync(T entity);
Task AddRangeAsync(IEnumerable<T> entities);
// 更新
void Update(T entity);
void UpdateRange(IEnumerable<T> entities);
// 删除
void Remove(T entity);
void RemoveRange(IEnumerable<T> entities);
// 其他
Task<bool> ExistsAsync(Expression<Func<T, bool>> predicate);
Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null);
}
}
Infrastructure/Data/Repositories/Repository.cs:
using System.Linq.Expressions;
using BlogSystem.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace BlogSystem.Infrastructure.Data.Repositories
{
public class Repository<T> : IRepository<T> where T : class
{
protected readonly BlogDbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(BlogDbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public virtual async Task<T?> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public virtual async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
public virtual async Task<(IEnumerable<T> Items, int TotalCount)> GetPagedAsync(
int page,
int pageSize,
Expression<Func<T, bool>>? filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null)
{
IQueryable<T> query = _dbSet;
// 应用筛选条件
if (filter != null)
{
query = query.Where(filter);
}
// 获取总数
int totalCount = await query.CountAsync();
// 应用排序
if (orderBy != null)
{
query = orderBy(query);
}
// 应用分页
var items = await query
.Skip((page – 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public virtual async Task<T> AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
return entity;
}
public virtual async Task AddRangeAsync(IEnumerable<T> entities)
{
await _dbSet.AddRangeAsync(entities);
}
public virtual void Update(T entity)
{
_dbSet.Update(entity);
}
public virtual void UpdateRange(IEnumerable<T> entities)
{
_dbSet.UpdateRange(entities);
}
public virtual void Remove(T entity)
{
_dbSet.Remove(entity);
}
public virtual void RemoveRange(IEnumerable<T> entities)
{
_dbSet.RemoveRange(entities);
}
public virtual async Task<bool> ExistsAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.AnyAsync(predicate);
}
public virtual async Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null)
{
if (predicate == null)
{
return await _dbSet.CountAsync();
}
return await _dbSet.CountAsync(predicate);
}
}
}
专用 Repository 接口
Core/Interfaces/IPostRepository.cs:
using BlogSystem.Core.Entities;
namespace BlogSystem.Core.Interfaces
{
public interface IPostRepository : IRepository<Post>
{
Task<Post?> GetBySlugAsync(string slug);
Task<Post?> GetByIdWithDetailsAsync(int id);
Task<(IEnumerable<Post> Items, int TotalCount)> GetPublishedPostsAsync(
int page,
int pageSize,
int? categoryId = null,
string? tagSlug = null);
Task<IEnumerable<Post>> GetPopularPostsAsync(int count);
Task IncrementViewCountAsync(int postId);
}
}
Core/Interfaces/ICommentRepository.cs:
using BlogSystem.Core.Entities;
namespace BlogSystem.Core.Interfaces
{
public interface ICommentRepository : IRepository<Comment>
{
Task<IEnumerable<Comment>> GetCommentsByPostIdAsync(int postId);
Task<IEnumerable<Comment>> GetApprovedCommentsWithRepliesAsync(int postId);
}
}
Core/Interfaces/ICategoryRepository.cs:
using BlogSystem.Core.Entities;
namespace BlogSystem.Core.Interfaces
{
public interface ICategoryRepository : IRepository<Category>
{
Task<Category?> GetBySlugAsync(string slug);
Task<Category?> GetWithPostsAsync(int id);
}
}
Core/Interfaces/ITagRepository.cs:
using BlogSystem.Core.Entities;
namespace BlogSystem.Core.Interfaces
{
public interface ITagRepository : IRepository<Tag>
{
Task<Tag?> GetBySlugAsync(string slug);
Task<IEnumerable<Tag>> GetPopularTagsAsync(int count);
Task<Tag> GetOrCreateAsync(string name, string slug);
}
}
专用 Repository 实现
Infrastructure/Data/Repositories/PostRepository.cs:
using BlogSystem.Core.Entities;
using BlogSystem.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace BlogSystem.Infrastructure.Data.Repositories
{
public class PostRepository : Repository<Post>, IPostRepository
{
public PostRepository(BlogDbContext context) : base(context)
{
}
public async Task<Post?> GetBySlugAsync(string slug)
{
return await _dbSet
.Include(p => p.Author)
.Include(p => p.Category)
.Include(p => p.Tags)
.FirstOrDefaultAsync(p => p.Slug == slug);
}
public async Task<Post?> GetByIdWithDetailsAsync(int id)
{
return await _dbSet
.Include(p => p.Author)
.Include(p => p.Category)
.Include(p => p.Tags)
.Include(p => p.Comments.Where(c => c.IsApproved && c.ParentCommentId == null))
.ThenInclude(c => c.Author)
.Include(p => p.Comments)
.ThenInclude(c => c.Replies.Where(r => r.IsApproved))
.ThenInclude(r => r.Author)
.FirstOrDefaultAsync(p => p.Id == id);
}
public async Task<(IEnumerable<Post> Items, int TotalCount)> GetPublishedPostsAsync(
int page,
int pageSize,
int? categoryId = null,
string? tagSlug = null)
{
IQueryable<Post> query = _dbSet
.Include(p => p.Author)
.Include(p => p.Category)
.Include(p => p.Tags)
.Where(p => p.Status == PostStatus.Published);
// 按分类筛选
if (categoryId.HasValue)
{
query = query.Where(p => p.CategoryId == categoryId.Value);
}
// 按标签筛选
if (!string.IsNullOrEmpty(tagSlug))
{
query = query.Where(p => p.Tags.Any(t => t.Slug == tagSlug));
}
// 获取总数
int totalCount = await query.CountAsync();
// 应用分页和排序
var items = await query
.OrderByDescending(p => p.PublishedAt)
.Skip((page – 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, totalCount);
}
public async Task<IEnumerable<Post>> GetPopularPostsAsync(int count)
{
return await _dbSet
.Include(p => p.Author)
.Include(p => p.Category)
.Where(p => p.Status == PostStatus.Published)
.OrderByDescending(p => p.ViewCount)
.Take(count)
.ToListAsync();
}
public async Task IncrementViewCountAsync(int postId)
{
var post = await _dbSet.FindAsync(postId);
if (post != null)
{
post.ViewCount++;
_dbSet.Update(post);
}
}
}
}
Infrastructure/Data/Repositories/CommentRepository.cs:
using BlogSystem.Core.Entities;
using BlogSystem.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace BlogSystem.Infrastructure.Data.Repositories
{
public class CommentRepository : Repository<Comment>, ICommentRepository
{
public CommentRepository(BlogDbContext context) : base(context)
{
}
public async Task<IEnumerable<Comment>> GetCommentsByPostIdAsync(int postId)
{
return await _dbSet
.Include(c => c.Author)
.Where(c => c.PostId == postId)
.OrderByDescending(c => c.CreatedAt)
.ToListAsync();
}
public async Task<IEnumerable<Comment>> GetApprovedCommentsWithRepliesAsync(int postId)
{
return await _dbSet
.Include(c => c.Author)
.Include(c => c.Replies.Where(r => r.IsApproved))
.ThenInclude(r => r.Author)
.Where(c => c.PostId == postId && c.IsApproved && c.ParentCommentId == null)
.OrderBy(c => c.CreatedAt)
.ToListAsync();
}
}
}
Infrastructure/Data/Repositories/CategoryRepository.cs:
using BlogSystem.Core.Entities;
using BlogSystem.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace BlogSystem.Infrastructure.Data.Repositories
{
public class CategoryRepository : Repository<Category>, ICategoryRepository
{
public CategoryRepository(BlogDbContext context) : base(context)
{
}
public async Task<Category?> GetBySlugAsync(string slug)
{
return await _dbSet
.FirstOrDefaultAsync(c => c.Slug == slug);
}
public async Task<Category?> GetWithPostsAsync(int id)
{
return await _dbSet
.Include(c => c.Posts.Where(p => p.Status == PostStatus.Published))
.ThenInclude(p => p.Author)
.FirstOrDefaultAsync(c => c.Id == id);
}
}
}
Infrastructure/Data/Repositories/TagRepository.cs:
using BlogSystem.Core.Entities;
using BlogSystem.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace BlogSystem.Infrastructure.Data.Repositories
{
public class TagRepository : Repository<Tag>, ITagRepository
{
public TagRepository(BlogDbContext context) : base(context)
{
}
public async Task<Tag?> GetBySlugAsync(string slug)
{
return await _dbSet
.FirstOrDefaultAsync(t => t.Slug == slug);
}
public async Task<IEnumerable<Tag>> GetPopularTagsAsync(int count)
{
return await _dbSet
.Include(t => t.Posts)
.OrderByDescending(t => t.Posts.Count)
.Take(count)
.ToListAsync();
}
public async Task<Tag> GetOrCreateAsync(string name, string slug)
{
var tag = await GetBySlugAsync(slug);
if (tag == null)
{
tag = new Tag
{
Name = name,
Slug = slug
};
await AddAsync(tag);
}
return tag;
}
}
}
Unit of Work 模式
Core/Interfaces/IUnitOfWork.cs:
namespace BlogSystem.Core.Interfaces
{
public interface IUnitOfWork : IDisposable
{
IPostRepository Posts { get; }
ICommentRepository Comments { get; }
ICategoryRepository Categories { get; }
ITagRepository Tags { get; }
Task<int> SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}
}
Infrastructure/Data/UnitOfWork.cs:
using BlogSystem.Core.Interfaces;
using BlogSystem.Infrastructure.Data.Repositories;
using Microsoft.EntityFrameworkCore.Storage;
namespace BlogSystem.Infrastructure.Data
{
public class UnitOfWork : IUnitOfWork
{
private readonly BlogDbContext _context;
private IDbContextTransaction? _transaction;
// 仓储实例
private IPostRepository? _posts;
private ICommentRepository? _comments;
private ICategoryRepository? _categories;
private ITagRepository? _tags;
public UnitOfWork(BlogDbContext context)
{
_context = context;
}
// 延迟初始化仓储
public IPostRepository Posts
{
get
{
_posts ??= new PostRepository(_context);
return _posts;
}
}
public ICommentRepository Comments
{
get
{
_comments ??= new CommentRepository(_context);
return _comments;
}
}
public ICategoryRepository Categories
{
get
{
_categories ??= new CategoryRepository(_context);
return _categories;
}
}
public ITagRepository Tags
{
get
{
_tags ??= new TagRepository(_context);
return _tags;
}
}
public async Task<