Transactions - lobodava/artisan-orm GitHub Wiki
Artisan.Orm offers two transaction patterns — manual commit and auto commit / rollback — plus async variants for both. Pick based on which outcome should be the default when the action returns:
| Method | Default outcome | Caller's responsibility |
|---|---|---|
BeginTransaction(...) |
rollback | call tran.Commit() to persist |
RunInTransaction(...) |
commit on success | nothing — handled automatically |
BeginTransactionAsync(...) |
rollback | await tran.CommitAsync(ct) |
RunInTransactionAsync(...) |
commit on success | nothing — handled automatically |
All four methods accept an optional IsolationLevel; when omitted, SQL Server uses its default (normally READ COMMITTED).
Inside action, any commands issued through GetByCommand / ExecuteCommand / Read* / Execute* on the same repository automatically attach to the started SqlTransaction.
Use when "do nothing" should mean "rolled back" — typical for read-then-modify flows that may decide partway through to abandon the change.
public void CheckRuleForUser(int userId)
{
BeginTransaction(IsolationLevel.RepeatableRead, tran =>
{
var user = GetByCommand(cmd =>
{
cmd.UseProcedure("dbo.GetUserById");
cmd.AddIntParam("@Id", userId);
return cmd.ReadTo<User>();
});
if (Array.IndexOf(user.RoleIds, 2) > -1 && Array.IndexOf(user.RoleIds, 3) == -1)
{
var newRoleIds = user.RoleIds.ToList();
newRoleIds.Add(3);
user.RoleIds = newRoleIds.ToArray();
}
ExecuteCommand(cmd =>
{
cmd.UseProcedure("dbo.SaveUser");
cmd.AddTableRowParam("@User", user);
cmd.AddTableParam("@RoleIds", user.RoleIds);
});
// Forgetting tran.Commit() here would roll back — by design.
tran.Commit();
});
}The method opens the connection if it was closed, begins the transaction, runs action, and on exit:
-
try— runsaction, -
catch— rolls back the transaction and rethrows, -
finally— disposes the transaction (which rolls back ifCommit()was not called) and closes the connection if it was opened by this method.
Use for the typical "do these steps; if any throws, undo all" flow.
public void TransferFunds(int fromAccountId, int toAccountId, decimal amount)
{
RunInTransaction(IsolationLevel.Serializable, tran =>
{
Execute("dbo.MoveFunds",
new SqlParameter("@From", fromAccountId),
new SqlParameter("@To", toAccountId),
new SqlParameter("@Amount", amount));
Execute("dbo.RecordTransfer",
new SqlParameter("@From", fromAccountId),
new SqlParameter("@To", toAccountId),
new SqlParameter("@Amount", amount));
// No tran.Commit() here — RunInTransaction commits automatically
// on normal return, and rolls back if action throws.
});
}If action throws, the transaction is rolled back and the exception is rethrown. The caller must not call Commit or Rollback manually inside action.
Both BeginTransactionAsync and RunInTransactionAsync accept a CancellationToken and pass it through to every awaited call inside.
public async Task TransferFundsAsync(int fromId, int toId, decimal amount,
CancellationToken ct = default)
{
await RunInTransactionAsync(IsolationLevel.Serializable, async (tran, ct2) =>
{
await ExecuteAsync("dbo.MoveFunds", ct2,
new SqlParameter("@From", fromId),
new SqlParameter("@To", toId),
new SqlParameter("@Amount", amount));
await ExecuteAsync("dbo.RecordTransfer", ct2,
new SqlParameter("@From", fromId),
new SqlParameter("@To", toId),
new SqlParameter("@Amount", amount));
}, ct);
}For the manual-commit async version:
await BeginTransactionAsync(async (tran, ct) =>
{
// ... reads / writes ...
await tran.CommitAsync(ct).ConfigureAwait(false);
}, cancellationToken);To allow an outer transaction (started in C#) to coexist with a procedure that may also want its own when called standalone, write each procedure to detect and respect an existing one:
create procedure dbo.SaveUser
...
as
begin
declare @StartTranCount int;
begin try
set @StartTranCount = @@trancount;
-- Begin transaction only when there is no outer transaction
if @StartTranCount = 0 begin transaction;
begin -- save Users
...
end;
begin -- save UserRoles
...
end;
-- Commit only when this proc started the transaction
if @StartTranCount = 0 commit transaction;
end try
begin catch
if xact_state() <> 0 and @StartTranCount = 0 rollback transaction;
declare @ErrorMessage nvarchar(4000) = dbo.GetErrorMessage();
raiserror (@ErrorMessage, 16, 1);
return;
end catch;
end;See also:
- RepositoryBase
- Using Artisan.Orm in ASP.NET Core — sharing transactions across services in one HTTP request
- SQL function
dbo.GetErrorMessage()