如何使用 C# 和 Entity Framework Core 在事务中创建数据库行并将文件保存到磁盘?

问题描述 投票:0回答:1

我正在开发一个 C# 应用程序,我需要在事务中创建数据库记录并同时将文件保存到磁盘。我使用 Entity Framework Core 进行数据库操作,使用标准 C# 文件处理进行文件操作。

但是,我正在努力确保数据库操作和文件保存在事务中以原子方式发生。

c# entity-framework entity-framework-core
1个回答
0
投票

基本上,你需要运行 特定数据库策略实例中的

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);
}
© www.soinside.com 2019 - 2024. All rights reserved.