
Bitwarden server 如何编写 Dapper 存储过程与配套迁移脚本【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/server在 Bitwarden serverserver仓库中为 MSSQL 数据访问路径新增或修改一个功能时标准做法不是在 C# 里直接写内联 SQL而是编写存储过程并同步维护两份 SQL 文件一份是 SSDT 项目中的模式源文件一份是随util/Migrator执行的迁移脚本再在 Dapper 仓储中用一层薄方法调用它们最后补上仓库层集成测试。本文按这条路径走一遍完整的操作从定位文件、写对语法差异到迁移脚本的幂等要求、仓储方法写法和测试验证方式。需要动到的三个位置一个存储过程功能涉及三处文件各自有固定位置和语法约束作用位置要求的语法SSDT 模式源MSSQL 查询行为的源头src/Sql/dbo/Stored Procedures/普通CREATE PROCEDURE迁移脚本实际部署到数据库util/Migrator/DbScripts/CREATE OR ALTER PROCEDUREDapper 仓储方法src/Infrastructure.Dapper/Repositories/C#commandType: CommandType.StoredProcedure两套 SQL 语法差异是这个仓库最容易踩的坑SSDT 项目不支持CREATE OR ALTER用了会产生构建错误。SSDT 通过自己的部署模型管理对象生命周期所以源文件里必须是裸CREATE PROCEDURE。迁移脚本必须幂等因为它可能被重跑。CREATE OR ALTER在过程存在或不存在时都能工作迁移脚本里不要用裸CREATE PROCEDURE。命名方面存储过程统一遵循{Entity}_{Action}模式例如User_Create、Cipher_ReadManyByUserId、Organization_DeleteByIdSSDT 侧一个过程一个文件文件名为{Entity}_{Action}.sql。工具链和代码生成依赖这个约定来把仓储方法映射到对应过程。第一步编写 SSDT 源存储过程在src/Sql/dbo/Stored Procedures/下新建{Entity}_{Action}.sql。仓库中现存的 Device_ReadById.sql 可以作为一个最小参照CREATE PROCEDURE [dbo].[Device_ReadById] Id UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON SELECT * FROM [dbo].[DeviceView] WHERE [Id] Id END编写时需要遵守的硬性规则来自 implementing-dapper-queries/SKILL.md 的约定也是仓库现存脚本的共同形态每个存储过程开头写SET NOCOUNT ON参数名用ParamNamePascalCase 形式与 C# 属性名对应GUID 列一律用UNIQUEIDENTIFIER且值由应用层CoreHelpers.GenerateComb()生成不要在存储过程里用NEWID()或NEWSEQUENTIALID()约束命名遵循PK_TableName、FK_Child_Parent、IX_Table_Column、DF_Table_Column。给已有的过程加参数时新参数必须可空并带默认值NewParam DATATYPE NULL——现有调用方不会传这个参数没有默认值会直接把它们弄坏-- 正确 —— 现有调用方不受影响 CREATE OR ALTER PROCEDURE [dbo].[Cipher_Create] Id UNIQUEIDENTIFIER, NewField NVARCHAR(MAX) NULL -- 默认值保护现有调用方 -- 错误 —— 没有默认值意味着必填参数立即破坏所有现有调用方 CREATE OR ALTER PROCEDURE [dbo].[Cipher_Create] Id UNIQUEIDENTIFIER, NewField NVARCHAR(MAX)另有一条与 SSDT 表文件相关的规则在src/Sql/dbo/Tables/里CREATE TABLE与后续的CREATE INDEX/CREATE NONCLUSTERED INDEX之间必须有GO批分隔符否则 SSDT 构建报错。第二步编写配套迁移脚本在util/Migrator/DbScripts/下新建迁移脚本把同一个过程用CREATE OR ALTER再写一遍。文件名沿用仓库既有的YYYY-MM-DD_序号_描述.sql形式日期前缀可参照现存脚本如 2026-08-12_00_AddFolderDeleteByIds.sql该文件开头即CREATE OR ALTER PROCEDURE [dbo].[Folder_DeleteByIds] Ids AS [dbo].[GuidIdArray] READONLY, UserId UNIQUEIDENTIFIER AS BEGIN SET NOCOUNT ON ... END GO迁移脚本的幂等要求同样适用于 DDL。两条高频易错点添加 NOT NULL 列用内联默认值不要用“先加可空列、UPDATE、再 ALTER”的三步法。三步法在大表上会触发全表扫描带内联DEFAULT的ADD在 SQL Server 中是仅元数据的操作-- 正确 —— 仅元数据操作无表扫描 ALTER TABLE [dbo].[Organization] ADD [UseCustomPermissions] BIT NOT NULL CONSTRAINT DF_Organization_UseCustomPermissions DEFAULT 0 -- 错误 —— 在大表上造成全表扫描 ALTER TABLE [dbo].[Organization] ADD [UseCustomPermissions] BIT NULL UPDATE [dbo].[Organization] SET [UseCustomPermissions] 0 ALTER TABLE [dbo].[Organization] ALTER COLUMN [UseCustomPermissions] BIT NOT NULL默认值只用于数值类型BIT、TINYINT、INT、BIGINT。不要给VARCHAR、NVARCHAR或 MAX 类型设默认值——字符串类型的默认值会与 EF Core 迁移产生预期之外的行为。不要在迁移脚本里给大表建索引如dbo.Cipher、dbo.OrganizationUser可能造成服务中断也永远不要写ONLINE ON——生产环境会自动处理且该选项在不支持的 SQL Server 版本上会直接失败。大型索引操作应放到DbScripts_manual。另外两个最容易被漏掉的步骤修改表之后引用该表的视图元数据会过期需要对受影响的视图调用sp_refreshview修改视图之后对依赖它的存储过程调用sp_refreshsqlmodule。第三步在 Dapper 仓储中实现薄方法存储过程是 MSSQL 查询行为的唯一事实来源仓储方法刻意保持“薄”把 C# 参数映射成 SQL 参数、把结果集映射回领域对象不写任何内联查询逻辑。基类 Repository.cs 已经为常规 CRUD 提供了自动映射例如GetByIdAsync直接按命名约定拼接过程名var results await connection.QueryAsyncT( $[{Schema}].[{Table}_ReadById], new { Id id }, commandType: CommandType.StoredProcedure);这正是{Entity}_{Action}命名约定存在的意义。当需要自定义查询时参照 DeviceRepository.cs 中GetByIdentifierAsync的写法using (var connection new SqlConnection(ConnectionString)) { var results await connection.QueryAsyncDevice( $[{Schema}].[{Table}_ReadByIdentifier], new { Identifier identifier }, commandType: CommandType.StoredProcedure); return results.FirstOrDefault(); }匿名对象属性名与存储过程参数名对应Identifier→Identifier。内联 SQL 只应出现在基类和父模式自动提供的场景中不要在具体仓储方法里临时手写。EF 对等要求每个存储过程的行为必须能被 EF Core 实现逐一对等复刻——同样的过滤、排序和副作用。如果过程包含条件更新、多表操作等复杂逻辑在编写时就要把预期行为写清楚供 EF 侧实现对照。第四步用 [DatabaseData] 写集成测试并验证仓库层测试放在test/Infrastructure.IntegrationTest/用[DatabaseData]特性驱动。UserRepositoryTests.cs 中的真实示例[DatabaseTheory, DatabaseData] public async Task DeleteAsync_Works(IUserRepository userRepository) { var user await userRepository.CreateAsync(new User { Name Test User, Email $test{Guid.NewGuid()}example.com, ApiKey TEST, SecurityStamp stamp, }); await userRepository.DeleteAsync(user); var deletedUser await userRepository.GetByIdAsync(user.Id); Assert.Null(deletedUser); }测试依赖的仓储实例由 DI 注入。DatabaseDataAttribute.cs 的工作机制是从用户密钥、BW_TEST_前缀的环境变量或命令行参数读取数据库配置对每个已配置的数据库生成一条理论数据行对 SQL Server 且未启用 EF 的路径它会注册 Dapper 仓储服务services.AddDapperRepositories(...)从而让同一个测试同时覆盖 Dapper 与 EF 两条实现。未配置或未启用的数据库会以Unconfigured/Not-Enabled标签被跳过而不是失败。验证方式对test/Infrastructure.IntegrationTest项目执行dotnet test。判断成功的标准是新仓储方法的测试用例在已配置的 SQL ServerDapper 路径下通过测试体内像上面示例那样“写入 → 操作 → 读回断言”如Assert.Null(deletedUser)全部成立。限制与注意事项两份 SQL 文件必须同步维护但语法不同SSDT 侧用裸CREATE PROCEDURE迁移侧用CREATE OR ALTER。只改一处会导致构建错误或线上库行为不一致。迁移脚本必须可重复执行DbScripts目录下的脚本会在应用启动或 CI/CD 流水线中由 Migrator 执行。修改表结构后记得sp_refreshview/sp_refreshsqlmodule这是最常被漏掉的一步。行为要以 EF 对等为前提设计复杂逻辑需要把预期行为写明否则 EF 侧无法复刻。完整的约定清单含上述示例的出处见 implementing-dapper-queries/SKILL.mdMigrator 的运行时机说明见 util/Migrator/README.md。【免费下载链接】serverBitwarden infrastructure/backend (API, database, Docker, etc).项目地址: https://gitcode.com/GitHub_Trending/ser/server创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考