 
                        在某些涉及 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 获取下载地址