在某些涉及 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
】