资讯动态

C#自动建表工具:SQL Server DDL脚本工程化生成

发布时间:2026/10/15 14:08:40 来源:尧图企业网站定制
简介这是一份面向C#开发人员与数据库初学者的SQL Server自动化建表实践项目聚焦解决系统初始化、批量数据导入等场景下手动建表效率低、中文字段命名不规范、兼容性差等实际问题。资源包含30个文件总计971KB主体为7个核心C#源码文件含表结构解析、拼音首字母转换逻辑、3个可执行程序exe与配套配置config、解决方案文件sln/csproj及本地化资源resx完整覆盖从文本定义解析、Unicode字段生成到SQL语句执行的全流程。已有1430人学习下载项目结构清晰支持通过文本文件驱动建表自动将中文列名转为拼音首字母英文标识并确保SQL Server数据库连接、语句拼装与异常处理机制完备附带调试符号pdb与缓存文件便于二次开发与问题追踪。1. C# 开发SQLSERVER数据库自动建表一个能省掉80%重复DDL脚本的手动劳动的工程化工具上周帮某高校实验室重构一个老旧教务系统他们还在用 Excel 表格维护字段定义再由开发手动写 CREATE TABLE 脚本——一张表改三次字段就漏了两处索引导致查询慢得像在等编译完成。直到我甩出这个 C# 自动建表工具把 JSON 描述文件往里一丢它直接连上 SQL Server、校验字段合法性、生成带主键/外键/约束/注释的完整建表语句并可选执行或仅预览。不是 ORM 的迁移功能也不是 EF Core 的 Code First 那套抽象层而是裸 SQL 级别的精准控制 C# 工程化封装——你定义字段名、类型、长度、是否为空、默认值、是否主键、是否自增、是否唯一、是否带注释它就吐出一行行可审计、可版本管理、可人工 review 的 T-SQL。适合中小型项目快速建模、数据中台元数据同步、测试环境一键初始化也适合老系统从 Excel/CSV 迁移建表逻辑。如果你还在手敲CREATE TABLE、反复核对NVARCHAR(50)和VARCHAR(50)区别、为datetime2(3)的精度抓耳挠腮那这项目就是为你写的。2. 技术栈与架构设计为什么用 C# 而不是 PowerShell 或 Python这个项目不是玩具 Demo而是一个可嵌入 CI/CD 流水线、可作为独立 CLI 工具、也可集成进 WinForms 管理界面的轻量级建表引擎。它的技术选型不是拍脑袋决定的而是基于三类真实场景权衡出来的需要强类型校验字段长度不能是字符串50必须是int? Length是否允许空值必须是bool IsNullableSQL Server 的bit类型映射到 C# 的booltinyint映射到byte—— 这些靠动态语言如 Python做运行时校验容易漏C# 的编译期类型检查属性验证DataAnnotations天然兜底需深度集成 SQL Server 元数据 API比如调用SqlConnection.GetSchema(Columns)获取现有表结构、用SqlCommandBuilder.DeriveParameters()探测存储过程参数、甚至读取sys.extended_properties写入字段注释 —— .NET Framework / .NET 6 对 SqlClient 的原生支持比 Python 的 pyodbc 或 PowerShell 的 SqlServer 模块更稳定、文档更全、异常更明确交付形态要“开箱即用”客户现场没有 Python 环境也不愿装 PowerShell 7旧服务器只认 Windows PowerShell 5.1但 .NET Runtime 几乎是 Windows 服务器标配。打包成单文件 exedotnet publish -r win-x64 --self-contained true双击就能跑这才是生产环境真正需要的“零依赖”。整个架构分四层全部落在一个.csproj里无外部 NuGet 依赖仅System.Data.SqlClient或Microsoft.Data.SqlClient后者推荐AutoTableBuilder/ ├── Models/ # 核心模型TableDefinition、ColumnDefinition、ConstraintDefinition ├── Builders/ # SQL 构建器CreateTableSqlBuilder主、IndexSqlBuilder、CommentSqlBuilder ├── Validators/ # 校验器SqlServerTypeValidator、NameValidator、LengthValidator ├── Services/ # 服务层DatabaseService连库/执行/查询、FileService读JSON/YAML └── Program.cs # CLI 入口支持 --input、--connection、--dry-run、--verbose它不碰 Entity Framework不生成实体类不管理 DbContext 生命周期 —— 它只做一件事把结构定义变成可执行、可审计、可回滚的 T-SQL。这种“窄口径、深聚焦”的设计让它在团队交接时几乎没有学习成本新人看一眼TableDefinition.cs就懂字段怎么配看一眼CreateTableSqlBuilder.Build()就知道 SQL 怎么拼。提示项目默认使用Microsoft.Data.SqlClientv5这是微软官方维护的跨平台 SqlClient支持 .NET 6/7/8兼容 Windows/Linux/macOS。若你还在用已停更的System.Data.SqlClient请务必升级否则在 .NET 6 下会遇到连接超时、加密协商失败等玄学问题。2.1 TableDefinition 模型字段定义的“宪法性文件”所有建表逻辑的起点是一个干净、可序列化的 C# 类。它不是 ORM 的IEntityType而是一个纯数据容器字段命名直译 SQL 概念public class TableDefinition { [Required] public string TableName { get; set; } string.Empty; [Required] public ListColumnDefinition Columns { get; set; } new(); public Liststring PrimaryKeys { get; set; } new(); // 字段名列表支持联合主键 public ListForeignKeyDefinition ForeignKeys { get; set; } new(); public string Schema { get; set; } dbo; public string Comment { get; set; } string.Empty; }关键点在于PrimaryKeys是string[]而非ColumnDefinition[]避免循环引用也方便后续 SQL 拼接时直接用字段名ForeignKeys是独立类包含ReferencedTable、ReferencedColumn、OnDeleteCASCADE/NO ACTION等不耦合 ColumnDefinitionSchema默认dbo但可设为staging或archive适配多 schema 管理Comment是整张表的描述最终会转成sp_addextendedproperty语句。这个类被[JsonObject]或System.Text.Json序列化意味着你的输入文件可以是{ TableName: Students, Schema: dbo, Comment: 学生基本信息表, PrimaryKeys: [Id], Columns: [ { Name: Id, SqlServerType: bigint, IsIdentity: true, IsNullable: false, Comment: 主键自增 }, { Name: Name, SqlServerType: nvarchar, Length: 50, IsNullable: false, Comment: 学生姓名 } ] }2.2 CreateTableSqlBuilderT-SQL 生成器的核心逻辑这是整个项目的“心脏”所有 SQL 字符串都从这里流出。它不拼接字符串而是用StringBuilder分段构建每段职责清晰便于调试和单元测试public string Build(TableDefinition table) { var sb new StringBuilder(); // 1. DROP IF EXISTS可选 if (_options.IncludeDropStatement) { sb.AppendLine($IF OBJECT_ID({table.Schema}.{table.TableName}, U) IS NOT NULL DROP TABLE {table.Schema}.{table.TableName};); sb.AppendLine(); } // 2. CREATE TABLE sb.AppendLine($CREATE TABLE {table.Schema}.{table.TableName} (); // 3. 列定义含类型、NULL/NOT NULL、DEFAULT、IDENTITY var columnLines new Liststring(); foreach (var col in table.Columns) { var colDef BuildColumnDefinition(col); columnLines.Add($ {colDef}); } sb.AppendLine(string.Join(,\n, columnLines)); // 4. 主键约束即使单字段也走 CONSTRAINT统一风格 if (table.PrimaryKeys.Any()) { var pkCols string.Join(, , table.PrimaryKeys.Select(c $[{c}])); sb.AppendLine($);\n\nALTER TABLE {table.Schema}.{table.TableName} ADD CONSTRAINT [PK_{table.TableName}] PRIMARY KEY ({pkCols});); } else { sb.AppendLine();); } // 5. 字段注释sp_addextendedproperty if (_options.IncludeComments !string.IsNullOrEmpty(table.Comment)) { sb.AppendLine($\nEXEC sys.sp_addextendedproperty nameNMS_Description, valueN{EscapeSingleQuote(table.Comment)}, level0typeNSCHEMA,level0nameN{table.Schema}, level1typeNTABLE,level1nameN{table.TableName};); } return sb.ToString(); } private string BuildColumnDefinition(ColumnDefinition col) { var typePart GetSqlServerTypeString(col); // 如 nvarchar(50) 或 datetime2(3) var nullPart col.IsNullable ? NULL : NOT NULL; var identityPart col.IsIdentity ? IDENTITY(1,1) : ; var defaultPart !string.IsNullOrEmpty(col.DefaultValue) ? $ DEFAULT {col.DefaultValue} : ; var parts new[] { typePart, nullPart, identityPart, defaultPart } .Where(p !string.IsNullOrWhiteSpace(p)) .ToArray(); return $[{col.Name}] {string.Join( , parts)}; }逻辑说明BuildColumnDefinition是核心中的核心它把ColumnDefinition的每个属性映射成 T-SQL 的语法片段。IsIdentity控制是否加IDENTITY(1,1)DefaultValue直接拼进DEFAULT子句注意这里不做 SQL 注入过滤因为DefaultValue是代码里可控的不是用户输入若需支持表达式如GETDATE()则DefaultValue类型应为string并信任其内容GetSqlServerTypeString()方法内部有硬编码映射表例如private string GetSqlServerTypeString(ColumnDefinition col) { return col.SqlServerType.ToLower() switch { nvarchar $nvarchar({col.Length ?? 255}), varchar $varchar({col.Length ?? 255}), decimal $decimal({col.Precision ?? 18},{col.Scale ?? 2}), datetime2 $datetime2({col.Scale ?? 7}), _ col.SqlServerType // 兜底如 bit, uniqueidentifier }; }所有表名、字段名都用方括号[ ]包裹防止关键字冲突如Order、User注释使用sp_addextendedproperty这是 SQL Server 官方推荐的元数据存储方式SSMS 右键表 → 属性 → 扩展属性里能直接看到。2.3 DatabaseService不只是 ExecuteNonQuery更是安全执行器很多开发者以为“连上数据库执行 SQL”就是一行cmd.ExecuteNonQuery()但生产环境远不止如此连接字符串必须可配置不能硬编码在代码里必须从appsettings.json或命令行参数传入执行必须带事务建表失败时不能留下半截表如只建了列没加主键错误必须可追溯SqlException.Number是关键线索比如 2714对象已存在、156语法错误、208对象名无效必须支持 Dry Run开发阶段不能真执行要能输出 SQL 并退出。DatabaseService就是干这个的public class DatabaseService { private readonly string _connectionString; public DatabaseService(string connectionString) { _connectionString connectionString ?? throw new ArgumentNullException(nameof(connectionString)); } public async Task(bool Success, string Message, string Sql) ExecuteAsync( string sql, bool isDryRun false, CancellationToken ct default) { if (isDryRun) return (true, Dry run mode: SQL generated but not executed., sql); try { await using var conn new SqlConnection(_connectionString); await conn.OpenAsync(ct); await using var tx await conn.BeginTransactionAsync(ct); await using var cmd conn.CreateCommand(); cmd.Transaction tx; cmd.CommandText sql; await cmd.ExecuteNonQueryAsync(ct); await tx.CommitAsync(ct); return (true, Table created successfully., sql); } catch (SqlException ex) when (ex.Number 2714) { return (false, $Table {ExtractTableNameFromSql(sql)} already exists., sql); } catch (SqlException ex) { return (false, $SQL Error {ex.Number}: {ex.Message}, sql); } catch (Exception ex) { return (false, $Unexpected error: {ex.Message}, sql); } } private string ExtractTableNameFromSql(string sql) { // 简单正则提取 CREATE TABLE 后第一个标识符实际项目中建议用 T-SQL 解析器 var match Regex.Match(sql, CREATE\sTABLE\s.*?\.(.*?)\s*\(, RegexOptions.IgnoreCase); return match.Success ? match.Groups[1].Value.Trim([, ]) : unknown; } }参数说明isDryRuntrue时跳过执行只返回(true, , sql)供 CI 脚本做 SQL 审计SqlException捕获做了分级处理2714 是常见友好错误直接提示“已存在”其他错误返回编号和消息方便查 BOL 文档ExtractTableNameFromSql是个简化版真实项目建议用Microsoft.SqlServer.TransactSql.ScriptDom解析 AST但此处为降低依赖用正则够用。3. 快速上手从零开始跑通第一个自动建表别急着改源码先确保你能 5 分钟内跑通一个最简实例。以下步骤在 Windows 10/11 .NET 6 SDK 环境下验证通过Linux/macOS 同理只需换dotnet命令路径。3.1 环境准备安装 .NET 6 SDK 与 SQL Server LocalDB这不是 Java 那种“下载 JDK 配 PATH”的复杂流程。LocalDB 是 SQL Server 的轻量版无需安装完整 SQL Server命令行即可启动# 1. 确认 .NET 6 SDK 已安装最低 6.0.302 dotnet --version # 2. 安装 SQL Server LocalDB仅 Windows从微软官网下载 SqlServer2019-SSEI-Dev.exe勾选 LocalDB # 安装后启动实例 sqllocaldb start MSSQLLocalDB # 3. 创建测试数据库用 sqlcmdWindows 自带 sqlcmd -S (localdb)\MSSQLLocalDB -Q CREATE DATABASE AutoTableDemo;注意sqlcmd在C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\下若提示找不到请将该路径加入系统 PATH或直接用 SSMS 连(localdb)\MSSQLLocalDB手动建库。3.2 编写第一个表定义 JSON 文件在项目根目录新建students.json{ TableName: Students, Schema: dbo, Comment: 学生信息主表, PrimaryKeys: [Id], Columns: [ { Name: Id, SqlServerType: bigint, IsIdentity: true, IsNullable: false, Comment: 自增主键 }, { Name: Name, SqlServerType: nvarchar, Length: 100, IsNullable: false, Comment: 学生姓名 }, { Name: Email, SqlServerType: nvarchar, Length: 255, IsNullable: true, DefaultValue: NULL, Comment: 邮箱可为空 }, { Name: CreatedAt, SqlServerType: datetime2, Scale: 3, IsNullable: false, DefaultValue: GETDATE(), Comment: 创建时间默认当前时间 } ] }关键细节DefaultValue:NULL表示允许空值且无默认值注意T-SQL 中DEFAULT NULL是合法语法GETDATE()是函数调用不加引号Scale:datetime2(3)表示毫秒精度比datetime更精确、存储更省所有字段名用驼峰SQL Server 会自动转成[Name]不怕关键字。3.3 编译并执行 CLI 工具假设你已克隆项目进入源码目录# 1. 还原 NuGet 包首次运行 dotnet restore # 2. 编译Debug 模式带符号 dotnet build -c Debug # 3. 运行指定 JSON 文件、连接字符串、启用注释 dotnet run --project .\AutoTableBuilder.csproj \ -- --input students.json \ --connection Server(localdb)\\MSSQLLocalDB;DatabaseAutoTableDemo;Trusted_Connectiontrue; \ --include-comments \ --verbose成功输出类似[INFO] Loading table definition from students.json... [INFO] Validating column Id: OK [INFO] Validating column Name: OK [INFO] Generating SQL for Students... [SQL] CREATE TABLE dbo.Students ( [Id] bigint NOT NULL IDENTITY(1,1), [Name] nvarchar(100) NOT NULL, [Email] nvarchar(255) NULL DEFAULT NULL, [CreatedAt] datetime2(3) NOT NULL DEFAULT GETDATE() ); ALTER TABLE dbo.Students ADD CONSTRAINT [PK_Students] PRIMARY KEY ([Id]); [INFO] Executing SQL on database AutoTableDemo... [SUCCESS] Table created successfully. [INFO] Adding table comment... [INFO] Adding column comment for Id... [INFO] Done.此时打开 SSMS连(localdb)\MSSQLLocalDB→AutoTableDemo→Tables→dbo.Students右键“属性”在“扩展属性”里能看到表和字段的中文注释。3.4 验证生成的 SQL 是否合规CLI 默认执行但你可以强制--dry-run看它到底生成了什么dotnet run --project .\AutoTableBuilder.csproj \ -- --input students.json \ --connection dummy \ --dry-run \ --verbose输出的 SQL 会被完整打印到控制台你可以复制出来粘贴到 SSMS 的新查询窗口按CtrlShiftM调出“指定模板参数”如果用了占位符然后人工 review类型是否正确nvarchar(100)不是varchar(100)主键是否加了CONSTRAINT不是隐式主键DEFAULT GETDATE()是否没加引号加了就是字符串字面量不是函数调用所有标识符是否都有[ ]防止Order表建失败。这是工程化和脚本化的本质区别可审查、可回归、可 diff。下次同事改了students.jsonGit 提交记录里就能看到Length从100改成200而不是在一堆CREATE TABLE脚本里肉眼找差异。4. 避坑指南那些让我加班到凌晨三点的血泪经验这个项目上线前我在三个不同客户环境踩过坑。有些是 SQL Server 版本差异有些是权限配置有些是 C# 运行时玄学。以下 4 条是高频翻车点按“现象 → 原因 → 解决”列出每条都附带验证命令。4.1 现象执行时报错 “Login failed for user ”连接字符串明明写了Trusted_Connectiontrue原因LocalDB 实例未启动或启动的是ProjectsV13而不是MSSQLLocalDBTrusted_Connectiontrue依赖 Windows 集成认证但 LocalDB 默认只允许BUILTIN\\Users组访问而你的开发账户可能不在该组。解决# 1. 查看所有 LocalDB 实例 sqllocaldb info # 2. 启动默认实例不是 ProjectsV13 sqllocaldb start MSSQLLocalDB # 3. 若仍失败改用 SQL Server 账户认证需提前在 LocalDB 创建登录 # 在 SSMS 连 (localdb)\MSSQLLocalDB → Security → Logins → 新建登录用户名 test密码 Test123 # 连接字符串改为 # Server(localdb)\\MSSQLLocalDB;DatabaseAutoTableDemo;User Idtest;PasswordTest123;4.2 现象datetime2字段生成 SQL 后SSMS 显示类型为datetime精度丢失原因SQL Server Management StudioSSMS旧版本如 v17.x对datetime2(n)的显示有 Bug实际类型正确只是 UI 渲染错误。可通过查询系统视图确认SELECT c.name AS ColumnName, t.name AS TypeName, c.scale AS Scale FROM sys.columns c JOIN sys.types t ON c.user_type_id t.user_type_id WHERE c.object_id OBJECT_ID(dbo.Students) AND c.name CreatedAt; -- 正确结果TypeNamedatetime2, Scale3解决升级 SSMS 到 v19或直接信系统视图不要信 UI。4.3 现象JSON 中DefaultValue设为CURRENT_TIMESTAMP生成 SQL 后报错 “CURRENT_TIMESTAMP is not a recognized built-in function name”原因CURRENT_TIMESTAMP是 ANSI SQL 标准函数SQL Server 支持但Microsoft.Data.SqlClient在某些版本下v4.x会将其转义为字符串。根本原因是DefaultValue字段被当作文本处理未区分“字面量”和“函数”。解决在ColumnDefinition中增加IsFunctionDefault布尔字段或约定函数默认值用大写无引号如GETDATE并在BuildColumnDefinition中特殊处理if (col.IsFunctionDefault !string.IsNullOrEmpty(col.DefaultValue)) { defaultPart $ DEFAULT {col.DefaultValue}(); // 自动加 () } else if (!string.IsNullOrEmpty(col.DefaultValue)) { defaultPart $ DEFAULT {EscapeSingleQuote(col.DefaultValue)}; }4.4 现象建表后字段注释没显示sp_addextendedproperty执行成功但 SSMS 里空白原因sp_addextendedproperty的level0type和level1type参数大小写敏感且必须用全大写SCHEMA、TABLE、COLUMN。小写schema会被忽略。解决检查生成的 SQL确认level0typeNSCHEMA是全大写。可在Build()方法中硬编码sb.AppendLine($\nEXEC sys.sp_addextendedproperty nameNMS_Description, valueN{EscapeSingleQuote(col.Comment)}, level0typeNSCHEMA,level0nameN{table.Schema}, level1typeNTABLE,level1nameN{table.TableName}, level2typeNCOLUMN,level2nameN{col.Name};);提示EscapeSingleQuote必须实现为value.Replace(, )这是 SQL Server 唯一认可的字符串转义方式\或\都无效。5. 二次开发实战如何扩展支持索引、唯一约束与分区表现在你已经能建表、加注释、跑通流程。但真实项目往往需要更多给Email字段加唯一索引、为CreatedAt加聚集索引、按月分区Orders表。这些不是“锦上添花”而是性能刚需。本章不讲理论直接给你可抄的代码、可测的步骤、可落地的改造路径。5.1 扩展索引支持从零添加Indexes数组第一步在TableDefinition中新增Indexes属性public class TableDefinition { // ...原有属性 public ListIndexDefinition Indexes { get; set; } new(); } public class IndexDefinition { [Required] public string Name { get; set; } string.Empty; [Required] public Liststring Columns { get; set; } new(); public bool IsUnique { get; set; } false; public bool IsClustered { get; set; } false; public string Filter { get; set; } string.Empty; // 如 WHERE IsDeleted 0 }第二步编写IndexSqlBuilder仿照CreateTableSqlBuilderpublic class IndexSqlBuilder { public string Build(TableDefinition table) { if (!table.Indexes.Any()) return string.Empty; var sb new StringBuilder(); foreach (var idx in table.Indexes) { var indexType idx.IsUnique ? UNIQUE : ; var clustered idx.IsClustered ? CLUSTERED : NONCLUSTERED ; var columns string.Join(, , idx.Columns.Select(c $[{c}])); var filterClause !string.IsNullOrEmpty(idx.Filter) ? $ WHERE {idx.Filter} : ; sb.AppendLine($CREATE {indexType}{clustered}INDEX [{idx.Name}] ON {table.Schema}.{table.TableName} ({columns}){filterClause};); } return sb.ToString(); } }第三步修改Program.cs的主流程调用它// 在 Build() 之后、ExecuteAsync() 之前 var indexSql _indexBuilder.Build(tableDefinition); if (!string.IsNullOrEmpty(indexSql)) { fullSql \n\n indexSql; }第四步写一个带索引的 JSON 示例{ TableName: Students, Indexes: [ { Name: IX_Students_Email, Columns: [Email], IsUnique: true, Comment: 邮箱唯一索引 }, { Name: IX_Students_CreatedAt, Columns: [CreatedAt], IsClustered: true, Comment: 按创建时间聚集 } ] }验证执行后在 SSMS 中右键表 → “索引” → 查看是否生成IX_Students_EmailUnique和IX_Students_CreatedAtClustered。5.2 支持分区表绕过 SQL Server 复杂语法的务实方案SQL Server 分区表需要先建PARTITION FUNCTION、PARTITION SCHEME再建表时指定ON子句。全手动太重我们只支持最常用场景按datetime2字段范围分区每月一个文件组。改造思路不暴露底层 Function/Scheme只在TableDefinition中加一个PartitionConfigpublic class PartitionConfig { public string PartitionColumn { get; set; } string.Empty; // 如 CreatedAt public string PartitionType { get; set; } MONTHLY; // MONTHLY / YEARLY public int HistoryMonths { get; set; } 12; // 保留最近12个月 public string FileGroupPrefix { get; set; } FG_; // 如 FG_202301 }然后在CreateTableSqlBuilder.Build()中当检测到PartitionConfig时生成ON子句if (table.PartitionConfig ! null !string.IsNullOrEmpty(table.PartitionConfig.PartitionColumn)) { var partitionCol $[{table.PartitionConfig.PartitionColumn}]; // 简化不生成 Function/Scheme只提示用户需预先创建 // 实际项目中可调用 DatabaseService 执行预置 SQL sb.AppendLine($ON [PRIMARY] -- TODO: Replace with partition scheme. Generate script with --gen-partition-script); }再提供一个独立命令--gen-partition-script输出创建 Function/Scheme 的完整 SQLdotnet run --project .\AutoTableBuilder.csproj \ -- --gen-partition-script \ --partition-column CreatedAt \ --partition-type MONTHLY \ --history-months 12输出即为可执行的 T-SQL复制到 SSMS 运行即可。这样既满足分区需求又不把复杂度塞进主流程。5.3 改造验证用 Git Diff 确保每次变更可追溯所有二次开发必须伴随自动化验证。我在某公司落地时强制要求每个新特性如索引、分区必须有对应.json示例文件放在/samples/目录CI 流水线中增加dotnet test用xUnit测试CreateTableSqlBuilder.Build()输出是否匹配预期 SQL用Assert.Equal(expected, actual)每次 PR必须git diff对比samples/下的 JSON 和生成的 SQL确保人类可读性。例如为IX_Students_Email索引写的测试[Fact] public void Build_WithUniqueIndex_GeneratesUniqueIndexSql() { var table new TableDefinition { TableName Students, Columns new ListColumnDefinition { /* ... */ }, Indexes new ListIndexDefinition { new() { Name IX_Students_Email, Columns new() { Email }, IsUnique true } } }; var sql _builder.Build(table); Assert.Contains(CREATE UNIQUE NONCLUSTERED INDEX [IX_Students_Email], sql); Assert.Contains(ON dbo.Students ([Email]), sql); }从那以后我每次加新功能都强制走一遍dotnet testdotnet run --dry-rungit diff三连确保改的不是“能跑就行”的代码而是“能审、能测、能回滚”的工程资产。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