How can I execute multiple transactions under same connection? - c#

Here my requirement is like this:
Initially I execute a stored procedure to insert\update a record in DB, under one transaction.
In case the SP execution fails due to some issue, I must be able to re-invoke the SP again under different transaction.
Even if the re-invocation of SP fails then only, I should throw error to calling function.
Here is the sample code. Is this the proper way of handling transactions and errors, or is there a better way to do this?
public void InsertUser(string code)
{
bool bRetry=false;
SqlTransaction transaction = null;
Exception RetrunEx = null;
using (SqlConnection sqlConnection = new SqlConnection(this.connectionString))
{
sqlConnection.Open();
SqlCommand sqlCommand = new SqlCommand("InsertUser", sqlConnection);
sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
SqlParameter param = sqlCommand.Parameters.Add("#UserCode", SqlDbTypes.VarChar);
param.Value = code;
//Is this the proper place to begin a transaction?
SqlTransaction transaction = connection.BeginTransaction();
sqlCommand.Transaction = transaction;
try
{
sqlCommand.ExecuteNonQuery();
transaction.Commit();
}
catch(SqlException SqlEx)
{
transaction.Rollback();
bRetry = true;
RetrunEx = SqlEx;
}
catch(Exception ex)
{
transaction.Rollback();
RetrunEx = ex;
}
//Will this be treated as new transaction?
//Is there any best way of doing this?
transaction = connection.BeginTransaction();
sqlCommand.Transaction = transaction;
try
{
if (bRetry)
{
sqlCommand.ExecuteNonQuery();
transaction.Commit();
ReturnEx = null;
}
}
catch(Exception Ex)
{
transaction.Rollback();
RetrunEx = Ex;
}
//When both the trials fails then throw exception
if (RetrunEx != null)
{
throw RetrunEx;
}
}
}

The easiest way to work with transaction is to use System.Transaction
It is a very simple yet powerful api.
...
using (TransactionScope ts = new TransactionScope())
{
//Do Transactional Work
...
...
//Commit your transaction
ts.Complete();
}
You dont need no try/catch to rollback your transaction. If you exit the "using" with an exception, or without calling the "ts.Complete" statement, your transaction will be automatically rolled back.

I'd probably have a method that's only purpose is to try an execute this stored procedure. Pseudo Code:
public bool ExecuteSP()
{
try{
//open connection, begin tran execture sp
//commit transaction, return true;
}
catch(SqlException){
//rollback transaction
//return false
}
}
Then in your calling code you can just do something like this:
if(!ExecuteSP() && !ExecuteSP())
{
//throw Exception
}

Related

This SqlTransaction has completed; it is no longer usable. - Cannot figure out what is wrong

