Skip to content

Instantly share code, notes, and snippets.

@impworks
Last active December 21, 2020 11:51
Show Gist options
  • Save impworks/877ba85dd1b7b3e968ca4b4964eedde6 to your computer and use it in GitHub Desktop.
Save impworks/877ba85dd1b7b3e968ca4b4964eedde6 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace TransactionHandlerTest
{
/// <summary>
/// Distributed transaction helper.
/// </summary>
public class Transaction: IAsyncDisposable
{
private Transaction(IEnumerable<DbConnection> connections)
{
Connections = connections.ToList();
Id = "t_" + Guid.NewGuid().ToString("N").Substring(0, 30);
}
private IReadOnlyList<DbConnection> Connections { get; }
private string Id { get; }
private bool? State { get; set; }
/// <summary>
/// Creates a transaction for the specified connections.
/// </summary>
public static async Task<Transaction> CreateAsync(IEnumerable<DbConnection> connections)
{
var txn = new Transaction(connections);
await txn.Begin();
return txn;
}
/// <summary>
/// Creates a new transaction for the specified EF DbContexts.
/// </summary>
public static Task<Transaction> CreateAsync(params DbContext[] contexts)
{
return CreateAsync(contexts.Select(x => x.Database.GetDbConnection()));
}
/// <summary>
/// Begins the transaction.
/// </summary>
private ValueTask Begin()
{
return SendCommandAsync($"BEGIN DISTRIBUTED TRANSACTION {Id}");
}
/// <summary>
/// Commits the transaction.
/// </summary>
public async ValueTask CommitAsync()
{
CheckState();
await SendCommandAsync("COMMIT TRANSACTION");
State = true;
}
/// <summary>
/// Discards any changes made in the transaction.
/// </summary>
public async ValueTask RollbackAsync()
{
CheckState();
await SendCommandAsync($"ROLLBACK TRANSACTION {Id}");
State = false;
}
/// <summary>
/// Rolls the transaction back if it has not been manually rolled back or committed.
/// </summary>
public async ValueTask DisposeAsync()
{
if (State != null)
return;
try
{
await RollbackAsync();
}
catch
{
// ignore all errors
}
}
/// <summary>
/// Dispatches a command to all connections.
/// </summary>
private async ValueTask SendCommandAsync(string sql)
{
foreach (var conn in Connections)
{
if (conn.State != ConnectionState.Open)
await conn.OpenAsync();
var cmd = conn.CreateCommand();
cmd.CommandText = sql;
cmd.CommandType = CommandType.Text;
await cmd.ExecuteNonQueryAsync();
}
}
/// <summary>
/// Ensures that the transaction is neither committed nor rolled back.
/// </summary>
private void CheckState()
{
if (State == true)
throw new Exception("Transaction is already committed!");
if (State == false)
throw new Exception("Transaction is already rolled back!");
}
public override string ToString()
{
return Id;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment