.NET Core 中的事务中间件

作者:微信公众号:【架构师老卢】
9-13 20:12
9

概述:在某些涉及 HTTP 请求的业务场景中,需要在请求处理的每个步骤中将数据保存到数据库,并在某个部分失败时回滚之前的更改。本文将演示中间件如何促进事务,并在没有异常发生时隐式地将更改提交到数据库。我们将假设使用 SQL 数据库、Dapper micro ORM 和存储库模式来抽象数据访问层。让我们从创建一个连接提供程序类开始:public class SqlConnectionProvider{ private readonly IDbConnection _connection; private IDbTransaction _transaction; public SqlC

在某些涉及 HTTP 请求的业务场景中,需要在请求处理的每个步骤中将数据保存到数据库,并在某个部分失败时回滚之前的更改。本文将演示中间件如何促进事务,并在没有异常发生时隐式地将更改提交到数据库。我们将假设使用 SQL 数据库、Dapper micro ORM 和存储库模式来抽象数据访问层。

让我们从创建一个连接提供程序类开始:

public class SqlConnectionProvider
{
    private readonly IDbConnection _connection;
    private IDbTransaction _transaction;

    public SqlConnectionProvider(string connectionString)
    {
        _connection = new SqlConnection(connectionString);
    }

    public IDbConnection GetDbConnection => _connection;

    public IDbTransaction GetTransaction => _transaction;

    public IDbTransaction CreateTransaction()
    {
        if (_connection.State == ConnectionState.Closed)
            _connection.Open();

        _transaction = _connection.BeginTransaction();

        return _transaction;
    }
}

SqlConnectionProvider创建用于存储库注入的对象,并负责创建事务。SqlConnection

我们需要注册 scoped lifetime。SqlConnectionProvider

builder.Services.AddScoped(_ => new SqlConnectionProvider(Configuration.GetConnectionString("Default")));

是时候开发事务中间件了。我将继续插入所需的代码,以逐步管理交易。下面是 middleware 的定义。

public class DbTransactionMiddleware
{
    private readonly RequestDelegate _next;

    public DbTransactionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, SqlConnectionProvider connectionProvider)
    {
    }
}

上面的代码很简单。我们启动一个事务,调用后续的中间件,然后提交事务,并最终处理它。

public async Task Invoke(HttpContext httpContext, SqlConnectionProvider connectionProvider)
{
    IDbTransaction transaction = null;

    try
    {
        transaction = connectionProvider.CreateTransaction();

        await _next(httpContext);

        transaction.Commit();
    }
    finally
    {
        transaction?.Dispose();
    }
}

假设我们有一个 todo 仓库。有必要将 注入到存储库中,并将打开的事务从中间件传递到已建立的 SQL 连接。SqlConnectionProvider

public class TodoItemRepository : ITodoItemRepository
{
    private readonly SqlConnectionProvider _connectionProvider;
    private readonly IDbConnection _connection;

    public TodoItemRepository(SqlConnectionProvider connectionProvider)
    {
        _connectionProvider = connectionProvider;
        _connection = connectionProvider.GetDbConnection;
    }

    public Task<int> AddTodoItemAsync(TodoItem todoItem)
    {
        const string command = "INSERT INTO TodoItems (Title, Note, TodoListId) VALUES (@Title, @Note, @TodoListId)";
        var parameters = new DynamicParameters();
        parameters.Add("Title", todoItem.Title, DbType.String);
        parameters.Add("Note", todoItem.Note, DbType.String);
        parameters.Add("TodoListId", todoItem.TodoListId, DbType.Int32);

        // Passing transaction to ExecuteAsync method
        return _connection.ExecuteAsync(command, parameters, _connectionProvider.GetTransaction);
    }

    public Task<IEnumerable<TodoItem>> GetTodoItemsAsync()
    {
        return _connection.ExecuteScalarAsync<IEnumerable<TodoItem>>("SELECT * FROM TodoItems");
    }
}

创建用于注册中间件的扩展方法:

public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseDbTransaction(this IApplicationBuilder app)
        => app.UseMiddleware<DbTransactionMiddleware>();
}

将中间件放在请求管道中 Authorization 中间件之后:

var builder = WebApplication.CreateBuilder(args);  
  
builder.Services.AddControllers();  
  
var app = builder.Build();  
  
app.UseAuthorization();  
  
app.UseDbTransaction()  
  
app.MapControllers();  
  
app.Run();

让我们进一步改进中间件实现。我们可以避免为 HTTP 请求打开事务。通常,对于请求,我们只获取数据,而在 和 中,我们修改数据。GETGETPOSTPUTDELETE

// For HTTP GET opening transaction is not required  
if (httpContext.Request.Method.Equals("GET", StringComparison.CurrentCultureIgnoreCase))  
{  
    await _next(httpContext);  
    return;  
}

有时,每个 , 和 请求 都不需要打开事务。我们可以通过使用 attribute 来装饰 action 来更准确地发起交易。POSTPUTDELETE

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class TransactionAttribute : Attribute
{
}

并使用 attribute 来装饰我们的 action:Transaction

[Transaction]
[HttpPost("todo-item")]
public async Task<IActionResult> Post(...)
{
    ...
}

这是 transaction 中间件的最终代码:

public class DbTransactionMiddleware
{
    private readonly RequestDelegate _next;

    public DbTransactionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, SqlConnectionProvider connectionProvider)
    {
        // For HTTP GET opening transaction is not required
        if (httpContext.Request.Method.Equals("GET", StringComparison.CurrentCultureIgnoreCase))
        {
            await _next(httpContext);
            return;
        }

        // If action is not decorated with TransactionAttribute then skip opening transaction
        var endpoint = httpContext.Features.Get<IEndpointFeature>()?.Endpoint;
        var attribute = endpoint?.Metadata.GetMetadata<TransactionAttribute>();
        if (attribute == null)
        {
            await _next(httpContext);
            return;
        }

        IDbTransaction transaction = null;

        try
        {
            transaction = connectionProvider.CreateTransaction();

            await _next(httpContext);

            transaction.Commit();
        }
        finally
        {
            transaction?.Dispose();
        }
    }
}

如果您使用的是 Entity Framework,则可以按以下方式打开和提交事务:

IDbContextTransaction transaction = null;
try
{
    transaction = await dbContext.Database.BeginTransactionAsync();

    await _next(httpContext);

    await transaction.CommitAsync();
}
finally
{
    if (transaction != null)
        await transaction.DisposeAsync();
}

源代码获取:公众号回复消息【code:31915

相关代码下载地址
重要提示!:取消关注公众号后将无法再启用回复功能,不支持解封!
第一步:微信扫码关键公众号“架构师老卢”
第二步:在公众号聊天框发送code:31915,如:code:31915 获取下载地址
第三步:恭喜你,快去下载你想要的资源吧
阅读排行