ASP.NET Core .NET8 单体产品级开发指南(小产品专用)

ASP.NET Core .NET8 单体产品级开发指南(小产品专用)

核心定位

小型 SaaS / 工具 / 后台 / ToB 小系统,单体优先,拒绝拆分四层类库,兼顾:开发快、部署简单、维护成本低、可平滑扩微服务,适配个人独立开发。

选型基准

  • 框架:ASP.NET Core .NET8 LTS(长期稳定)
  • 架构:简化版 Clean 单体(同项目内文件夹分层,不拆 4 个类库)
  • 数据库:PostgreSQL / SQLite(本地小产品首选 SQLite,上线换 PG)
  • 前端方案二选一:
    1. 极简:Razor Pages + Bootstrap(不用前后分离,最快上线)
    2. 灵活:内置 WebAPI + Vue3/React(后续可拆分前端独立部署)
  • 必备组件内置:JWT、FluentValidation、EF Core、Serilog、Swagger、本地缓存(小产品不用 Redis)

一、单体目录结构(单个 Web 项目,无多类库)

plaintext

YourProduct/  # 唯一项目,dotnet new webapi / webapp
├─ Domain/           # 领域实体、值对象、业务异常(原独立Domain层)
│  ├─ Entities/
│  ├─ Exceptions/
│  └─ Interfaces/    # IRepository 仓储抽象
├─ Application/     # 业务逻辑、CQRS、DTO、校验、映射(原Application)
│  ├─ Dtos/
│  ├─ CQRS/Commands Queries
│  ├─ Validators/
│  └─ Mappings/
├─ Infrastructure/  # 数据库、仓储实现、第三方工具(原Infrastructure)
│  ├─ Persistence/   # DbContext、实体配置、Repository实现
│  └─ Utils/         # 加密、文件操作、工具类
├─ Api/              # 接口层,仅Controller、中间件、过滤器
│  ├─ Controllers/
│  ├─ Middlewares/
│  ├─ Filters/
│  └─ Models/        # 统一返回封装
├─ wwwroot/          # 静态资源(Razor/前端打包产物)
├─ appsettings.json
└─ Program.cs        # 统一DI注册,全部分层在这里注册

优势对比(单体 vs 多类库分层)

  1. 新建 / 调试速度快,不用管理多项目引用
  2. 小型业务代码量少,跳转方便,不用切换解决方案
  3. 发布仅一个 dll,Docker 打包极简,单文件部署
  4. 业务膨胀后,可直接把 Domain/Application/Infrastructure 剪切拆分类库,无缝转微服务

二、分层权责规则(单体内部隔离,大厂规范不变)

  1. Domain:零外部依赖,只定义实体、仓储接口、业务异常;不能引用 EF、Controller、工具类
  2. Application:依赖 Domain,写业务逻辑 CQRS;只调用仓储抽象,不碰数据库实现
  3. Infrastructure:实现 Domain 的仓储接口、EF 上下文;数据库、文件、第三方 SDK 都放这里
  4. Api:仅接收 http 请求、鉴权、参数转发给 MediatR;禁止写任何业务、数据库代码

三、最小依赖 NuGet 清单(小产品够用,不冗余)

xml

<!-- 框架基础 -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.*" />
<!-- ORM -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.*" />
<!-- CQRS解耦业务 -->
<PackageReference Include="MediatR" Version="12.*" />
<!-- 参数校验 -->
<PackageReference Include="FluentValidation.AspNetCore" Version="11.*" />
<!-- 对象映射 -->
<PackageReference Include="Mapster" Version="7.*" />
<!-- 日志 -->
<PackageReference Include="Serilog.AspNetCore" Version="8.*" />
<!-- 接口文档 -->
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.*" />
<!-- JWT登录授权 -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.*" />

小产品建议:舍弃 Redis,使用IMemoryCache本地缓存,减少部署依赖;并发上千再考虑引入 Redis。

四、核心代码精简模板(可直接复制使用)

1. Domain 业务异常 Domain/Exceptions/BusinessException.cs

csharp

运行

namespace YourProduct.Domain.Exceptions;

public class BusinessException : Exception
{
    public int Code { get; }
    public BusinessException(int code, string msg) : base(msg)
    {
        Code = code;
    }
}

2. 领域实体 Domain/Entities/User.cs

csharp

运行

namespace YourProduct.Domain.Entities;

public class User
{
    public long Id { get; set; }
    public string Username { get; set; } = string.Empty;
    public string PasswordHash { get; set; } = string.Empty;
    public DateTime CreateTime { get; set; }
    public bool IsDeleted { get; set; }
}

3. 仓储抽象 Domain/Interfaces/IUserRepo.cs

