
1. 为什么默认字段的索引总是被漏掉在 .Net 项目里用 EFCore Code First 做数据层最容易被忽略的不是主键、不是外键而是那些「每个实体都有、但没人专门管」的默认字段。典型代表就是CreatedAt、UpdatedAt、IsDeleted、TenantId这几个。它们通常来自一个BaseEntity基类业务代码里到处都在用可一到建索引这一步大家往往只记得给Name、Email这种显眼字段加默认字段就集体裸奔了。后果在数据量小的时候完全看不出来等到单表几十万行、查询里又带着WHERE IsDeleted 0 AND CreatedAt start这种条件时全表扫描的代价就上来了。更麻烦的是这类字段的索引需求是「批量」的——十几个实体都要加你不可能一个个手写HasIndex写漏一个就是隐患。这篇就聚焦这个落地问题在 EFCore Code First 模式下怎么用一套可复用的配置骨架给所有实体的默认字段批量补上索引并且用迁移命令验证它真的生效了。同时我会把 TaoToken 的统一 Key/API 通道配置片段一起放进来因为很多团队在本地调试模型、跑迁移脚本、做代码生成时Key 管理是另一件烦人事顺手统一掉能省不少事。适合谁看正在用 .Net 6/7/8 EFCore 做 Code First 开发实体有公共基类想一次性把默认字段索引规范落地的同学。下面所有代码都可以直接复制进项目改。2. TaoToken 前置统一 Key 与 API 通道在动手写索引配置之前先把工具链的入口理顺。TaoToken 在这里的角色是统一模型调用与 API 通道你可以在官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 了解整体能力API 入口是 https://taotoken.net/api这个地址不加 UTM。为什么索引配置这件事要提它因为实际开发里你经常需要让模型帮你生成实体、审查迁移脚本、或者解释某条 SQL 的执行计划。如果每个项目、每台机器都各配一套 Key切换环境时很容易乱。把 Key 收敛到一份settings.json里配合统一的 API 通道本地调试和 CI 都能复用。先拿到 Key进入控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 创建一个。创建后复制出来注意它只完整显示一次。然后在项目根目录或用户配置目录放一份settings.json片段如下{ TaoToken: { BaseUrl: https://taotoken.net/api, ApiKey: sk-你的Key粘贴在这里, DefaultModel: claude-sonnet, TimeoutSeconds: 60 } }读取时用 .Net 的配置系统绑定即可比如在Program.cs里var builder WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile(settings.json, optional: true, reloadOnChange: true); var taoToken builder.Configuration.GetSection(TaoToken); var baseUrl taoToken[BaseUrl]; var apiKey taoToken[ApiKey];这里有个坑要提前说settings.json千万别提交到 Git。把它加进.gitignore团队里每人本地放一份或者用环境变量覆盖ApiKey。我见过有人把 Key 直接写进appsettings.json推上仓库后面只能紧急轮换。配置好之后你可以先用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 做一次连通性验证确认 Key 和通道都正常再回到索引配置的正题。3. 可复制的 DbContext 配置骨架现在进入核心部分。目标很明确在OnModelCreating里对所有继承自公共基类的实体自动给默认字段加索引同时保留手动配置复合索引的能力。先定义基类把默认字段集中起来public abstract class BaseEntity { public DateTime CreatedAt { get; set; } public DateTime? UpdatedAt { get; set; } public bool IsDeleted { get; set; } public long TenantId { get; set; } }然后是DbContext的骨架。关键思路是遍历所有实体类型判断它是否继承自BaseEntity如果是就按字段名找到对应属性并调用HasIndex。这样新增实体时不用改配置自动生效。public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 默认字段索引批量应用到所有 BaseEntity 子类 foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { if (!typeof(BaseEntity).IsAssignableFrom(entityType.ClrType)) continue; var builder modelBuilder.Entity(entityType.ClrType); // CreatedAt 单列索引常用于时间范围查询 builder.HasIndex(nameof(BaseEntity.CreatedAt)) .HasDatabaseName($IX_{entityType.ClrType.Name}_CreatedAt); // IsDeleted 单列索引配合软删除过滤 builder.HasIndex(nameof(BaseEntity.IsDeleted)) .HasDatabaseName($IX_{entityType.ClrType.Name}_IsDeleted); // TenantId IsDeleted 复合索引多租户场景高频 builder.HasIndex(nameof(BaseEntity.TenantId), nameof(BaseEntity.IsDeleted)) .HasDatabaseName($IX_{entityType.ClrType.Name}_Tenant_Deleted); } } }这里用HasIndex(string)而不是 lambda是因为遍历时拿不到强类型表达式用属性名字符串更直接。HasDatabaseName统一命名规则避免 EFCore 自动生成的名字在不同版本间漂移迁移脚本对比时更干净。如果你只想给部分实体加可以加一个标记接口做白名单public interface IIndexedEntity { } public class Order : BaseEntity, IIndexedEntity { }然后把判断条件改成typeof(IIndexedEntity).IsAssignableFrom(entityType.ClrType)。这样默认字段索引只落在你明确标记的实体上避免给日志表这种写入密集、查询很少的表加无用索引。复合索引的排序和筛选条件也可以在这个骨架里扩展。比如软删除场景下你希望索引只覆盖未删除数据builder.HasIndex(nameof(BaseEntity.TenantId), nameof(BaseEntity.CreatedAt)) .HasDatabaseName($IX_{entityType.ClrType.Name}_Tenant_CreatedAt) .HasFilter([IsDeleted] 0);注意HasFilter里的写法是 SQL Server 方言如果你用 PostgreSQL要改成IsDeleted false。这个差异在跨数据库项目里必须留意否则迁移会报错。4. 迁移命令与验证动作配置写完必须通过迁移落到数据库并且验证索引真的建出来了。三步走。第一步生成迁移dotnet ef migrations add AddDefaultFieldIndexes如果你项目里DbContext不在启动项目需要指定dotnet ef migrations add AddDefaultFieldIndexes \ --project src/Infrastructure \ --startup-project src/Api第二步检查生成的迁移文件。打开Migrations/xxxx_AddDefaultFieldIndexes.cs你应该能看到类似这样的CreateIndex调用migrationBuilder.CreateIndex( name: IX_Order_CreatedAt, table: Orders, column: CreatedAt); migrationBuilder.CreateIndex( name: IX_Order_Tenant_Deleted, table: Orders, columns: new[] { TenantId, IsDeleted });如果这里没有你预期的索引说明OnModelCreating里的判断条件没命中回去检查实体是否真的继承自BaseEntity以及GetEntityTypes()是否包含了它。第三步应用迁移并验证dotnet ef database update然后直接查数据库的索引视图确认。SQL Server 下SELECT i.name AS IndexName, t.name AS TableName FROM sys.indexes i JOIN sys.tables t ON i.object_id t.object_id WHERE i.name LIKE IX_%_CreatedAt OR i.name LIKE IX_%_Tenant_Deleted ORDER BY t.name;PostgreSQL 下SELECT indexname, tablename FROM pg_indexes WHERE indexname LIKE IX_%_CreatedAt ORDER BY tablename;能查到记录就说明批量索引生效了。这一步别省我踩过的坑就是迁移文件生成了但database update时因为筛选条件方言不匹配静默失败只看代码以为成了实际库里啥都没有。验证完索引顺手用 TaoToken 的模型对话通道 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 把生成的迁移脚本贴进去让它帮你审一遍有没有遗漏的实体或命名冲突比人眼扫快得多。5. 本篇常见错排查报错一The property CreatedAt cannot be added to entity type X because it is not defined原因是你遍历到的实体没有CreatedAt属性但判断条件却让它进来了。检查typeof(BaseEntity).IsAssignableFrom是否写反——正确写法是基类在前、实体类型在后。写反了会匹配到一堆无关类型。报错二迁移生成成功但database update报Incorrect syntax near IsDeleted这是HasFilter方言问题。SQL Server 用[IsDeleted] 0PostgreSQL 用IsDeleted falseMySQL 8 用IsDeleted 0。如果你的项目要跨库建议把筛选条件抽成配置项按 Provider 分支var isSqlServer Database.IsSqlServer(); var filter isSqlServer ? [IsDeleted] 0 : \IsDeleted\ false;报错三索引名重复冲突如果你手动给某个实体写过HasIndex批量逻辑又给它加了一遍迁移会报重复索引名。解决办法是批量逻辑里先判断该索引是否已存在或者统一约定默认字段索引只走批量逻辑业务字段索引才手写。命名前缀区分开IX_给批量UX_给唯一索引一眼能分清来源。报错四settings.json读取不到Key 为 null检查AddJsonFile的路径和CopyToOutputDirectory。如果settings.json放在项目根但没设置复制到输出目录运行时读的是bin下的路径自然找不到。在.csproj里加ItemGroup None Updatesettings.json CopyToOutputDirectoryPreserveNewest/CopyToOutputDirectory /None /ItemGroup报错五迁移文件里索引顺序和预期不一致HasIndex的字段顺序决定复合索引的列顺序而列顺序直接影响查询能否命中。TenantId, IsDeleted和IsDeleted, TenantId是两个不同的索引。多租户查询通常是WHERE TenantId t AND IsDeleted 0所以TenantId放前面。这个顺序错了索引可能完全用不上。6. 把配置沉淀成团队规范索引配置这件事写一次不难难的是让团队每个人都记得写、写对。我的做法是把上面这套骨架放进项目的Infrastructure层作为BaseDbContext的默认行为新项目直接继承。同时配一份简短的 README说明三件事默认字段索引自动加、业务字段索引手写、迁移后必须查索引视图验证。长期做编码和 Agent 协作的话可以考虑用 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 把模型调用额度统一管理配合接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里的说明把代码审查、迁移脚本生成这些环节串起来。Claude Code 相关的接入可以参考 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 把索引规范的检查做成一个可复用的提示词模板。最后留一个实用技巧在 CI 里加一步迁移生成后自动 grep 迁移文件里CreateIndex的数量和实体数量做对比数量对不上就 fail。这样默认字段索引漏加的问题在合并前就能拦住不用等到线上慢查询报警。