How can I use transactional operation between multiple database with Entity framework core ((2.1))? (Distributed Transaction)
try
{
using (var tranScope = new TransactionScope())
{
using (var ctx1 = new TestDBContext())
using (var ctx2 = new TestDB2Context())
{
ctx1.Person.Add(new Person { Name = "piran" });
ctx2.Course.Add(new Course { Name = "C#" });
ctx1.SaveChanges();
ctx2.SaveChanges();
}
tranScope.Complete();
}
}
catch(Exception ex)
{
Debug.WriteLine(ex.Message);
}
When running above code I give this exception:
This platform does not support distributed transactions
Related
What does the Rollback method in EF Core do? If I didn't use Commit, I don't need it anyway. If I used Commit, the transaction has already been completed.
using (var context = new AppDbContext())
{
using (var transaction = context.Database.BeginTransaction())
{
try
{
var myObjectOne = new MyObjectOne() { Name = "Book" };
context.MyObjectOnes.Add(myObjectOne);
context.SaveChanges();
var myVal = myObjectOne.Id * 3.14;
var myObjectTwo = new MyObjectTwo() { Name = "Notebook", Price = 100, ReferenceId = myVal };
context.MyObjectTwos.Add(myObjectTwo);
context.SaveChanges();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
}
}
}
What does the RollBack Method do? C# EF Core.
For example I am adding the peoples data to database per state (this is not what I am doing exactly but the model is same). We have list of states and each state has millions of people. So initially in code, I am saving the state to get the State ID and then use that ID to bulk insert peoples data.
If something goes wrong while adding the peoples data, let's say 20th million record threw some exception, is there a way to revert back the data already saved in both Peoples and State table?
Any suggestion is highly appreciated..
List <Peoples> PeopleList = new List<Peoples>();
int peopleCounter = 0;
foreach (var stateVal in States)
{
using (var context = new StateEntities())
{
State st = new State();
st.ID = stateVal.ID;
st.Name = stateVal.Name;
context.State.Add(st);
context.SaveChanges();
if (stateVal.Peoples != null )
{
foreach (var _p in stateVal.Peoples)
{
Peoples _people = new Peoples();
_people.Name = _p.Name;
_people.Age = _P.Age;
_people.State_ID = stateVal.ID; // Getting state ID from State object as it already saved to DB
PeopleList.Add(_people)
peopleCounter++;
if (peopleCounter == 100000)
{
InsertPeople(PeopleList, context); // does bulk insert when PeopleList reaches 100k
PeopleList.Clear();
peopleCounter = 0;
}
}
}
}
}
private static void InsertPeople(List<Peoples> PeopleList, StateEntities context)
{
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required, new System.TimeSpan(0, 30, 0)))
{
context.BulkInsert(PeopleList, options => options.BatchTimeout = 0);
context.SaveChanges();
transactionScope.Complete();
}
}
You can use transaction of SQL to rollback. It's supported by EF.
using (var context = new SchoolContext())
{
using (DbContextTransaction transaction = context.Database.BeginTransaction())
{
try
{
//**TODO: Do your bulk insert codes here**
// Save changes data in context
context.SaveChanges();
// Commit changes
transaction.Commit();
}
catch (Exception ex)
{
// Rollback all changes
transaction.Rollback();
}
}
}
Ref: https://learn.microsoft.com/en-us/ef/core/saving/transactions
I currently have two classes in one layer, which perform the inclusion of data in the database:
using Dapper;
using System;
using System.Data.SqlClient;
using System.Linq;
namespace repositories
{
public class DAOBook
{
private readonly string _connection;
public DAOBook(string databaseConnection)
{
_connection = databaseConnection;
}
public bool IncludeBook(string title)
{
try
{
using (var connection = new SqlConnection(_connection))
{
var sql = $#"
INSERT INTO books
(title)
VALUES
('{title}' ";
var result = connection.Execute(sql);
return result != 0;
}
}
catch (Exception ex)
{
throw new Exception($"{ex.Message}", ex);
}
}
}
}
using Dapper;
using System;
using System.Data.SqlClient;
using System.Linq;
namespace repositories
{
public class DAOBookTag
{
private readonly string _connection;
public DAOBookTag(string databaseConnection)
{
_connection = databaseConnection;
}
public bool IncludeBookTag(string tag, int userid)
{
try
{
using (var connection = new SqlConnection(_connection))
{
var sql = $#"
INSERT INTO bookTag
(tag, userid)
VALUES
('{tag}', {userid} ";
var result = connection.Execute(sql);
return result != 0;
}
}
catch (Exception ex)
{
throw new Exception($"{ex.Message}", ex);
}
}
}
}
In my service layer, I can call these two classes normally, and they insert them into the database.
try
{
var connectionString = "<my_connection_string>";
var daoBook = new DAOBook(connectionString);
var daoBookTag = new DAOBookTag(connectionString);
dao.IncludeBook("Alice");
dao.IncludeBookTag("Romance", 1);
}
catch (Exception ex)
{
throw new Exception($"{ex.Message}", ex);
}
However, I want to place a transaction control, so that in case of an error in the insertion of the second class, it undoes the transaction in catch, something like this:
try
{
var connectionString = "<my_connection_string>";
var daoBook = new DAOBook(connectionString);
var daoBookTag = new DAOBookTag(connectionString);
// begin transaction
dao.IncludeBook("Alice");
dao.IncludeBookTag("Romance", 1);
// commit
}
catch (Exception ex)
{
// rollback
throw new Exception($"{ex.Message}", ex);
}
I know it must be a beginner's question, but I can't seem to find a way for the two persistence classes to share the same transaction.
I saw an example of implementing Dapper's transaction control, but I don't know how I could implement it in my service layer (instead of the persistence layer).
https://riptutorial.com/dapper/example/22536/using-a-transaction
Thank you
There are two ways of handling transactions in ADO.NET; the usually preferred mechanism is an ADO.NET transaction, i.e. BeginTransaction. This has limitations, but is very efficient and maps natively into most providers. The key restriction of an ADO.NET transaction is that it only spans one connection, and your connection must last at least as long as the transaction.
In terms of Dapper usage, you must also pass the transaction into the call; for example:
using (var conn = new SqlConnection(connectionString))
{
connection.Open();
using (var tran = connection.BeginTransaction())
{
// ... your work
tran.Commit();
}
}
where "your work" here effectively uses the same conn and tran instances, using:
var result = conn.Execute(sql, args, transaction: tran);
The much lazier way is to use TransactionScope. This is simpler to use, but
more more involved. I usually advise against it, but it works.
You should also parameterize:
var sql = #"
INSERT INTO bookTag (tag, userid)
VALUES (#tag, #userId)";
var result = connection.Execute(sql, new { tag, userId });
Use a TransactionScope:
using (var transactionScope = new TransactionScope())
{
var connectionString = "<my_connection_string>";
var daoBook = new DAOBook(connectionString);
var daoBookTag = new DAOBookTag(connectionString);
// begin transaction
dao.IncludeBook("Alice");
dao.IncludeBookTag("Romance", 1);
//commit
transactionScope.Complete();
}
https://dapper-tutorial.net/transaction
I am extracting content of the Files in SQL File Table. The following code works if I do not use Parallel.
I am getting the following exception, when reading sql file stream simultaneously (Parallel).
The process cannot access the file specified because it has been opened in another transaction.
TL;DR:
When reading a file from FileTable (using GET_FILESTREAM_TRANSACTION_CONTEXT) in a Parallel.ForEach I get the above exception.
Sample Code for you to try out:
https://gist.github.com/NerdPad/6d9b399f2f5f5e5c6519
Longer Version:
Fetch Attachments, and extract content:
var documents = new List<ExtractedContent>();
using (var ts = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
var attachments = await dao.GetAttachmentsAsync();
// Extract the content simultaneously
// documents = attachments.ToDbDocuments().ToList(); // This works
Parallel.ForEach(attachments, a => documents.Add(a.ToDbDocument())); // this doesn't
ts.Complete();
}
DAO Read File Table:
public async Task<IEnumerable<SearchAttachment>> GetAttachmentsAsync()
{
try
{
var commandStr = "....";
IEnumerable<SearchAttachment> attachments = null;
using (var connection = new SqlConnection(this.DatabaseContext.Database.Connection.ConnectionString))
using (var command = new SqlCommand(commandStr, connection))
{
connection.Open();
using (var reader = await command.ExecuteReaderAsync())
{
attachments = reader.ToSearchAttachments().ToList();
}
}
return attachments;
}
catch (System.Exception)
{
throw;
}
}
Create objects for each file:
The object contains a reference to the GET_FILESTREAM_TRANSACTION_CONTEXT
public static IEnumerable<SearchAttachment> ToSearchAttachments(this SqlDataReader reader)
{
if (!reader.HasRows)
{
yield break;
}
// Convert each row to SearchAttachment
while (reader.Read())
{
yield return new SearchAttachment
{
...
...
UNCPath = reader.To<string>(Constants.UNCPath),
ContentStream = reader.To<byte[]>(Constants.Stream) // GET_FILESTREAM_TRANSACTION_CONTEXT()
...
...
};
}
}
Read the file using SqlFileStream:
Exception is thrown here
public static ExtractedContent ToDbDocument(this SearchAttachment attachment)
{
// Read the file
// Exception is thrown here
using (var stream = new SqlFileStream(attachment.UNCPath, attachment.ContentStream, FileAccess.Read, FileOptions.SequentialScan, 4096))
{
...
// extract content from the file
}
....
}
Update 1:
According to this article it seems like it could be an Isolation level issue. Has anyone ever faced similar issue?
The transaction does not flow in to the Parallel.ForEach, you must manually bring the transaction in.
//Switched to a thread safe collection.
var documents = new ConcurrentQueue<ExtractedContent>();
using (var ts = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
var attachments = await dao.GetAttachmentsAsync();
//Grab a reference to the current transaction.
var transaction = Transaction.Current;
Parallel.ForEach(attachments, a =>
{
//Spawn a dependant clone of the transaction
using (var depTs = transaction.DependentClone(DependentCloneOption.RollbackIfNotComplete))
{
documents.Enqueue(a.ToDbDocument());
depTs.Complete();
}
});
ts.Complete();
}
I also switched from List<ExtractedContent> to ConcurrentQueue<ExtractedContent> because you are not allowed call .Add( on a list from multiple threads at the same time.
I have some tasks (nWorkers = 3):
var taskFactory = new TaskFactory(cancellationTokenSource.Token,
TaskCreationOptions.LongRunning, TaskContinuationOptions.LongRunning,
TaskScheduler.Default);
for (int i = 0; i < nWorkers; i++)
{
var task = taskFactory.StartNew(() => this.WorkerMethod(parserItems,
cancellationTokenSource));
tasks[i] = task;
}
And the following method called by the tasks:
protected override void WorkerMethod(BlockingCollection<ParserItem> parserItems,
CancellationTokenSource cancellationTokenSource)
{
//...log-1...
using (var connection = new OracleConnection(connectionString))
{
OracleTransaction transaction = null;
try
{
cancellationTokenSource.Token.ThrowIfCancellationRequested();
connection.Open();
//...log-2...
transaction = connection.BeginTransaction();
//...log-3...
using (var cmd = connection.CreateCommand())
{
foreach (var parserItem in parserItems.GetConsumingEnumerable(
cancellationTokenSource.Token))
{
cancellationTokenSource.Token.ThrowIfCancellationRequested();
try
{
foreach (var statement in this.ProcessRecord(parserItem))
{
cmd.CommandText = statement;
try
{
cmd.ExecuteNonQuery();
}
catch (OracleException ex)
{
//...log-4...
if (!this.acceptedErrorCodes.Contains(ex.Number))
{
throw;
}
}
}
}
catch (FormatException ex)
{
log.Warn(ex.Message);
}
}
if (!cancellationTokenSource.Token.IsCancellationRequested)
{
transaction.Commit();
}
else
{
throw new Exception("DBComponent has been canceled");
}
}
}
catch (Exception ex)
{
//...log-5...
cancellationTokenSource.Cancel();
if (transaction != null)
{
try
{
transaction.Rollback();
//...log-6...
}
catch (Exception rollbackException)
{
//...log-7...
}
}
throw;
}
finally
{
if (transaction != null)
{
transaction.Dispose();
}
connection.Close();
//...log-8...
}
}
//...log-9...
}
There is a producer of ParserItem objects and these are the consumers. Normally it works fine, there are sometimes that there is an Oracle connection timeout, but in these cases I can see the exception message and everything works as designed.
But sometimes the process get stuck. When it gets stuck, in the log file I can see log-1 message and after that (more or less 15 seconds later) I see log-8 message, but what is driving me nuts is why i cannot see neither the exception message log-5 nor the log-9 message.
Since the cancellationTokenSource.Cancel() method is never called, the producer of items for the bounded collection is stuck until a timeout two hours later.
It is compiled for NET Framework 4 and I'm using Oracle.ManagedDataAccess libraries for the Oracle connection.
Any help would be greatly appreciated.
You should never dispose a transaction or connection when you use using scope. Second, you should rarely rely on exception based programming style. Your code rewritten below:
using (var connection = new OracleConnection(connectionString))
{
using (var transaction = connection.BeginTransaction())
{
connection.Open();
//...log-2...
using (var cmd = connection.CreateCommand())
{
foreach (var parserItem in parserItems.GetConsumingEnumerable(cancellationTokenSource.Token))
{
if (!cancellationTokenSource.IsCancellationRequested)
{
try
{
foreach (var statement in ProcessRecord(parserItem))
{
cmd.CommandText = statement;
try
{
cmd.ExecuteNonQuery();
}
catch (OracleException ex)
{
//...log-4...
if (!acceptedErrorCodes.Contains(ex.ErrorCode))
{
log.Warn(ex.Message);
}
}
}
}
catch (FormatException ex)
{
log.Warn(ex.Message);
}
}
}
if (!cancellationTokenSource.IsCancellationRequested)
{
transaction.Commit();
}
else
{
transaction.Rollback();
throw new Exception("DBComponent has been canceled");
}
}
}
}
//...log-9...
Let me know if this helps.
I can confirm everything you're saying. (program stuck, low CPU usage, oracle connection timeouts, etc.)
One workaround is to use Threads instead of Tasks.
UPDATE: after careful investigation I found out that when you use a high number of Tasks, the ThreadPool worker threads queued by the Oracle driver become slow to start, which ends up causing a (fake) connect timeout.
A couple of solutions for this:
Solution 1: Increase the ThreadPool's minimum number of threads, e.g.:
ThreadPool.SetMinThreads(50, 50); // YMMV
OR
Solution 2: Configure your connection to use pooling and set its minimum size appropriately.
var ocsb = new OracleConnectionStringBuilder();
ocsb.DataSource = ocsb.DataSource;
ocsb.UserID = "myuser";
ocsb.Password = "secret";
ocsb.Pooling = true;
ocsb.MinPoolSize = 20; // YMMV
IMPORTANT: before calling any routine that creates a high number of tasks, open a single connection using that will "warm-up" the pool:
using(var oc = new OracleConnection(ocsb.ToString()))
{
oc.Open();
oc.Close();
}
Note: Oracle indexes the connection pools by the connect string (with the password removed), so if you want to open additional connections you must use always the same exact connect string.