我正在开发一个 C# 应用程序,我需要在事务中创建数据库记录并同时将文件保存到磁盘。我使用 Entity Framework Core 进行数据库操作,使用标准 C# 文件处理进行文件操作。
但是,我正在努力确保数据库操作和文件保存在事务中以原子方式发生。
基本上,你需要运行 特定数据库策略实例中的
DbContext.SaveChanges()
或 DbContext.SaveChangesAsync()
:
Microsoft.EntityFrameworkCore.Storage.IExecutionStrategy.Execute/ExecuteAsync(....., operation: {...... see below.....}, ....)
策略通过调用获取
var myStrategy = DbContext.Database.CreateExecutionStrategy();
DbContext.Dabase 必须设置为在不启用自动事务的情况下运行:
DbContext.Database.AutoTransactionsEnabled/AutoTransactionBehavior = false;
然后,在
myOwnTransactionAction
里面传入Microsoft.EntityFrameworkCore.Storage.IExecutionStrategy.Execute\ExecuteAsync
作为 operation
参数,您需要创建自己的数据库事务
var transaction = DbContext.Database.BeginTransaction/BeginTransactionAsync()
然后你就可以运行你的文件系统了。如果出现异常,可以捕获并回滚事务:
public static bool RunTryCatchRollbackStrategyWithOwnTransaction(IExecutionStrategy myStrategy)
{
IDbContextTransaction? transaction = null;
bool result;
try
{
result = myStrategy.Execute
(
state: false,
operation:
(DbContext ctx, _) =>
{
transaction = ctx.Database.BeginTransaction();
result = RunMySuperDuperFileSystemStaff();
if (result) ctx.SaveChanges();
},
verifySucceeded:
(_, s) => new ExecutionResult<bool>(s, true));
}
catch
{
transaction?.Rollback();
throw;
}
if (result) transaction.Commit(); else transaction.Rollback()
return result;
}
或者只是使用以下“UnitOfWork”模板或受到其启发 - 它实际上是底层 EF DbContext 的另一个包装器。它被用于生产中。它针对异常/回滚进行了良好的测试 - 不适用于多线程,实际上应该避免多线程,因为它基于 EF 上下文。
using System.Runtime.CompilerServices;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Jesivorka.Library.Infrastructure.Data.EntityFramework.Context;
/// <summary>
/// The "Unit of Work" template wrapping <see cref="DbContext"/> and implementing
/// the basic <see cref="IUnitOfWork.Commit"/> method using
/// <see cref="Microsoft.EntityFrameworkCore.DbContext.SaveChanges()"/> and
/// <see cref="Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(System.Threading.CancellationToken)"/>
/// </summary>
/// <typeparam name="TDbContext">Specific <see cref="DbContext"/> instance.</typeparam>
/// <remarks>
/// This implementation of <see cref="IUnitOfWork"/> wraps the <see cref="DbContext"/> to
/// 1) decouple the repos from the console, controllers, ASP.NET pages....
/// 2) decouple the DbContext and EF from the controllers
/// It exposes the <see cref="DbContext"/> for any other entity framework low level data manipulation.
/// </remarks>
/// <seealso href="http://www.codeproject.com/Articles/615499/Models-POCO-Entity-Framework-and-Data-Patterns"/>
/// <seealso cref="IUnitOfWork"/>
public class UnitOfWorkTemplate<TDbContext>
where TDbContext : DbContext
{
/// <summary>
/// Service provider, used for resolving services or creating scopes if specific implementation needs it.
/// </summary>
protected IServiceProvider ServiceProvider;
protected readonly ILogger Logger;
private Action? _afterSaveChangesAction;
private Action? _beforeSaveChangesAction;
private List<CancellableAction>? _commitActions;
private readonly object _commitActionSync = new();
/// <summary>
/// Creates instance with specific optional <see cref="DbContext"/>
/// </summary>
public UnitOfWorkTemplate(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Logger = ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(GetType());
DbContext = ServiceProvider.GetRequiredService<TDbContext>();
}
/// <summary>
/// Indicates if the wrapped <see cref="DbContext"/> should be disposed at disposing this instance.
/// </summary>
protected TDbContext DbContext { get; private set; }
public virtual Task<bool> CommitAsync() =>
RunWithoutAutoTransaction(async (strategy) =>
{
IDbContextTransaction? transaction = null;
bool result;
try
{
result = await strategy.ExecuteAsync(
false,
async (ctx, _, token) =>
{
transaction = await ctx.Database.BeginTransactionAsync(token);
return await RunRepetitiveSaveActionsAsync(token) && !token.IsCancellationRequested;
},
(_, s, _) => Task.FromResult(new ExecutionResult<bool>(s, true))
);
}
catch
{
if (transaction != null) await transaction.RollbackAsync();
throw;
}
transaction.CommitOrRollback(result);
return result;
});
public virtual bool Commit() =>
RunWithoutAutoTransaction(strategy =>
{
IDbContextTransaction? transaction = null;
bool result;
try
{
result = strategy.Execute(
false,
(ctx, _) =>
{
transaction = ctx.Database.BeginTransaction();
return RunRepetitiveSaveActions();
},
(_, s) => new ExecutionResult<bool>(s, true)
);
}
catch
{
transaction?.Rollback();
throw;
}
transaction.CommitOrRollback(result);
return result;
});
public virtual void AddTransactionAction(CancellableAction action)
{
lock (_commitActionSync)
{
_commitActions ??= new();
_commitActions.Add(action);
}
}
public virtual void AddAfterCommitAction(Action action) => _afterSaveChangesAction += action;
public virtual void AddBeforeCommitAction(Action action) => _beforeSaveChangesAction += action;
/// <summary>
/// Indicates that the <see cref="AddTransactionAction"/> method was called (at least once)
/// </summary>
protected bool IsNextTransactionAction
{
get
{
lock (_commitActionSync) return _commitActions?.Count > 0;
}
}
protected bool RunRepetitiveSaveActions()
{
SaveChanges();
while (IsNextTransactionAction)
{
CancellableAction action;
lock (_commitActionSync) action = _commitActions![0];
var shouldContinue = action().Result;
lock (_commitActionSync) _commitActions.RemoveAt(0);
if (!shouldContinue) return false;
SaveChanges();
}
return true;
}
protected async Task<bool> RunRepetitiveSaveActionsAsync(CancellationToken token)
{
await SaveChangesAsync(token);
while (!token.IsCancellationRequested && IsNextTransactionAction)
{
CancellableAction action;
lock (_commitActionSync) action = _commitActions![0];
var shouldContinue = await action();
lock (_commitActionSync) _commitActions.RemoveAt(0);
if (!shouldContinue || token.IsCancellationRequested) return false;
await SaveChangesAsync(token);
}
return true;
}
protected void SaveChanges()
{
var beforeAction = _beforeSaveChangesAction;
var afterAction = _afterSaveChangesAction;
try
{
_beforeSaveChangesAction = null;
_afterSaveChangesAction = null;
beforeAction?.Invoke();
DbContext.SaveChanges();
afterAction?.Invoke();
}
catch
{
_afterSaveChangesAction = afterAction;
_beforeSaveChangesAction = beforeAction;
throw;
}
}
protected async Task SaveChangesAsync(CancellationToken token)
{
var beforeAction = _beforeSaveChangesAction;
var afterAction = _afterSaveChangesAction;
try
{
_beforeSaveChangesAction = null;
_afterSaveChangesAction = null;
if (!token.IsCancellationRequested) beforeAction?.Invoke();
if (!token.IsCancellationRequested) await DbContext.SaveChangesAsync(token);
if (!token.IsCancellationRequested) afterAction?.Invoke();
}
catch
{
_afterSaveChangesAction = afterAction;
_beforeSaveChangesAction = beforeAction;
throw;
}
}
protected TResult RunWithoutAutoTransaction<TResult>(Func<IExecutionStrategy, TResult> ownTransactionAction)
{
var database = DbContext.Database;
var autoTransactionsEnabled = database.AutoTransactionsEnabled;
try
{
var strategy = database.CreateExecutionStrategy();
return ownTransactionAction(strategy);
}
finally
{
database.AutoTransactionsEnabled = autoTransactionsEnabled;
}
}
protected Task<TResult> RunWithoutAutoTransaction<TResult>(Func<IExecutionStrategy, Task<TResult>> ownTransactionAction)
=> RunWithoutAutoTransaction<Task<TResult>>(ownTransactionAction);
}
public static class IDbContextTransactionExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CommitOrRollback(this IDbContextTransaction? transaction, bool commit)
{
if (transaction == null) return;
if (commit)
{
transaction.Commit();
}
else
{
transaction.Rollback();
}
}
}
然后,您可以使用一些布尔条件动态添加已添加操作的嵌套操作,例如
var unitOfWork = ServiceProvider.GetRequiredService<UnitOfWorkTemplate>();
//first step in the transaction
unitOfWork.AddBeforeCommitAction(() => { Console.WriteLine("1-st message"); });
// at the background: dbContext.SaveChangesAsync()
unitOfWork.AddAfterCommitAction(() => { Console.WriteLine("2-nd message"); });
unitOfWork.AddTransactionAction(MySuperDuperFileSystemStuffAsync); //second step in the transaction
// third step in the transaction adding before/after commit actions dynamically
unitOfWork.AddTransactionAction
(
async () => {
Console.WriteLine("7-th message after calling finishing MySuperDuperFileSystemStuff");
unitOfWork.AddBeforeCommitAction(() => { Console.WriteLine("8-th message"); });
// at the background: dbContext.SaveChangesAsync()
unitOfWork.AddAfterCommitAction(() => { Console.WriteLine("9-th message"); });
return Task.FromResult(true);
}
);
// executing transaction and all added transaction or commit actions
await unitOfWork.CommitAsync();
// this is the second step in the transaction - it does add dynamically last action if there is need to add it
public static async Task<bool> MySuperDuperFileSystemStuffAsync()
{
Console.Write("3-rd message: This is called as second step within a transaction after calling first SaveChanges");
bool someRelevantCondition = true;
if (someRelevantCondition)
{
unitOfWork.AddTransactionAction(async () => {
Console.Write("10-th message");
return Task.FromResult(true);
})
unitOfWork.AddBeforeCommitAction(() => { console.WriteLine("4-th message"); });
// at the background: dbContext.SaveChangesAsync()
unitOfWork.AddAfterCommitAction(() => { console.WriteLine("5-th message"); });
}
return Task.FromResult(true);
}