You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 17:15:24