资讯动态

ASP.NET Core WebAPI与EF Core:构建可复用增删改查模板

发布时间:2026/9/14 14:40:54 来源:尧图企业网站定制
简介面向ASP.NET Core初学者及需要快速搭建WebAPI数据接口的后端开发者这是一份框架搭建入门级配套代码包演示如何通过ADO方式连接MySQL数据库完成增删改查、分页等常用接口可作为搭建可运行WebAPI的最小项目骨架。资源共41个文件压缩包仅132KB包含12个C#源文件、1个SQL脚本、6个JSON配置文件以及项目工程文件、DLL等控制器、模型、数据库操作类分层清晰SQL脚本可导入建表配置好连接串后即可运行查看效果整体结构简洁便于二次开发。已有1458人浏览学习受到不少后端学习者关注。模板覆盖WebAPI路由设计、ADO数据访问封装、分页参数处理、配置文件读取等关键编写思路目录入口与数据访问层拆分明了适合作为项目初始模板、课程实训或毕业设计参考也可按需扩展为更完整的数据服务层。1. ASP.NET Core WebAPI 与数据增删改查模板先解决骨架再谈业务几年前我第一次接手 .NET 组的时候团队里每个新项目的起手式都不一样有人用控制台改成 Web 服务有人把仓储层抄了三百行还有人直接在 Controller 里拼 SQL。后来我们把“ASP.NET Core 框架搭建 WebAPI 数据增删改查模板”定成内部公共项目的第一个环节效果很直接新后端能在一小时内跑通一条数据链路前端拿到的不再是接口风格各异的半成品。这个标题的关键不在“建一个空项目”而在于把项目结构、数据访问方式、返回格式和异常处理统一成一套可复用的模板后续新增一张业务表时只改实体、DTO 和路由就能交付。适合刚转 .NET 的工程师理解 WebAPI 的骨架也适合团队负责人借此统一日常后端项目的落地规范。2. 搭建 WebAPI 项目骨架模板、最小 API 与 Program.cs 的关键配置2.1 用 dotnet new 创建 WebAPI并确认两个模板参数新建项目的命令本身不值得花太多篇幅但有两个参数决定了后续手感--use-controllers和--use-minimal-apis。在 .NET 8/9 的模板里默认并不是控制器风格直接dotnet new webapi出来的是 MapGet/MapPost 的最小 API 写法。如果你要做的是一套对数据表增删改查模板我一般会明确指定控制器模板dotnet new webapi -n Demo.Api --use-controllers cd Demo.Api dotnet add package Microsoft.EntityFrameworkCore.Sqlite dotnet add package Microsoft.EntityFrameworkCore.Design code .-n Demo.Api指定项目名--use-controllers生成带 Controllers 目录的经典结构。.Design包是为了后续在命令行执行迁移命令用的只在开发期需要。如果你团队用的是 Visual Studio勾选“控制器”选项即可效果一样。需要注意模板自带的WeatherForecastController或SampleController只是演示代码模板搭建阶段建议直接删掉避免后面在 Swagger 列表里看到一堆无关路径。2.2 最小 API 与控制器 API 的取舍模板基座选哪个标题里写的是“框架搭建”这里的框架指的是承载 CRUD 的服务端骨架。ASP.NET Core 官方从 .NET 6 开始主推最小 API它的优点是路由代码集中、启动文件短但放到“数据增删改查模板”这个场景里有明显短板控制器天然支持[ApiController]提供的自动 400 响应、模型校验、路由前缀还可以配合过滤器统一处理日志和异常。最小 API 也能做这些事但每个路由都要用扩展方法去挂载模板化之后可读性会变差。我的经验是如果项目里接口数量少于 20 个且不打算做后台管理类系统最小 API 更顺手如果目标是“给数据表做一套标准操作模板后续复制粘贴”控制器 EF Core 的组合是更常规的选择。下面的内容就以控制器方案为准但这不妨碍你把 Controller 里的逻辑抽到服务层Controller 只做参数映射和结果包装。2.3 Program.cs 中必须注册的五个配置项用模板创建的项目带一个很干净的 Program.cs但离“能稳定支撑增删改查的 WebAPI”还差四样东西数据库上下文注册、控制器服务、Swagger 接口文档和 JSON 序列化选项。一个典型的最小可运行版本如下var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers() .AddJsonOptions(options { // 避免导航属性循环引用导致序列化抛异常 options.JsonSerializerOptions.ReferenceHandler System.Text.Json .Serialization.ReferenceHandler.IgnoreCycles; }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContextAppDbContext(options options.UseSqlite(builder.Configuration.GetConnectionString(Default))); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.MapControllers(); app.Run();AddControllers注册 MVC 控制器所需的服务没有它Controller 不会被发现AddEndpointsApiExplorer和AddSwaggerGen负责生成 OpenAPI 文档调试 CRUD 接口时直接在 Swagger UI 里点按钮即可AddDbContext的注册生命周期默认是 Scoped也就是每个 HTTP 请求一个上下文实例这是 EF Core 的标准用法不要改成 Singleton。ReferenceHandler.IgnoreCycles的作用比较隐蔽但很实用当你返回的实体包含导航属性而导航属性又指回父实体时默认的 JSON 序列化会抛出循环引用异常开启忽略后虽然会截断链路但接口不会挂掉。3. 用 EF Core 对接数据源DbContext、实体映射与迁移机制3.1 按数据库选包与连接串参数做数据增删改查模板先要确定数据访问层。EF Core 是 ASP.NET Core 生态里的标准 ORM它能把你写的 LINQ 查询翻译成 SQL并支持代码优先迁移。数据库方面本地演示用 SQLite 最省事连appsettings.json都不用改权限实际企业项目里 SQL Server 和 MySQL 更常见。选择包的方式很简单# SQLite 开发调试 dotnet add package Microsoft.EntityFrameworkCore.Sqlite # SQL Server 生产环境 dotnet add package Microsoft.EntityFrameworkCore.SqlServer # MySQL 8.x 社区常用 Pomelo 驱动 dotnet add package Pomelo.EntityFrameworkCore.MySqlPomelo 连接串里的参数要比 SQLite 多几个常见的配置项如下参数说明示例Server数据库地址127.0.0.1 或 localhostPort端口3306Database库名school_devUser / Password账号密码root / 你的密码TreatTinyAsBooleantinyint(1) 是否映射成 boolfalseAllowPublicKeyRetrievalMySQL 8 是否允许请求公钥true一个完整的 MySQL 连接串看起来像这样Server127.0.0.1;Port3306;Databaseschool_dev;Userroot;Passwordyour_pwd;TreatTinyAsBooleanfalse;AllowPublicKeyRetrievaltrue注意TreatTinyAsBoolean默认是 true如果某张表的tinyint(1)列实际存的是 0 和 1 以外的值实体属性类型用 byte 会更安全。这个选项是很多新手查半天才发现的坑。3.2 实体类设计的三个注意点主键、软删除、并发令牌增删改查模板终究要落到实体类上。设计表对应的实体时除了“数据库有什么字段就写什么属性”之外有三个约定我会提前放进模板第一个是主键类型尽量统一为long比int多一倍的上限也比字符串主键节省索引空间。第二个是软删除字段后台管理系统里删除操作经常要留痕IsDeleted字段配合全局过滤可以让查询自动屏蔽已删数据。第三个是并发令牌用RowVersion能避免两个请求同时更新一条记录时互相覆盖。public class Student { public long Id { get; set; } [MaxLength(50)] public string Name { get; set; } default!; public int Age { get; set; } [MaxLength(100)] public string? Email { get; set; } public bool IsDeleted { get; set; } public byte[] RowVersion { get; set; } default!; }[MaxLength]会参与迁移生成控制列长度default!是给非空引用类型一个空值默认避免编译器告警byte[] RowVersion在 SQL Server 里被配置为 rowversion 后每次更新数据库自动改值EF Core 会在 UPDATE 语句的 WHERE 条件里带上旧值以此判断有没有被别的事务改过。SQLite 不支持自动 rowversion本地演示时可以暂时去掉这个属性不影响其他部分。3.3 DbContext 注册与迁移三连从模型到数据库表实体定义完需要建一个继承DbContext的类public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetStudent Students SetStudent(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityStudent(entity { entity.ToTable(student_info); entity.HasIndex(x x.Name); entity.HasQueryFilter(x !x.IsDeleted); if (Database.IsSqlServer()) { entity.Property(x x.RowVersion) .IsRowVersion() .IsConcurrencyToken(); } }); } }DbSetStudent是暴露给查询用的入口没写set;而用表达式体 SetStudent()是为了在上下文里用受控方式获取集合。HasQueryFilter是关键它会把这个表达式自动合并到所有针对Student的 LINQ 查询末尾相当于给每一条 SQL 都加上WHERE IsDeleted 0。Database.IsSqlServer()的写法让同一个模型可以在不同数据库间切换从 SQL Server 换到 SQLite 时不需要改实体代码。迁移命令在项目根目录执行dotnet ef migrations add InitialCreate -o Data/Migrations dotnet ef database updatemigrations add InitialCreate会在Data/Migrations目录生成迁移文件database update把变更应用到当前连接串指向的数据库。注意区分EnsureCreated和迁移EnsureCreated只适合原型演示它不记录迁移历史后续模型一改就麻凡是给团队维护的项目一律走迁移。迁移失败时先看最后的HResult和连接串绝大多数情况不是 SQL 语法问题而是数据库连接串没配上或Design包未安装。需要输出 SQL 脚本时用dotnet ef migrations script -o upgrade.sql这个脚本可以在 CI 或 DBA 手里执行不用在本机装 SDK。4. 在控制器里落地增删改查模板异步方法、状态码与数据校验4.1 REST 路由与增删改查动作的对应关系控制器是增删改查模板最直观的体现。前面已经提到[ApiController]特性会启用自动模型状态验证配合 REST 风格的路由一个资源通常有五个标准动作GET 列表、GET 单条、POST 新增、PUT 整笔更新、DELETE 删除。路由设计为/api/student而不是/api/student/getlist是为了让 URL 与 HTTP 方法共同表达语义。HTTP 方法路由用途成功响应GET/api/student查询列表200 OKGET/api/student/{id}查询单条200 OKPOST/api/student新增数据201 CreatedPUT/api/student/{id}修改数据200 OKDELETE/api/student/{id}删除数据204 No Content状态码不是随便定的。新增后返回201而不是200是因为前者带 Location 头并明确指出资源已创建删除成功返回204 No Content避免返回一个空 body 还占用带宽找不到资源时返回404参数校验失败时返回400这些都是前端和联调同学最容易依赖的规则。4.2 一个可直接复制的 Student 控制器把前面的实体与上下文组合起来一个可用的增删改查控制器代码如下[ApiController] [Route(api/[controller])] public class StudentController : ControllerBase { private readonly AppDbContext _db; public StudentController(AppDbContext db) { _db db; } [HttpGet] public async TaskActionResultListStudent GetList( string? keyword, int page 1, int pageSize 20, CancellationToken ct default) { var query _db.Students.AsNoTracking().AsQueryable(); if (!string.IsNullOrWhiteSpace(keyword)) { query query.Where(x x.Name.Contains(keyword) || x.Email!.Contains(keyword)); } var total await query.CountAsync(ct); var items await query .OrderBy(x x.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(ct); return Ok(new { total, items }); } [HttpGet({id:long})] public async TaskActionResultStudent GetById(long id, CancellationToken ct) { var student await _db.Students .AsNoTracking() .FirstOrDefaultAsync(x x.Id id, ct); if (student is null) { return NotFound(); } return Ok(student); } [HttpPost] public async TaskActionResultStudent Create( StudentDto input, CancellationToken ct) { var entity new Student { Name input.Name, Age input.Age, Email input.Email }; _db.Students.Add(entity); await _db.SaveChangesAsync(ct); return CreatedAtAction(nameof(GetById), new { id entity.Id }, entity); } [HttpPut({id:long})] public async TaskActionResultStudent Update( long id, StudentDto input, CancellationToken ct) { var entity await _db.Students .FirstOrDefaultAsync(x x.Id id, ct); if (entity is null) { return NotFound(); } entity.Name input.Name; entity.Age input.Age; entity.Email input.Email; await _db.SaveChangesAsync(ct); return Ok(entity); } [HttpDelete({id:long})] public async TaskIActionResult Delete(long id, CancellationToken ct) { var entity await _db.Students .FirstOrDefaultAsync(x x.Id id, ct); if (entity is null) { return NotFound(); } _db.Students.Remove(entity); await _db.SaveChangesAsync(ct); return NoContent(); } }控制器直接注入AppDbContext是为了让模板更短等你积累多个业务模块后再把它抽到 Service 层也不迟。每个方法都传了CancellationToken ct客户端断开连接时数据库查询会及时取消避免线程池里堆积无效任务。AsNoTracking()用在哪两个地方值得注意查询接口用它因为只读数据不需要跟踪更新接口不用它因为必须先让 EF Core 跟踪实体修改属性后SaveChangesAsync才能生成 UPDATE 语句。CreatedAtAction的返回值里包含了新实体的Id前端不用再去查一次。DTO 的作用是防止客户端传入模板里不允许的字段。举个典型例子创建学生时主键Id和软删除标记IsDeleted应该由服务端控制如果把整个Student实体作为 POST 入参懂接口的人完全可以传一个IsDeleted true进来。规范做法是用只读 DTOpublic class StudentDto { [Required, MaxLength(50)] public string Name { get; set; } default!; [Range(0, 150)] public int Age { get; set; } [EmailAddress, MaxLength(100)] public string? Email { get; set; } }[Required]触发 400 校验[Range]挡住负数年龄[EmailAddress]做格式检查。使用 DTO 之后实体类可以作为内部模型自由加字段不会把实现细节暴露给接口消费者。4.3 并发冲突与事务增删改查模板不能漏的两个边界数据库层面的并发问题在删除和更新接口里最容易出现。两个请求同时读到同一条记录都改了名字后提交的人会把先提交的人覆盖掉这种问题用乐观并发处理try { await _db.SaveChangesAsync(ct); } catch (DbUpdateConcurrencyException ex) { foreach (var entry in ex.Entries) { var dbValues await entry.GetDatabaseValuesAsync(ct); if (dbValues is null) { return NotFound(); } entry.OriginalValues.SetValues(dbValues); } await _db.SaveChangesAsync(ct); }DbUpdateConcurrencyException表示数据库端行版本与实体进入跟踪状态时不一致。这里的处理策略是“以数据库当前值为准重新提交”适合后台管理这类低频场景。如果业务要求“客户端强制覆盖”可以把entry.CurrentValues.SetValues(dbValues)改成加载客户端的原始值再合并需要保留的字段。这个异常处理可以抽到一个全局过滤器里避免每个 Update 方法都写一遍。当一次请求需要操作多张表时比如新增学生同时初始化他的账号模板里要显式使用事务await using var transaction await _db.Database.BeginTransactionAsync(ct); try { _db.Students.Add(student); _db.Accounts.Add(account); await _db.SaveChangesAsync(ct); await transaction.CommitAsync(ct); } catch { await transaction.RollbackAsync(ct); throw; }BeginTransactionAsync之后同一上下文内的操作会在一个数据库事务里执行中途任何一次SaveChangesAsync失败前面的写入都会被回滚。值得强调的是EF Core 的SaveChanges本身是隐式事务但一次请求里调用两次就会变成两个独立事务中途异常时第一个操作不会撤销所以多条写操作需要手动包一层事务。4.4 给 CRUD 接口套一个统一返回结构上面控制器的写法直接返回实体本身特点是代码少、直观但前端需要同时处理多种数据结构成功时是数组失败时是错误文本。另一个更规范的方案是统一返回包装类public record ApiResultT(int Code, string Message, T? Data) { public static ApiResultT Ok(T data) new(0, ok, data); public static ApiResultT Fail(string message, int code 1) new(code, message, default); }控制器里的接口改成return Ok(ApiResultStudent.Ok(student))前端只需要约定Code 0代表成功。配合全局异常过滤器业务异常可以统一映射到ApiResultT.Fail的 JSON 结构而不是返回一个 ASP.NET Core 默认的空错误对象。对于纯 REST 风格的项目直接返回实体没有错但一旦前端有几个团队并行开发统一的失败结构能省掉大量沟通成本。5. 让查询参数、分页与排序进模板增删改查的补充能力5.1 用 IQueryable 动态拼接查询条件避免全表取出再过滤标准的增删改查模板往往只做按主键操作但实际项目里列表页几乎都会带搜索条件。实现动态查询的常见做法是先构造IQueryable再根据参数决定是否追加Where条件。关键点是Where要在调用ToListAsync之前执行否则会把整张表加载到内存再过滤。[HttpGet] public async TaskActionResultListStudent GetList( string? name, int? minAge, int? maxAge, int page 1, int pageSize 20) { var query _db.Students.AsNoTracking().AsQueryable(); if (!string.IsNullOrWhiteSpace(name)) { query query.Where(x x.Name.Contains(name)); } if (minAge.HasValue) { query query.Where(x x.Age minAge.Value); } if (maxAge.HasValue) { query query.Where(x x.Age maxAge.Value); } var total await query.CountAsync(); var items await query .OrderBy(x x.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return Ok(new { total, items }); }IQueryable的本质是表达式树Where方法叠加时只是在组合表达式不会立即执行查询。CountAsync会生成SELECT COUNT(*)ToListAsync才真正把数据取回来。这里的page和pageSize对应列表页的分页参数框架里没有默认值保护时pageSize传 100000 会把所有数据一次性拽出来属于接口性能隐患。5.2 分页模型的参数上限与排序白名单分页模板不能只写Skip/Take还要处理边界。常见约定是page从 1 开始小于 1 时按 1 处理pageSize上限 100超出后截断。实现方式很多用扩展方法最干净public static class QueryableExtensions { public static IQueryableT PageByT( this IQueryableT query, int page, int pageSize, int maxPageSize 100) { if (page 1) page 1; pageSize Math.Clamp(pageSize, 1, maxPageSize); return query.Skip((page - 1) * pageSize).Take(pageSize); } }Math.Clamp是 .NET 6 起的内置方法用来把 pageSize 限制在 1 到 100 之间。控制器里调用query.PageBy(page, pageSize)就能统一控制。排序参数要防止 SQL 注入和性能问题。最直接的做法是不让客户端传字段名服务端用字典白名单映射private static readonly Dictionarystring, ExpressionFuncStudent, object OrderMap new() { [id] x x.Id, [age] x x.Age, [name] x x.Name }; var orderExpr OrderMap.TryGetValue(orderBy, out var expr) ? expr : OrderMap[id]; query orderByDesc ? query.OrderByDescending(orderExpr) : query.OrderBy(orderExpr);白名单字典的好处是客户端只能从id、age、name里选一个排序字段传其他值自动落到id既不需要拼接字符串又让排序行为完全可控。orderByDesc是一个 bool 参数用于控制升序还是降序。如果业务需要支持多字段排序可以把字典值改为ListSortDescriptor结构但单表单查询场景用一个字段排序足够。5.3 模糊查询的转义与 NoTracking 的误用边界模糊查询用Contains生成LIKE %关键字%在数据量超过几十万行时性能和正确性都要注意一个细节用户输入里的%和_是 SQL LIKE 的通配符。比如搜索100%时实际结果会包含“1001”这不符合大多数人的预期。EF Core 里处理方式是var escaped keyword .Replace(\\, \\\\) .Replace(%, \\%) .Replace(_, \\_); query query.Where(x EF.Functions.Like(x.Name, $%{escaped}%));这里用EF.Functions.Like替代Contains因为Like支持指定转义符而Contains的转义行为在高版本里虽然已修复但语义不直观。代价是会导致这列索引失效不过列表页关键词搜索本身走全表扫描也常见真要加速需要加全文索引。另外AsNoTracking()不是万能的跟踪查询返回的实体一旦被修改SaveChanges会把这些修改一并提交所以写接口里不能用AsNoTracking读接口是否使用跟踪模式取决于你有没有“读取后改一两个字段再保存”的需求模板里默认加即可。5.4 用 Swagger 和 HttpClient 快速验证查询参数写完查询接口验证参数最直接的方式是打开 Swagger UI找到 GET 接口点 Try it out把分页和搜索参数填进去看返回。命令行里用 curl 也能测curl http://localhost:5000/api/student?name%E5%BC%A0page1pageSize20%E5%BC%A0是“张”的 URL 编码如果接口返回中文乱码多半是响应编码设置问题检查appsettings.json里有没有设置ApplicationUrl或反向代理层是否加了 charset。还有一个体积不大但很常用的做法在 WinForms 老项目里调 WebAPI 接口时用HttpClient写一段小工具把 GET/POST 的地址、body 都做成可配置项。这个工具能长期复用因为很多内部系统的联调环境就是客户端直连服务端不需要专门装 Postman。6. 模板化进阶用泛型基类封装通用增删改查再谈运行时模型6.1 用基类把增删改查模板改成可复用代码当你有多张结构相近的表时每个控制器都写一遍增删改查会开始烦躁。这时候可以把“按主键查找、新增、更新、删除”抽象到泛型基类里。假设约定每张表的主键属性名为Id基类可以写成[ApiController] public abstract class BaseCrudControllerTEntity, TKey : ControllerBase where TEntity : class { protected readonly AppDbContext Db; protected BaseCrudController(AppDbContext db) { Db db; } private static ExpressionFuncTEntity, bool BuildKeyEquals(TKey id) { var parameter Expression.Parameter(typeof(TEntity), x); var property Expression.Property(parameter, Id); var value Expression.Constant(id, typeof(TKey)); var body Expression.Equal(property, value); return Expression.LambdaFuncTEntity, bool(body, parameter); } [HttpGet({id})] public virtual async TaskActionResultTEntity Get( TKey id, CancellationToken ct) { var item await Db.SetTEntity() .AsNoTracking() .FirstOrDefaultAsync(BuildKeyEquals(id), ct); return item is null ? NotFound() : Ok(item); } [HttpPost] public virtual async TaskActionResultTEntity Create( TEntity entity, CancellationToken ct) { Db.SetTEntity().Add(entity); await Db.SaveChangesAsync(ct); var id typeof(TEntity).GetProperty(Id)?.GetValue(entity); return CreatedAtAction(nameof(Get), new { id id }, entity); } [HttpPut({id})] public virtual async TaskActionResultTEntity Update( TKey id, TEntity entity, CancellationToken ct) { var existing await Db.SetTEntity() .FirstOrDefaultAsync(BuildKeyEquals(id), ct); if (existing is null) { return NotFound(); } Db.Entry(existing).CurrentValues.SetValues(entity); await Db.SaveChangesAsync(ct); return Ok(existing); } [HttpDelete({id})] public virtual async TaskIActionResult Delete(TKey id, CancellationToken ct) { var item await Db.SetTEntity() .FirstOrDefaultAsync(BuildKeyEquals(id), ct); if (item is null) { return NotFound(); } Db.SetTEntity().Remove(item); await Db.SaveChangesAsync(ct); return NoContent(); } }这段代码用表达式树动态构造x.Id id的 Lambda这样泛型基类就不需要where TEntity : IHasIdTKey接口也可以工作。使用方式是让业务控制器继承它[Route(api/[controller])] public class StudentController : BaseCrudControllerStudent, long { public StudentController(AppDbContext db) : base(db) { } }继承之后增删改查的基本能力自动具备剩下的工作是在子类里补充查询条件、DTO 转换或缓存逻辑。注意Update方法里SetValues会把传入实体的所有属性覆盖到现有实体上如果业务上有“部分字段不允许修改”的需求这个方法要按字段逐个赋值不能被框架上的便捷操作带偏。6.2 运行时注册实体让模板应对动态业务表比泛型基类走得更远的一步是在OnModelCreating里运行时注册实体类型。这套机制适合低代码平台、MES 系统里用户自定义表单的场景实体类是编译期不知道的只能靠元数据生成。实现思路是在DbContext里维护一个类型列表public class AppDbContext : DbContext { private readonly ListType _dynamicEntityTypes new(); public void RegisterEntity(Type type) { _dynamicEntityTypes.Add(type); } protected override void OnModelCreating(ModelBuilder modelBuilder) { foreach (var type in _dynamicEntityTypes) { modelBuilder.Entity(type); } // 静态实体配置 } }使用的时候用Db.Set(type)拿到非泛型的 DbSet再配合Property反射调用Add、Update、Remove就能对任意一张运行时注册的表执行标准操作。把泛型基类和运行时注册组合起来就是一套完整的“增删改查模板”静态部分的实体表用基类继承动态部分用元数据驱动。这两种方式各有成本团队规模小、业务固定时模板化到泛型基类足够了业务经常加表、又不想改代码重发布时才值得上运行时模型。实现时记住一点动态注册的表也要走迁移流程建议用migrations script生成增量 SQL而不是依赖EnsureCreated。验证这套模板是否合格的标准很简单开一个新的控制器子类编译通过后直接跑 Swagger增删改查四个方法零改动就能用。如果每个新模块还需要手工改基类代码说明模板的抽象粒度还没到位继续把变化的部分往上提。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价