ASP.NET Core .NET8 单体产品级开发指南(小产品专用)
ASP.NET Core .NET8 单体产品级开发指南(小产品专用)
核心定位
小型 SaaS / 工具 / 后台 / ToB 小系统,单体优先,拒绝拆分四层类库,兼顾:开发快、部署简单、维护成本低、可平滑扩微服务,适配个人独立开发。
选型基准
- 框架:ASP.NET Core .NET8 LTS(长期稳定)
- 架构:简化版 Clean 单体(同项目内文件夹分层,不拆 4 个类库)
- 数据库:PostgreSQL / SQLite(本地小产品首选 SQLite,上线换 PG)
- 前端方案二选一:
- 极简:Razor Pages + Bootstrap(不用前后分离,最快上线)
- 灵活:内置 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 多类库分层)
- 新建 / 调试速度快,不用管理多项目引用
- 小型业务代码量少,跳转方便,不用切换解决方案
- 发布仅一个 dll,Docker 打包极简,单文件部署
- 业务膨胀后,可直接把 Domain/Application/Infrastructure 剪切拆分类库,无缝转微服务
二、分层权责规则(单体内部隔离,大厂规范不变)
- Domain:零外部依赖,只定义实体、仓储接口、业务异常;不能引用 EF、Controller、工具类
- Application:依赖 Domain,写业务逻辑 CQRS;只调用仓储抽象,不碰数据库实现
- Infrastructure:实现 Domain 的仓储接口、EF 上下文;数据库、文件、第三方 SDK 都放这里
- 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. 部署方案(单体优势)
- 单文件发布:
dotnet publish -c Release --self-contained true /p:PublishSingleFile=true - Windows:直接 exe 运行,搭配 nssm 注册服务
- Linux:Docker 单阶段镜像,一行命令启动;Rocky/Anolis 服务器直接运行
- 无需 Nginx 也可裸跑,域名场景再配 Caddy 反向代理自动 HTTPS
5. 避坑原则(个人开发少返工)
- 禁止 Controller 写业务,全部丢 CQRS Handler
- 禁止数据库实体直接返回前端,统一 DTO 隔离字段
- 所有 IO 操作必须 async/await,禁用.Result、.Wait ()
- 密钥、连接字符串禁止硬编码,上线用服务器环境变量覆盖配置文件
- 所有入参交给 FluentValidation 校验,不手写一堆 if 判断
- 统一全局异常中间件,接口只返回标准 ApiResult 格式
七、迭代扩展路线(产品做大平滑升级)
- 初期:单项目单体,Razor Pages 内置前端,SQLite
- 用户增长:数据库迁移 PostgreSQL,新增 Redis 缓存
- 业务模块膨胀:拆分 Domain/Application/Infrastructure 为独立类库,解决方案分层
- 多端需求:拆分 WebAPI 独立服务,前端 Vue/React 单独部署
- 高并发:拆微服务、引入消息队列、分库分表
阅读剩余
THE END