I have a piece of code that connects to the database to do logging; then calls a function and if that function is true, I call my database again to alter it.
If it is false, I return a System.Web.WebException which I need to handle.
However, no matter how I handle it, when it returns this exception, my 'transaction' return that it's already finished. I've tried removing the 2nd part of my code, so that's not my issue at all, but I'll include it so it makes more sense.
Before this revision, I used two catch statements and in each I had a rollback method, but converted it to one catch in order to see if that caused the issue, but apparently not.
public bool CreateNewLicense(LogInformation logInfo, string product, string email, string seats, out string responseQLM, out string responseLogger, out ILicenseInfo license)
{
bool status = false;
using (SqlConnection connection = new SqlConnection(System.Web.Configuration.WebConfigurationManager.ConnectionStrings["#REDACTED#"].ConnectionString))
{
connection.Open();
int logId = -1;
using (SqlCommand cmd = new SqlCommand(SQL, connection))
{
SqlTransaction transaction = connection.BeginTransaction(IsolationLevel.ReadCommitted);
cmd.Transaction = transaction;
#region Parameter definition from method input.
cmd.Parameters.Add("#ActionID", SqlDbType.Int).Value = logInfo.ActionId;
cmd.Parameters.Add("#ActionDesc", SqlDbType.VarChar).Value = string.IsNullOrEmpty(logInfo.ActionDesc) ? (object)DBNull.Value : logInfo.ActionDesc;
cmd.Parameters.Add("#Timestamp", SqlDbType.DateTime).Value = DateTime.Now;
cmd.Parameters.Add("#CustomerEmail", SqlDbType.VarChar).Value = string.IsNullOrEmpty(logInfo.CustomerEmail) ? (object)DBNull.Value : logInfo.CustomerEmail;
cmd.Parameters.Add("#KeyValueAfter", SqlDbType.VarChar).Value = string.IsNullOrEmpty(logInfo.KeyValueAfter) ? (object)DBNull.Value : logInfo.KeyValueAfter;
cmd.Parameters.Add("#LicenseKey", SqlDbType.VarChar).Value = string.IsNullOrEmpty(logInfo.LicenseKey) ? (object)DBNull.Value : logInfo.LicenseKey;
cmd.Parameters.Add("#GUID", SqlDbType.VarChar).Value = logInfo.Guid;
#endregion
try
{
logId = Convert.ToInt32(cmd.ExecuteScalar());
transaction.Commit();
responseLogger = "Successfully Logged";
if (!_licenseHandler.CreateNewLicense(product, email, seats, out responseQLM, out license))
throw new System.Net.WebException("Error Creating License");
responseQLM = "Successfully made a new License Key:";
}
catch (Exception e)
{
if (e.GetType() == typeof(System.Net.WebException))
{
responseQLM = e.Message;
responseLogger = "";
}
else
{
responseLogger = e.Message;
}
try
{
transaction.Rollback();
}
catch (SqlException sqlException)
{
responseLogger += #"\n" + sqlException.Message;
}
throw;
}
status = true;
if (logId >= 0)
{
string alterSQL = #"UPDATE [PLISLMTTraceback]
SET [LicenseKey] = #LicenseKey
WHERE [LogIndex] = #LogId; ";
using (SqlCommand alterCmd = new SqlCommand(alterSQL, connection))
{
SqlTransaction transactionTwo = connection.BeginTransaction(IsolationLevel.ReadCommitted);
alterCmd.Transaction = transactionTwo;
alterCmd.Parameters.Add("#LicenseKey", SqlDbType.VarChar).Value = string.IsNullOrEmpty(license.ActivationKey) ? (object)DBNull.Value : license.ActivationKey;
alterCmd.Parameters.Add("#LogId", SqlDbType.Int).Value = logId;
try
{
alterCmd.ExecuteNonQuery();
transactionTwo.Commit();
}
catch (Exception sqlAlterException)
{
responseLogger = "License Key Logging Failed: " + sqlAlterException.Message;
try
{
transactionTwo.Rollback();
}
catch (SqlException sqlException)
{
responseLogger += #"\n" + sqlException.Message;
}
throw;
}
}
}
}
}
return status;
}
}
I'd like for it to do the rollback after throwing that exception.
throw new System.Net.WebException("Error Creating License");
You are throwing an exception, and then trying to rollback a transaction that you already committed. This can't work. Once you have committed it, you can't roll it back.
I'd suggest one of two approaches.
Approach One:
Change your logic to create the licence key up front then do a single INSERT to the database. This will be faster, hold very few DB locks (and for a short window of time) and not require any use of explicit transactions (since you will be doing a single insert). The code will likely be substantially simpler.
Approach Two:
Remove all use of transactions. Insert the record (basically how you do now). Then, without keeping a DB connection / transaction open, create the licence. If the licence was created successfully, then run a DB update (of the record you just inserted). If the licence was not created successfully, then run a DB delete (of the record you just inserted).

System.InvalidOperationException -> DataReader associated with this Command [Commit of Transaction Failed]

