定位:轻量 DDD,不搞重型 CleanArchitecture,适合中小型后台 / 博客系统,避免过度分层、样板代码爆炸
原则:
- Domain 领域层:纯实体、领域事件、常量,无任何外部依赖
- Application 应用层:业务用例、DTO、接口,依赖 Domain,不依赖 EF/Blazor
- Infrastructure 基础设施:EF、Postgres、仓储实现、第三方服务
- Web 客户端层:Blazor Server、UI 组件、页面、Identity 配置、DI 入口
- Identity 用户实体纳入 Domain;数据库操作统一由仓储封装
解决方案结构(4 个类库 + 1 个 Blazor Web 主项目)
XyBlog.sln
├── XyBlog.Domain # 【领域层 · 无外部依赖】
│ ├── Entities # 领域实体(聚合根、实体)
│ │ ├── AppUser.cs # Identity用户聚合根
│ │ ├── Article.cs
│ │ └── Category.cs
│ ├── Enums # 领域枚举
│ ├── Interfaces # 仓储【抽象】(重要!抽象放领域,实现放Infra)
│ │ ├── IRepository.cs
│ │ ├── IArticleRepository.cs
│ │ └── IUserRepository.cs
│ ├── Constants
│ └── DomainEvents # 可选:领域事件
├── XyBlog.Application # 【应用层 · 业务用例】
│ ├── Dtos # 入参、输出DTO
│ │ ├── ArticleDto.cs
│ │ └── LoginDto.cs
│ ├── Services # 应用服务(业务逻辑,调用领域仓储接口)
│ │ ├── Interfaces
│ │ │ ├── IArticleAppService.cs
│ │ │ └── IAuthAppService.cs
│ │ └── Implementations
│ │ ├── ArticleAppService.cs
│ │ └── AuthAppService.cs
│ └── Mappers # 实体 <=> DTO 映射(可选 Mapster)
├── XyBlog.Infrastructure # 【基础设施层 · 技术实现】
│ ├── Data
│ │ ├── AppDbContext.cs # EF DbContext (继承IdentityDbContext)
│ │ ├── Migrations # EF迁移文件(PostgreSQL)
│ │ └── ModelConfigurations # EF实体配置
│ ├── Repositories # 仓储实现,实现Domain定义的IXXXRepository
│ │ ├── EfRepository.cs
│ │ └── ArticleRepository.cs
│ ├── Extensions # Infra层DI扩展
│ └── Services # 第三方实现:文件存储、邮件服务等
└── XyBlog.Web # 【表现层 Blazor Server + BootstrapBlazor + Identity】
├── Program.cs # 统一注册所有层服务
├── App.razor
├── appsettings.json
├── wwwroot
├── Pages # Blazor路由页面
│ ├── Auth/
│ ├── Admin/
│ └── Index.razor
├── Shared
│ ├── MainLayout.razor
│ ├── NavMenu.razor
│ └── Components/ # BootstrapBlazor封装组件
├── Extensions # Web层扩展方法
└── Utils项目依赖关系(严格单向,DDD 核心约束,不能反向引用)
XyBlog.Web
├─> XyBlog.Application
└─> XyBlog.Infrastructure
XyBlog.Application
└─> XyBlog.Domain
XyBlog.Infrastructure
└─> XyBlog.Domain
XyBlog.Domain
⚠️ 【零外部业务依赖】禁止引用 Application / Infrastructure / Web1. NuGet 包引用规划
XyBlog.Domain
只需要基础库,不要引入 EF、Identity UI
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="8.0.x" />XyBlog.Application
<ProjectReference Include="..\XyBlog.Domain\XyBlog.Domain.csproj" />
<!-- 可选 Mapster 对象映射 -->
<PackageReference Include="Mapster" Version="7.4.x" />XyBlog.Infrastructure
<ProjectReference Include="..\XyBlog.Domain\XyBlog.Domain.csproj" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.x" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.x" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.x" />XyBlog.Web(Blazor Server 主项目)
<ProjectReference Include="..\XyBlog.Application\XyBlog.Application.csproj" />
<ProjectReference Include="..\XyBlog.Infrastructure\XyBlog.Infrastructure.csproj" />
<PackageReference Include="BootstrapBlazor" />
<PackageReference Include="BootstrapBlazor.FontAwesome" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.x" />- 关键代码样例
① XyBlog.Domain/Entities/AppUser.cs(聚合根,扩展 Identity)
using Microsoft.AspNetCore.Identity;
namespace XyBlog.Domain.Entities;
public class AppUser : IdentityUser<Guid>
{
public string NickName { get; set; } = string.Empty;
public string Avatar { get; set; } = string.Empty;
public DateTime CreateTime { get; set; } = DateTime.UtcNow;
}
② XyBlog.Domain/Interfaces/IArticleRepository.cs(仓储抽象【放在领域!】)
csharp
运行
using XyBlog.Domain.Entities;
namespace XyBlog.Domain.Interfaces;
public interface IArticleRepository
{
Task<List<Article>> GetListAsync();
Task<Article?> GetByIdAsync(Guid id);
Task AddAsync(Article article);
Task UpdateAsync(Article article);
Task DeleteAsync(Guid id);
}③ XyBlog.Infrastructure/Repositories/ArticleRepository.cs(实现仓储)
using Microsoft.EntityFrameworkCore;
using XyBlog.Domain.Entities;
using XyBlog.Domain.Interfaces;
using XyBlog.Infrastructure.Data;
namespace XyBlog.Infrastructure.Repositories;
public class ArticleRepository : IArticleRepository
{
private readonly AppDbContext _dbContext;
public ArticleRepository(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<List<Article>> GetListAsync()
{
return await _dbContext.Articles.ToListAsync();
}
public async Task<Article?> GetByIdAsync(Guid id)
{
return await _dbContext.Articles.FindAsync(id);
}
public async Task AddAsync(Article article)
{
await _dbContext.Articles.AddAsync(article);
await _dbContext.SaveChangesAsync();
}
public async Task UpdateAsync(Article article)
{
_dbContext.Articles.Update(article);
await _dbContext.SaveChangesAsync();
}
public async Task DeleteAsync(Guid id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_dbContext.Articles.Remove(entity);
await _dbContext.SaveChangesAsync();
}
}
}④ XyBlog.Application/Services/Interfaces/IArticleAppService.cs
using XyBlog.Application.Dtos;
namespace XyBlog.Application.Services.Interfaces;
public interface IArticleAppService
{
Task<List<ArticleDto>> GetAllAsync();
Task<ArticleDto?> GetAsync(Guid id);
Task CreateAsync(ArticleDto dto);
}⑤ XyBlog.Application/Services/Implementations/ArticleAppService.cs
应用服务:协调仓储、转换 DTO,不写原生 EF 查询
using Mapster;
using XyBlog.Application.Dtos;
using XyBlog.Application.Services.Interfaces;
using XyBlog.Domain.Entities;
using XyBlog.Domain.Interfaces;
namespace XyBlog.Application.Services.Implementations;
public class ArticleAppService : IArticleAppService
{
private readonly IArticleRepository _articleRepo;
public ArticleAppService(IArticleRepository articleRepo)
{
_articleRepo = articleRepo;
}
public async Task<List<ArticleDto>> GetAllAsync()
{
var list = await _articleRepo.GetListAsync();
return list.Adapt<List<ArticleDto>>();
}
public async Task<ArticleDto?> GetAsync(Guid id)
{
var entity = await _articleRepo.GetByIdAsync(id);
return entity?.Adapt<ArticleDto>();
}
public async Task CreateAsync(ArticleDto dto)
{
var article = dto.Adapt<Article>();
await _articleRepo.AddAsync(article);
}
}⑥ XyBlog.Infrastructure/Data/AppDbContext.cs
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using XyBlog.Domain.Entities;
namespace XyBlog.Infrastructure.Data;
public class AppDbContext : IdentityDbContext<AppUser, IdentityRole<Guid>, Guid>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<Article> Articles { get; set; }
public DbSet<Category> Categories { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Postgres 小写表名
foreach (var entityType in builder.Model.GetEntityTypes())
{
entityType.SetTableName(entityType.DisplayName().ToLower());
}
}
}Program.cs 注册代码(XyBlog.Web)
建议封装扩展 InfrastructureModule.cs、ApplicationModule.cs 统一批量注册,保持 Program 整洁using BootstrapBlazor; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using XyBlog.Domain.Entities; using XyBlog.Infrastructure.Data; var builder = WebApplication.CreateBuilder(args); // 1. Postgres EF var conn = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext<AppDbContext>(opt => opt.UseNpgsql(conn)); // 2. Identity builder.Services.AddDefaultIdentity<AppUser>(options => { options.Password.RequiredLength = 6; options.Password.RequireDigit = false; options.Password.RequireNonAlphanumeric = false; }) .AddRoles<IdentityRole<Guid>>() .AddEntityFrameworkStores<AppDbContext>(); // 3. 批量注册仓储、应用服务(推荐封装扩展方法) // builder.Services.AddInfrastructureServices(); // builder.Services.AddApplicationServices(); // 4. Blazor Server builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); // 5. BootstrapBlazor builder.Services.AddBootstrapBlazor(); var app = builder.Build(); // 自动迁移 using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); db.Database.Migrate(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseAntiforgery(); app.MapRazorComponents<App>() .AddInteractiveServerRenderMode(); app.Run();Blazor 页面调用示例 Pages/Admin/ArticleList.razor
@attribute [Authorize(Roles = "Admin")] @inject IArticleAppService ArticleAppService @inject ToastService ToastService <PageTitle>文章管理</PageTitle> <Table TItem="ArticleDto" DataSource="_articles" IsStriped="true"> <TableColumn Header="标题" Field="Title" /> <TableColumn Header="操作"> <Template Context="row"> <Button Color="Color.Primary" OnClick="() => Edit(row)">编辑</Button> </Template> </TableColumn> </Table> @code { private List<ArticleDto> _articles = []; protected override async Task OnInitializedAsync() { _articles = await ArticleAppService.GetAllAsync(); } private async Task Edit(ArticleDto dto) { // 打开弹窗编辑 } }5. 架构重要规范(避坑重点)仓储接口定义在 Domain,实现在 Infrastructure(DDD 经典,依赖倒置)
Blazor 页面只依赖 Application 层 IAppService,绝对不要直接注入 IRepository、DbContext
Domain 实体只包含属性、领域行为,禁止注入服务、不能引用 EF
DTO 统一放在 Application,领域实体禁止直接返回前端
Blazor Server 所有注入服务保持 Scoped,不要 Singleton
EF Migrations 放在 Infrastructure 项目!
shell
Add-Migration InitDb -Project XyBlog.Infrastructure -StartupProject XyBlog.Web
Update-Database -Project XyBlog.Infrastructure -StartupProject XyBlog.Web
可选升级方向
引入MediatR,应用层改用 CQRS(查询 / 命令分离)
增加领域事件
增加单元测试项目 XyBlog.Application.UnitTests,Mock 仓储测试业务逻辑
评论 (1)