csharp

运行

using YourProduct.Domain.Entities;

namespace YourProduct.Domain.Interfaces;

public interface IUserRepo
{
    Task<User?> GetByIdAsync(long id, CancellationToken ct = default);
    Task AddAsync(User user, CancellationToken ct = default);
    Task<User?> GetByNameAsync(string name, CancellationToken ct = default);
}

4. Application DTO、CQRS、校验示例

Dtos/UserDto.cs

csharp

运行

namespace YourProduct.Application.Dtos;

public class UserDto
{
    public long Id { get; set; }
    public string Username { get; set; } = string.Empty;
    public DateTime CreateTime { get; set; }
}

public class CreateUserInput
{
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
}

CQRS/Commands/CreateUserCommand.cs

csharp

运行

using MediatR;
using YourProduct.Application.Dtos;
using YourProduct.Domain.Entities;
using YourProduct.Domain.Exceptions;
using YourProduct.Domain.Interfaces;

namespace YourProduct.Application.CQRS.Commands;

public record CreateUserCommand(CreateUserInput Input) : IRequest<UserDto>;

public class CreateUserHandler : IRequestHandler<CreateUserCommand, UserDto>
{
    private readonly IUserRepo _repo;
    public CreateUserHandler(IUserRepo repo) => _repo = repo;

    public async Task<UserDto> Handle(CreateUserCommand req, CancellationToken ct)
    {
        var exists = await _repo.GetByNameAsync(req.Input.Username, ct);
        if (exists != null)
            throw new BusinessException(400, "用户名已存在");

        var user = new User
        {
            Username = req.Input.Username,
            PasswordHash = req.Input.Password, // 生产加密
            CreateTime = DateTime.Now
        };
        await _repo.AddAsync(user, ct);
        return new UserDto { Id = user.Id, Username = user.Username, CreateTime = user.CreateTime };
    }
}

Validators/CreateUserInputValidator.cs

csharp

运行

using FluentValidation;
using YourProduct.Application.Dtos;

namespace YourProduct.Application.Validators;

public class CreateUserInputValidator : AbstractValidator<CreateUserInput>
{
    public CreateUserInputValidator()
    {
        RuleFor(x => x.Username).NotEmpty().Length(3,20);
        RuleFor(x => x.Password).NotEmpty().MinimumLength(6);
    }
}

5. Infrastructure EF 上下文 + 仓储实现

Persistence/AppDbContext.cs

csharp

运行

using Microsoft.EntityFrameworkCore;
using YourProduct.Domain.Entities;

namespace YourProduct.Infrastructure.Persistence;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> opt) : base(opt) { }
    public DbSet<User> Users => Set<User>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<User>().HasQueryFilter(u => !u.IsDeleted); // 全局软删除
    }
}

Persistence/Repositories/UserRepo.cs

csharp

运行

using Microsoft.EntityFrameworkCore;
using YourProduct.Domain.Entities;
using YourProduct.Domain.Interfaces;

namespace YourProduct.Infrastructure.Persistence.Repositories;

public class UserRepo : IUserRepo
{
    private readonly AppDbContext _db;
    public UserRepo(AppDbContext db) => _db = db;

    public async Task<User?> GetByIdAsync(long id, CancellationToken ct = default)
    {
        return await _db.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
    }

    public async Task AddAsync(User user, CancellationToken ct = default)
    {
        await _db.Users.AddAsync(user, ct);
        await _db.SaveChangesAsync(ct);
    }

    public async Task<User?> GetByNameAsync(string name, CancellationToken ct = default)
    {
        return await _db.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Username == name, ct);
    }
}

6. Api 层统一返回模型 Api/Models/ApiResult.cs

csharp

运行

namespace YourProduct.Api.Models;

public class ApiResult<T>
{
    public int Code { get; set; }
    public string Msg { get; set; } = string.Empty;
    public T? Data { get; set; }

    public static ApiResult<T> Success(T? data, string msg = "成功")
        => new() { Code = 200, Msg = msg, Data = data };

    public static ApiResult<T> Fail(int code, string msg)
        => new() { Code = code, Msg = msg };
}

7. 全局异常中间件 Api/Middlewares/GlobalExceptionMiddleware.cs

统一捕获业务异常 / 系统异常,不用每个接口 try-catch

8. Controller 示例 Api/Controllers/UserController.cs

csharp

运行

using MediatR;
using Microsoft.AspNetCore.Mvc;
using YourProduct.Api.Models;
using YourProduct.Application.CQRS.Commands;
using YourProduct.Application.Dtos;

namespace YourProduct.Api.Controllers;

