.NET8 Blazor Server + BootstrapBlazor + PostgreSQL + Identity【简易 DDD 架构】

.NET8 Blazor Server + BootstrapBlazor + PostgreSQL + Identity【简易 DDD 架构】

芯宇宙技术社
2026-07-31 / 1 评论 / 3 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2026年07月31日,已超过56天没有更新,若内容或图片失效,请留言反馈。

定位:轻量 DDD,不搞重型 CleanArchitecture,适合中小型后台 / 博客系统,避免过度分层、样板代码爆炸
原则:

  1. Domain 领域层:纯实体、领域事件、常量,无任何外部依赖
  2. Application 应用层:业务用例、DTO、接口,依赖 Domain,不依赖 EF/Blazor
  3. Infrastructure 基础设施:EF、Postgres、仓储实现、第三方服务
  4. Web 客户端层:Blazor Server、UI 组件、页面、Identity 配置、DI 入口
  5. 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 / Web

1. 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" />
  1. 关键代码样例
    ① 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());
        }
    }
}
  1. 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();
  2. 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

评论 (1)

取消
  1. 头像
    芯宇宙技术社 作者
    Windows 10 · Google Chrome

    表情

    回复