如何使用C#和Entity Framework Core在事务中创建数据库行并保存文件到磁盘?
解决EF Core事务与文件保存的原子性问题
由于数据库事务(EF Core)和文件系统操作分属不同资源管理器,无法直接纳入单一ACID事务,以下是几种可靠的实现方案:
方案1:临时文件+事务提交后转正(推荐跨场景使用)
核心思路是先将文件写入临时存储,确保数据库事务提交成功后再将文件移动到目标位置(同分区下文件移动为原子操作),失败则清理临时文件:
using var transaction = await _dbContext.Database.BeginTransactionAsync(); string tempFilePath = string.Empty; try { // 生成唯一临时文件路径 tempFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.dat"); // 写入临时文件 await File.WriteAllBytesAsync(tempFilePath, fileContent); // 执行数据库记录创建 var fileRecord = new FileRecord { FileName = "示例文件", StoragePath = targetFilePath, CreateTime = DateTime.UtcNow }; _dbContext.FileRecords.Add(fileRecord); await _dbContext.SaveChangesAsync(); // 提交数据库事务 await transaction.CommitAsync(); // 原子性移动文件到目标路径(同分区下无中间状态) File.Move(tempFilePath, targetFilePath, overwrite: true); } catch (Exception) { // 回滚数据库事务 await transaction.RollbackAsync(); // 清理临时文件 if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); } throw; // 抛出异常供上层处理 }
方案2:补偿事务(处理极端异常场景)
如果数据库事务提交成功,但文件移动失败,需要执行补偿操作删除已创建的数据库记录,同时可配合定时扫描清理无效记录:
using var transaction = await _dbContext.Database.BeginTransactionAsync(); int createdRecordId = 0; string tempFilePath = string.Empty; try { tempFilePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.dat"); await File.WriteAllBytesAsync(tempFilePath, fileContent); var fileRecord = new FileRecord { StoragePath = targetFilePath }; _dbContext.FileRecords.Add(fileRecord); await _dbContext.SaveChangesAsync(); createdRecordId = fileRecord.Id; await transaction.CommitAsync(); // 尝试移动文件 File.Move(tempFilePath, targetFilePath, overwrite: true); } catch (Exception) { // 若事务已提交,执行补偿删除数据库记录 if (transaction.TransactionId != null && createdRecordId != 0) { using var compensationContext = new AppDbContext(_dbContextOptions); using var compTransaction = await compensationContext.Database.BeginTransactionAsync(); var invalidRecord = await compensationContext.FileRecords.FindAsync(createdRecordId); if (invalidRecord != null) { compensationContext.FileRecords.Remove(invalidRecord); await compensationContext.SaveChangesAsync(); await compTransaction.CommitAsync(); } } else { await transaction.RollbackAsync(); } // 清理临时文件 if (File.Exists(tempFilePath)) File.Delete(tempFilePath); throw; } // 可选:定时任务扫描数据库,删除对应文件不存在的无效记录
方案3:Windows NTFS文件系统事务(仅限Windows环境)
利用Windows的TransactionScope将数据库和文件操作纳入同一事务,但仅支持NTFS文件系统:
// 需引用System.Transactions NuGet包 using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); try { // EF Core数据库操作 var fileRecord = new FileRecord { StoragePath = targetFilePath }; _dbContext.FileRecords.Add(fileRecord); await _dbContext.SaveChangesAsync(); // 事务性文件写入(需使用TransactionalFileStream) using var fs = new TransactionalFileStream( targetFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, Transaction.Current); await fs.WriteAsync(fileContent); // 提交整个事务 scope.Complete(); } catch (Exception) { // 事务自动回滚,数据库和文件操作全部撤销 throw; }
注意:TransactionalFileStream需自行实现或使用第三方库,且无法跨平台。
内容的提问来源于stack exchange,提问作者Peter Boshra
相关产品推荐
相关产品推荐