[ApiController]
[Route("api/user")]
public class UserController : ControllerBase
{
    private readonly IMediator _mediator;
    public UserController(IMediator mediator) => _mediator = mediator;

    [HttpPost("create")]
    public async Task<ActionResult<ApiResult<UserDto>>> Create(CreateUserInput input)
    {
        var res = await _mediator.Send(new CreateUserCommand(input));
        return Ok(ApiResult<UserDto>.Success(res));
    }
}

9. Program.cs 单体一站式 DI 注册(核心)

所有分层注册写在这里,无需拆分多个 DI 扩展类,简化小项目

csharp

运行

using MediatR;
using FluentValidation;
using Serilog;
using Microsoft.EntityFrameworkCore;
using YourProduct.Domain.Interfaces;
using YourProduct.Infrastructure.Persistence;
using YourProduct.Infrastructure.Persistence.Repositories;
using YourProduct.Application.Validators;
using YourProduct.Api.Middlewares;

var builder = WebApplication.CreateBuilder(args);

// 日志
builder.Host.UseSerilog((ctx, lc) => lc
    .ReadFrom.Configuration(ctx.Configuration)
    .WriteTo.Console()
    .WriteTo.File("logs/app.log", rollingInterval: RollingInterval.Day));

// MVC
builder.Services.AddControllers()
    .AddNewtonsoftJson();
builder.Services.AddEndpointsApiExplorer();

// 1. EF Core
builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlite(builder.Configuration.GetConnectionString("Sqlite")));

// 2. 仓储实现注入
builder.Services.AddScoped<IUserRepo, UserRepo>();

// 3. MediatR 扫描当前程序集CQRS
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

// 4. FluentValidation 自动扫描所有校验器
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserInputValidator>();

// 5. 本地缓存(小产品替代Redis)
builder.Services.AddMemoryCache();

// Swagger、JWT、跨域、认证授权按需追加

var app = builder.Build();

// 管道
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

五、appsettings.json 极简配置(SQLite 本地免安装)

json

{
  "ConnectionStrings": {
    "Sqlite": "Data Source=app.db"
  },
  "Serilog": {
    "MinimumLevel": "Information"
  },
  "Jwt": {
    "Secret": "你的超长密钥,生产环境放到环境变量",
    "ExpireMinutes": 1440
  }
}

六、小产品专属开发规范(简化、不折腾)

1. 数据库策略

  • 开发阶段:SQLite 单文件,无需安装数据库,一键运行
  • 上线用户量 < 500:继续 SQLite;并发高直接切换 PostgreSQL,仅改连接字符串
  • 统一软删除(全局查询过滤器),不物理删数据

2. 缓存策略(不引入 Redis)

  • 高频基础数据(配置、字典)用IMemoryCache
  • 缓存穿透 / 击穿简单处理:空值缓存 5 分钟,避免频繁查库
  • 用户上万并发再追加 Redis,架构代码无需改动

3. 登录权限简化方案

  • JWT 无状态登录,单表 User 存角色字符串(Admin/User)
  • 接口使用[Authorize(Roles = "Admin")]控制权限
  • 复杂 RBAC 等用户量起来再扩展,前期不提前设计

4. 部署方案(单体优势)

  1. 单文件发布:dotnet publish -c Release --self-contained true /p:PublishSingleFile=true
  2. Windows:直接 exe 运行,搭配 nssm 注册服务
  3. Linux:Docker 单阶段镜像,一行命令启动;Rocky/Anolis 服务器直接运行
  4. 无需 Nginx 也可裸跑,域名场景再配 Caddy 反向代理自动 HTTPS

5. 避坑原则(个人开发少返工)

  1. 禁止 Controller 写业务,全部丢 CQRS Handler
  2. 禁止数据库实体直接返回前端,统一 DTO 隔离字段
  3. 所有 IO 操作必须 async/await,禁用.Result、.Wait ()
  4. 密钥、连接字符串禁止硬编码,上线用服务器环境变量覆盖配置文件
  5. 所有入参交给 FluentValidation 校验,不手写一堆 if 判断
  6. 统一全局异常中间件,接口只返回标准 ApiResult 格式

七、迭代扩展路线(产品做大平滑升级)

  1. 初期:单项目单体,Razor Pages 内置前端,SQLite
  2. 用户增长:数据库迁移 PostgreSQL,新增 Redis 缓存
  3. 业务模块膨胀:拆分 Domain/Application/Infrastructure 为独立类库,解决方案分层
  4. 多端需求:拆分 WebAPI 独立服务,前端 Vue/React 单独部署
  5. 高并发:拆微服务、引入消息队列、分库分表
阅读剩余
THE END