I have such problem. I've added Transaction service to my SQL class. I get error, when I want to Commit query after its execution. I'll paste the code:
//SOME OF VARIABLES IN CLASS:
private SqlConnection connection;
private SqlCommand newQuery;
private SqlTransaction transaction;
private SqlDataReader result;
//BEGINNING TRANSACTION
public void BeginTransaction(string name)
{
try
{
transaction = connection.BeginTransaction(name);
}
catch
{
MessageBox.Show("Error while beginning transaction " + name);
}
}
//COMMIT TRANSACTION
public void Commit(string text)
{
try
{
transaction.Commit();
}
catch (Exception e)
{
MessageBox.Show("Couldn't commit transaction " + text + "\n\n" + e.ToString());
try
{
transaction.Rollback();
}
catch
{
MessageBox.Show("Couldn't Rollback transaction " + text);
}
}
transaction = null;
}
//EXECUTE QUERY METHOD
private SqlDataReader ExecuteQuery(string query)
{
try
{
if (connection.State == ConnectionState.Closed)
connection.Open();
if (result != null)
result.Close();
newQuery = connection.CreateCommand();
newQuery.CommandText = query;
newQuery.Transaction = transaction;
result = newQuery.ExecuteReader();
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
return result;
}
//EXAMPLE FUNCTION WITH TRANSACTION, WHICH OCCURS ERROR:
public bool DoesDatabaseExist(string dbName)
{
BeginTransaction("DoesDatabaseExist");
bool res = ExecuteQuery("SELECT * FROM master.dbo.sysdatabases WHERE name='" + dbName + "';").HasRows;
Commit("Does DB Exist 211");
return res;
}
Afrer running program, I get error, that Commit didn't pass. Like:
Couldn't commit transaction Does DB Exist 211
System.InvalidOperationException: There is already an open DataReader associated with this Command which must be closed first.
I'm studying programming in C# still, so probably it is easy to recognise error. But not for me. Please help.
Before I've added the transaction service, everything was ok, I didn't change or add any of queries or executions of queries. Please help.
Thanks, Mike.
The main issue is the way you are using your SqlDataReader instances.
Your first mistake is having it as a field of the class:
private SqlDataReader result;
Don't do that (please remove that line).
Then change the function to:
private SqlDataReader ExecuteQuery(string query)
{
try
{
if (connection.State == ConnectionState.Closed)
connection.Open();
newQuery = connection.CreateCommand();
newQuery.CommandText = query;
newQuery.Transaction = transaction;
var result = newQuery.ExecuteReader();
return result;
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
throw;
}
}
This ensures that every invocation of the above function returns its own instance of SqlDataReader.
OK, now the caller. You are currently using:
bool res = ExecuteQuery("SELECT * FROM master.dbo.sysdatabases WHERE name='" + dbName + "';").HasRows;
Commit("Does DB Exist 211");
This is incorrect, since ExecuteQuery is returning a SqlDataReader that you aren't cleaning up properly. Change it to:
var results = ExecuteQuery("SELECT * FROM master.dbo.sysdatabases WHERE name='" + dbName + "';");
using (results)
{
bool res = results.HasRows;
}
Commit("Does DB Exist 211");
The using will ensure that the SqlDataReader is disposed (closed) properly.
Note there are still a number of other remaining issues in your code. For example:
Your use of an explicit transaction is pointless, since you are executing a single SELECT statement
Your other fields (for Command, Connection and Transaction) are also a bad idea (like result was)
Your code is open to SQL Injection
You may wish to read up on Dapper

Will connection leak might Cause Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool?

I am receiving this error which cause my application to stop working. Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool.
This may have occurred because all pooled connections were in use and max pool size was reached.
However I have not reached my max connection pool. I have RDS and in my monitoring page, I found that number of connections was 33 at the time this error happens and my max one is 100 by default.
So, I was wondering that this could be due to a leak in my connection.
Here is the DBLayer class that I am using to connect to database.
public static DataTable GetDataTable(SqlCommand command, IsolationLevel isolationLevel = IsolationLevel.ReadUncommitted)
{
using (new LoggingStopwatch("Executing SQL " + command.CommandText, command.Parameters))
{
using (var connection = new SqlConnection(connectionString))
using (var dataAdapter = new SqlDataAdapter(command))
{
command.Connection = connection;
command.CommandTimeout = ShopexConfiguration.SqlTimeout;
connection.Open();
var transaction = connection.BeginTransaction(isolationLevel);
command.Transaction = transaction;
try
{
var result = new DataTable();
dataAdapter.Fill(result);
transaction.Commit();
return result;
}
catch
{
try
{
transaction.Rollback();
}
catch (Exception)
{
//
// This catch block will handle any errors that may have occurred
// on the server that would cause the rollback to fail, such as
// a closed connection.
}
throw;
}
}
}
}
I am just wondering, will this cause a connection leak?
I have seen this blog :
https://blogs.msdn.microsoft.com/spike/2008/08/25/timeout-expired-the-timeout-period-elapsed-prior-to-obtaining-a-connection-from-the-pool/
Any help is appreciated?
Are you sure, that your query don't reach execution timeout?
SqlConnection.ConnectionTimeout have default value 15 seconds
Also do you have some connection timeout values in your connectionString format is: "....;Connection Timeout=10..."
I think you should close your connection in your finally statement like below:
public static DataTable GetDataTable(SqlCommand command, IsolationLevel isolationLevel = IsolationLevel.ReadUncommitted)
{
using (new LoggingStopwatch("Executing SQL " + command.CommandText, command.Parameters))
{
using (var connection = new SqlConnection(connectionString))
using (var dataAdapter = new SqlDataAdapter(command))
{
command.Connection = connection;
command.CommandTimeout = ShopexConfiguration.SqlTimeout;
connection.Open();
var transaction = connection.BeginTransaction(isolationLevel);
command.Transaction = transaction;
try
{
var result = new DataTable();
dataAdapter.Fill(result);
transaction.Commit();
return result;
}
catch
{
try
{
transaction.Rollback();
}
catch (Exception)
{
//
// This catch block will handle any errors that may have occurred
// on the server that would cause the rollback to fail, such as
// a closed connection.
}
finally { connection.Close(); }
throw;
}
}
}
}
Maybe it solves your problem, because you are rollbacking your transaction in catch statement without closing your existing connection you can also use below condition before you are going to open your SQL connection :
if (connection.State == ConnectionState.Closed) {
// Open your connection here
connection.Open();
}
// Do your logic here
Hope it will helps you.
Thanks

How to rollback a transaction in Date Entity Framework

I am trying to use the Data Entity Framework to create my Data Access Layer. In the work I have done up to now I have used ADO.Net. I am trying to get my head around how transactions work in EF. I ave read loads but its all confussed me even more than I was before!! I would usually do somethink like (simplified for example):
using (SqlConnection conn = new SqlConnection(_connString))
{
using (SqlTransaction trans = conn.BeginTransaction())
{
try
{
using (SqlCommand cmd = new SqlCommand("usp_CreateNewInvoice", conn))
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("#InvoiceName", Invoice.invoicename));
cmd.Parameters.Add(new SqlParameter("#InvoiceAddess", Invoice.invoiceaddress));
_invoiceid = Convert.ToInt32(cmd.ExecuteScalar());
}
foreach (InvoiceLine inLine in Invoice.Lines)
{
using (SqlCommand cmd = new SqlCommand("usp_InsertInvoiceLines", conn))
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("#InvNo", _invoiceid));
cmd.Parameters.Add(new SqlParameter("#InvLineQty", inLine.lineqty));
cmd.Parameters.Add(new SqlParameter("#InvLineGrossPrice", inLine.linegrossprice));
cmd.ExecuteNonQuery();
}
}
trans.Commit();
}
catch (SqlException sqlError)
{
trans.Rollback();
}
}
}
Now I want to do the same in EF:
using (TransactionScope scope = new TransactionScope())
{
try
{
var DbContext = new CCTStoreEntities();
CCTInvoice invHead = new CCTInvoice();
invHead.Name = Invoice.invoicename;
invHead.Address = Invoice.invoiceaddress;
DbContext.CCTInvoices.Add(invHead);
DbContext.SaveChanges(false);
_iid = invHead.InvoiceId;
foreach (InvoiceLine inLine in Invoice.Lines)
{
CCTInvoiceLine invLine = new CCTInvoiceLine();
invLine.InvoiceNo = _iid;
invLine.Quantity = inLine.lineqty;
invLine.GrossPrice = inLine.linegrossprice;
DbContext.CCTInvoiceLines.Add(invHead);
DbContext.SaveChanges(false);
}
DbContext.SaveChanges();
scope.Complete();
}
catch
{
//Something went wrong
//Rollback!
}
}
From what I read SaveChanges(false) means the changes being made will continue to be tracked. But how do I rollback the transaction if something goes wrong?
You don't need to do anything on your catch block. Just by not calling DbContext.SaveChanges no changes will be sent to the database, and they will be lost once DbContext is disposed.
You do have a problem though. DbContext must be wrapped on a using block as follows to be properly disposed. BTW, I don't think DbContext.SaveChanges(false); is needed, your code should work with just the final DbContext.SaveChanges();. EF will take care of wiring up all your Foreign Keys, so you don't need to do that explicitly.
using (TransactionScope scope = new TransactionScope())
{
try
{
using (var DbContext = new CCTStoreEntities())
{
CCTInvoice invHead = new CCTInvoice();
invHead.Name = Invoice.invoicename;
invHead.Address = Invoice.invoiceaddress;
DbContext.CCTInvoices.Add(invHead);
DbContext.SaveChanges(false); // This is not needed
_iid = invHead.InvoiceId; // This is not needed
foreach (InvoiceLine inLine in Invoice.Lines)
{
CCTInvoiceLine invLine = new CCTInvoiceLine();
invLine.InvoiceNo = _iid; // This is not needed
invLine.Quantity = inLine.lineqty;
invLine.GrossPrice = inLine.linegrossprice;
DbContext.CCTInvoiceLines.Add(invHead);
DbContext.SaveChanges(false); // This is not needed
}
DbContext.SaveChanges();
scope.Complete();
}
}
catch
{
//Something went wrong
//Rollback!
}
}
The rollback mechanism in a TransactionScope is implicit.
Basically, if you don't call Complete before the TransactionScope gets disposed it will automatically rollback. See the Rolling back a transaction section in Implementing an Implicit Transaction using Transaction Scope.
So technically, you don't even need to use a try...catch here (unless you want to perform some other action like logging).

