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. 高并发:拆微服务、引入消息队列、分库分表
上一篇 ASP.NET Core .NET8 CleanArchitecture 产品级完整项目模板
下一篇 WPF 专业级产品开发完整指南(2026 .NET8,工业 / 桌面商用软件标准)
signoz和日志系统 elk的区别?

signoz和日志系统 elk的区别?

SigNoz vs ELK(Elastic Stack),结合你 C# 上位机 + PLC 采集 的场景 一句话概括: ELK:以日志全文检索为核心的日志栈(ES 倒排索引,擅长全文搜报错文本),指标、链路追踪是附加能力,多组件拼凑 SigNoz:OTel 原生一体化可观测平台,

2026-10-20
0 0 0
DateTime.UtcNow , DateTime.Now 有什么区别

DateTime.UtcNow , DateTime.Now 有什么区别

DateTime.UtcNow vs DateTime.Now(C#) 一句话总结 DateTime.UtcNow:获取世界协调时间(UTC,零时区),不受电脑系统时区影响。 DateTime.Now:获取本地时间 = UTC 时间 + 当前操作系统时区偏移。 详

2026-10-16
0 0 0
winform 开机启动

winform 开机启动

c# winform 写的采集程序。平时是登录 windows 桌面,手动点开运行 winform 程序。但是要改成开机启动,不要登录 windows 桌面也能执行 winform 程序。有没有什么办法?不要改造 winform 程序 先说核心结论 WinForm

2026-10-16
0 0 0
C# + Scriban:生成 HTML 报表,自动发邮件(MES / 生产报表场景)

C# + Scriban:生成 HTML 报表,自动发邮件(MES / 生产报表场景)

补充:这套方案在工厂圈子使用情况 MailKit 自动发邮件:大量 MES 在用 Playwright 自动截图 HTML 报表:不少工厂自动化报表在用,属于成熟方案,但不是每个项目都会上。很多项目嫌麻烦就直接 PDF 或者纯表格邮件。 Scriban + Playwright

2026-10-14
0 0 0
C# + Scriban 做静态站点生成

C# + Scriban 做静态站点生成

C# + Scriban 做静态站点生成(对标 baifenwei 的 generate_site.py) Scriban 是 .NET 生态下高性能模板引擎,语法类似 Liquid / Jinja2,刚好用来替代 Python Jinja2,写一个 C# 控制台程序(GenerateSite

2026-10-14
0 0 0
REA(rea.tools)“万物可逆向”:噱头很大,但不是真的什么都能逆向

REA(rea.tools)“万物可逆向”:噱头很大,但不是真的什么都能逆向

REA(rea.tools)“万物可逆向”:噱头很大,但不是真的什么都能逆向 全称 REA = Reverse Engineer Anything,“逆向一切” 只是项目口号,不是能力承诺。 https://rea.tools https://github.com/morluto

2026-10-14
0 0 0
采集上位机LOG文件,不停追加,文件大小50MB

采集上位机LOG文件,不停追加,文件大小50MB

txt 文件很大,40MB,50MB,txt 持续追加,这种场景。Logstash File Input、Flume Taildir、阿里云 Logtail、NewLifeX/X 日志查看工具 Klogg Log Parser Lizard https://githu

2026-10-10
0 0 0
AOI 图片上传 【分布式】方案

AOI 图片上传 【分布式】方案

车间 aoi 的相机图片上传,用飞牛 nas,群晖 nas,高并发,按 5TB / 天、60 天、500 台设备,能不能抗住?如果我在一台服务器放多个 ftp 服务,每条线一个 ftp 服务,端口 8001,第二条线 ftp 服务,端口 8002。ftp 靠谱还 nas 靠谱?稳定,生成环境

2026-10-09
0 0 0
MES 系统 的几种ID比较,雪花id,ULID,UUID

MES 系统 的几种ID比较,雪花id,ULID,UUID

实际场景:浏览器指纹和cookie的UUID 项目FingerprintJS visitorId bdc65ad12633d79792ceeca33a78e6c551.la 访客 ID 5a00c3da-ab80-513d-b7a7-894bcaaecf92来源读取浏览器硬件 / 软件特征,前

2026-10-09
0 0 0
ACCESS数据库访问报错

ACCESS数据库访问报错

AI最擅长找错,分析原因。 💥💥问题1:exclusively locked 因为ACCESS打开数据库后,表:TO_LOGGER_INFO ,当设计视图窗体时,另一个c#小程序去访问这个access数据库会报错: ERROR [HY000] [Microsoft][ODBC Mi

2026-09-29
0 0 0