Framework for database transactions and connection to MS Sql Server
$ dotnet add package Plinth.Database.MSSqlStored Procedure based mini-framework for Microsoft SQL Server
Provides Transaction management, stored procedure execution, rowset handling, and transient error detection
// IConfiguration configuration;
var txnFactory = new SqlTransactionFactory(
configuration,
"MyDB",
config.GetConnectionString("MyDB"));
services.AddSingleton(txnFac); // for injecting the SqlTransactionFactory
services.AddSingleton(txnFac.GetDefault()); // for injecting the ISqlTransactionProvider
Example appsettings.json
:point_right: All settings in PlinthMSSqlSettings are optional. The defaults are shown below.
{
"ConnectionStrings": {
"MyDB": "Data Source=...."
},
"PlinthMSSqlSettings": {
"SqlCommandTimeout": "00:00:50",
"SqlRetryCount": 3,
"SqlRetryInterval": "00:00:00.200",
"SqlRetryFastFirst": true,
"DisableTransientRetry": false
}
}
TimeSpan formatted time for the default time for each SQL operation. Default is 50 seconds.true, upon the first transient error, the first retry will happen immediately. Subsequent transient errors will wait the SqlRetryInterval. Default is true.true, transient errors will not trigger retries. Default is false.It is very common on cloud hosted databases (especially on Azure SQL) to have the database return transient errors that will work perfectly if retried. These errors can be things like deadlocks, timeouts, throttling, and transport errors.
The framework accepts a function to execute the whole transaction. When a transient error occurs, the entire transaction is rolled back and the function is executed again.
:warning: Your code inside a transaction should be re-entrant. Anything that is performed that cannot be rolled back (such as sending an email), should be performed outside the transaction or be checked to confirm that it won't execute more than once.
Below is an example controller that creates a transaction, executes a stored procedure, and returns the result.
[Route("api/[controller]")]
[ApiController]
public class MyThingController : Controller
{
private readonly ISqlTransactionProvider _txnProvider;
public MyThingController(ISqlTransactionProvider _txnProvider)
{
_txnProvider = txnProvider;
}
[HttpGet]
[Route("{thingId}")]
[ProducesResponseType(200)]
public async Task<ActionResult<MyThing>> GetMyThing(Guid thingId, CancellationToken ct)
{
var myThing = await _txnProvider.ExecuteTxnAsync(connection =>
{
return await connection.ExecuteQueryProcOneAsync(
"usp_GetMyThingById",
row => Task.FromResult(new MyThing
{
Field1 = row.GetInt("Field1"),
Filed2 = row.GetDateTimeNull("Field2")
... etc
}),
new SqlParameter("@ThingID", thingId)).Value;
}, ct);
if (myThing is null)
throw new LogicalNotFoundException($"MyThing {thingId} was not found");
return Ok(myThing);
}
}
To execute a stored procedure that does not return a result set, use one of these three options. Typically used with DML procedures that insert/update/delete.
:point_right: All forms also have an overload that accepts a CancellationToken
ExecuteProcAsync(string procName, params SqlParameter[] parameters)
ExecuteProcAsync(string procName, int expectedRows, params SqlParameter[] parameters)
expectedRowsExecuteProcUncheckedAsync(string procName, params SqlParameter[] parameters)
To execute a stored procedure returns a result set, use one of these three options. Typically used with SELECT queries.
:point_right: All forms also have an overload that accepts a CancellationToken
ExecuteQueryProcAsync(string procName, params SqlParameter[] parameters)
IAsyncEnumerable<IResult> which can be enumerated to extract objects from rows.ExecuteQueryProcListAsync<T>(string procName, Func<IResult, Task<T>> readerFunc, params SqlParameter[] parameters)
List<T> of objects returned from the Func called on each row returned.List<T> that may be empty.ExecuteQueryProcOneAsync(string procName, Func<IResult, Task> readerFunc, params SqlParameter[] parameters)
Func with a single row result (if one found), returns true/false if row was found.ExecuteQueryProcOneAsync<T>(string procName, Func<IResult, Task<T>> readerFunc, params SqlParameter[] parameters)
Func with a single row result and returns the output inside a SqlSingleResult<T> object..Value to get the result and .RowReturned to determine if a row was returned.SetRollback() has been called on this transactionThese allow you to have code execute after a rollback or a commit occurs. Useful for cleaning up non-transaction items or taking actions after database operations are committed.
Post Rollback Actions:
AddRollbackAction(string? desc, Action onRollback)
AddAsyncRollbackAction(string? desc, Func<Task> onRollbackAsync)
Post Commit Actions:
AddPostCommitAction(string? desc, Action postCommit)
AddAsyncPostCommitAction(string? desc, Func<Task> postCommitAsync)
Some stored procedures can actually return multiple result sets in a single call.
To execute and process each result set, use this method:
ExecuteQueryProcMultiResultSetAsync(string procName, Func<IAsyncMultiResultSet, Task> readerFunc, params SqlParameter[] parameters)
Example
await c.ExecuteQueryProcMultiResultSetAsync(
"usp_GetMultipleResults",
async (mrs) =>
{
var rs = await mrs.NextResultSetAsync();
await processSet1(rs);
rs = await mrs.NextResultSetAsync();
await processSet2(rs);
rs = await mrs.NextResultSetAsync();
await processSet3(rs);
},
new SqlParameter("@Int1", 10));This allows for recording a sequence of stored procedure calls (without actually executing them) and then executing them all at one at a later time.
Example:
var deferred = _txnProvider.GetDeferred();
// no sql actions occur
deferred.ExecuteProc("usp_InsertThing". new SqlParameter("@ID", 5));
deferred.ExecuteProc("usp_InsertThing". new SqlParameter("@ID", 10));
await _txnProvider.ExecuteTxnAsync(connection =>
{
// now the sql actions are executed
await connection.ExecuteDeferredAsync(deferred);
});
Normal transactions as shown above only allow for executing stored procedures. There are times and cases where executing a raw SQL statement is required. To do so, use ExecuteRawTxnAsync as shown in the below example:
var myThing = await _txnProvider.ExecuteRawTxnAsync(connection =>
{
return await connection.ExecuteRawQueryOneAsync(
"SELECT Field1, Field2 FROM MyThings WHERE ThingID = @ThingID",
row => Task.FromResult(new MyThing
{
Field1 = row.GetInt("Field1"),
Filed2 = row.GetDateTimeNull("Field2")
... etc
}),
new SqlParameter("@ThingID", thingId)).Value;
}, ct);The methods are analogues of the methods in sections 5, 6 and 9.
ExecuteRawAsync for DMLExecuteRawQueryListAsync for queries that return a list of resultsExecuteRawQueryOneAsync for queries that return a single resultExecuteRawQueryMultiResultSetAsync for queries that return multiple result sets