Difference between Transaction Scope and manually defined transactions?

Hi just reading about using transaction scrope, previously I am used to making transactions inside a single DB class like
try
{
con.Open();
tran = con.BeginTransaction();
OleDbCommand myCommand1 = new OleDbCommand(query1, con);
OleDbCommand myCommand2 = new OleDbCommand(query2, con);
myCommand .Transaction = tran;
// Save Master
myCommand1.ExecuteNonQuery();
// Save Childred
myCommand2.ExecuteNonQuery();
// Commit transaction
tran.Commit();
}
catch (OleDbException ex)
{
tran.Rollback();
lblError.Text = "An error occured " + ex.ToString();
}
finally
{
if (con != null)
{
con.Close();
}
}
But now I come to know that I can execute transaction inside the Business Logic layer simply by using a transaction scope object and using separate DB classes like
public static int Save(Employee myEmployee)
{
using (TransactionScope myTransactionScope = new TransactionScope())
{
int RecordId = EmpDB.Save(myEmployee);
foreach (Address myAddress in myEmployee.Addresses)
{
myAddress.EmployeeId = EmployeeId;
AddressDB.Save(myAddress);
}
foreach (PhoneNumber myPhoneNumber in myEmployee.PhoneNumbers)
{
myPhoneNumber.EmployeeId = EmployeeId;
PhoneNumberDB.Save(myPhoneNumber);
}
myTransactionScope.Complete();
return EmployeeId;
}
}
Which one is the recommended coding practice and why ? Is using a Transaction Scope safe ? Is it the latest way to do things ? I am confused about both methods.
Thanks in advance.
One of the nice things about Transaction scope is that you don't need the try/catch block. You just have to call Complete on the scope in order to commit the transaction, and it rollsback automatically if an exception does occur.
You can also use other components that are able to participate in transactions, not just the DB connection. This is because the components (including the connection) look for a Transaction on the current thread. It is this transaction that is created by the call to
using (TransactionScope myTransactionScope = new TransactionScope())

Categories

Resources