I have just read about TransactionScope. It is very good and informative.
First of all, I was wondering if I really need transactions in MVC 4 / EF 6+. The reason for that is we always invoke DbContext.SaveChanges() to save changes. I'm wondering if SaveChanges() is something that simulates transaction close meaning if I invoke SaveChanges() I commit a transactions.
On the other hand, if I need transactions, then how to implement TransactionScope in MVC / EF applications. My scenario is something similar to the steps below:
I save valid record in database
I save a copy of an old and a new record in another table which is sort of archived version of the original table
I save user's activity in another table
I also provided code. As you can see if something goes wrong in the middle I have inconsistent data. I would be grateful for some examples on how to use TransactionScope. I may also need more to save in other tables. I would like to be certain either I save everything or nothing, such that I save everything if transaction is OK or roll back anything that happened up to the problem.
Thanks.
[HttpPost]
public ActionResult Edit(ApplicationViewModel viewmodel)
{
using(MyDbCOntext dbContext = new MyDbContext())
{
if(!MoselState.IsValid)
return View(application);
// Copy old data from database and assign to an object
ApplicationArchive applicationOld = CopyApplicationFromDB(db, viewmodel.ApplicationID);
// Update model
if (TryUpdateModel(applicationNew), null, null, new string[] { "ApplicationID" })
{
try
{
dbContext.Entry(userToUpdate).State = EntityState.Modified;
dbContext.SaveChanges();
// Archive old application
ApplicationArchive applicationNew = CopyApplicationFromDB(db, viewmodel.ApplicationID);
try
{
dbContext.ApplicationsArchive.Add(applicationOld);
dbCOntext.ApplicationsArchive.Add(applicationNew);
dbContext.SaveChanges();
// Register user activity
string username = GetUserNameFromCookie();
UserActivity useractivity = new UserActivity() { UserName = username, activity = "edit", Table = "application" };
try
{
dbContext.UserActivities.Add(useractivity);
dbContext.SaveChanges();
return RedirectView("Index");
}
}
}
catch
{
ModelState.AddModelError("", "Cannot update this application");
}
}
//
return View(application);
}
}
You need to wrap your database operation within a DbContextTransaction. See this link for Entity Framework transaction examples:
https://msdn.microsoft.com/en-us/data/dn456843.aspx
Related
The author of this article claimed that to delete an entity, it isn't necessary to receive that entity once and then delete it with another query:
{
var person = await db.Persons.FindAsync(id); // 1 database call
if (person is not null) db.Remove(person);
await db.SaveChangesAsync(); // 1 database call
// 2 total database calls
}
But it is better to refer to the database only once with the following method to delete an entity.
try
{
var person = db.Persons.Attach(new Person { Id = id });
person.State = EntityState.Deleted;
await db.SaveChangesAsync(); // 1 database call
}
catch (DbUpdateConcurrencyException e)
{
Console.WriteLine(e);
// will happen if record no longer exists
}
Well, that seems to be, the only problem that may arise with this method is the chance that another process or client request has already deleted the record, Of course, he has admitted that this problem can be handled with DbUpdateConcurrencyException, However, Is this the best way to delete an entity from database? Is it really possible to trust this method in all cases, and does it not cause any other problems?
I am currently using .Net6 and earlier in my project.
It is using Entity Framework Core to update database.
dbContextTransaction.Commit(); is working fine, and after this it is some file operation with bad result. And then it throws an error, so it tries to roll back using dbContextTransaction.Rollback(); but results in:
This SqlTransaction has completed; it is no longer usable.
DbContext dbContext = scope.ServiceProvider.GetService<DbContext>();
IDbContextTransaction dbContextTransaction = dbContext.Database.BeginTransaction();
try
{
IOrderDao orderDao = scope.ServiceProvider.GetService<IOrderDao>();
IItemSoldPriceDao itemSoldPriceDao = scope.ServiceProvider.GetService<IItemSoldPriceDao>();
ItemSoldPrice itemSoldPrice = new ItemSoldPrice
{
...
};
itemSoldPriceDao.AddItemSoldPrice(itemSoldPrice);
order.SoldPriceCaptured = true;
dbContext.SaveChanges();
dbContextTransaction.Commit();
//... some other file operation throws out error
throw new Exception("aaa");
}
catch (Exception ex)
{
CommonLog.Error("CaptureSoldPrice", ex);
dbContextTransaction.Rollback();
}
After a transaction is committed, it cannot be rolled back?
When using Entity Framework, explicit transactions are only required when you want to link the success or failure of operations against the DbContext with other operations outside of the scope of the DbContext. All operations within a DbContext prior to SaveChanges are already grouped within a transaction. So for instance saving entities across two or more tables within a DbContext do not require setting up an explicit transaction, they will both be committed or rolled back together if EF cannot save one or the other.
When using an explicit transaction, the Commit() call should be the last operation for what forms essentially a unit of work. It will be the last operation to determine whether everything in the transaction scope is successful or not. So as a general rule, all operations, whether Database-based, file based, or such should register with and listen to the success or failure of the transaction.
An example of using a transaction: Say we have a system that accesses two databases via two separate DbContexts. One is an order system that tracks orders and has a record for a Customer and one is a CRM that tracks customer information. When we accept a new order from a new customer we check the CRM system for a customer and create a customer record in both systems if it is someone new.
using (var orderContext = new OrderDbContext())
{
var transaction = orderContext.Database.BeginTransaction();
try
{
var order = new Order
{
// TODO: Populate order details..
}
if(customerDetails == null && registerNewCustomer) // Where customerDetails = details loaded if an existing customer logged in and authenticated...
{
using(var crmContext = new CrmDbContext())
{
crmContext.Database.UseTransaction(transaction);
var customer = new Customer
{
UserName = orderDetails.CustomerEmailAddress,
FirstName = orderDetails.CustomerFirstName,
LastName = orderDetails.CustomerLastName,
Address = orderDetails.BillingAddress
};
crmContext.Customers.Add(customer);
crmContext.SaveChanges();
var orderCustomer = new Orders.Customer
{
CustomerId = customer.CustomerId,
FirstName = customer.FirstName,
LastName = customer.LastName
}
orderContext.Customers.Add(orderCustomer);
}
}
order.CustomerId = crmContext.Customers
.Select(c => c.CustomerId)
.Single(c => c.UserName == customerDetails.UserName);
orderContext.Orders.Add(order);
orderContext.SaveChanges();
transaction.Commit();
}
catch(Exception ex)
{
// TODO: Log exception....
transaction.Rollback();
}
}
The order DB customer is just a thin wrapper of the CRM customer where we would go for all of the customer details. The CRM customer manages the Customer IDs which would correlate to an Order Customer record. This is by no means a production code type example, but rather just to outline how a Transaction might coordinate multiple operations when used.
In this way if there is any exception raised at any point, such as after a new Customer record has been created and saved, all saved changes will be rolled back and we can inspect any logged exception details along with recorded values to determine what went wrong.
When dealing with combinations of DbContext operations and other operations that we might want to support a rolling back process flow on failure then we can leverage constructs like the TransactionScope wrapper. However this should be used with caution and only in cases where you explicitly need to marry these operations rather than attempting to use the pattern as a standard across all operations. In most cases you will not need explicit transactions.
I try to understand how EF creates DB requests from object manipulations in code. My test scenario is simple:
using(var context = new Context())
{
var entity = context.Entities.First();
entity.A = "TST";
entity.B = "WrongValue";
context.SaveChanges();
}
My idea is to test, how EF deal with transaction.
Change A with correct value and change B with wrong value (non existing FK)
I track what happening in SQL Server DB.
I execute code and nothing change in DB, that was expected.
Strange part is that there are two independant SQL request and I don't understand how EF revert first one.
Both Entity Framework and EntityFramework core code are open source. You can check the code at
Entity Framework - Link
Entity Framework Core - Link
If you see the internal code of Save method (pasted the code snapshot below) then you can validate that it internally it creates a transaction if an external transaction is not provided.
internal int SaveChangesInternal(SaveOptions options, bool executeInExistingTransaction)
{
AsyncMonitor.EnsureNotEntered();
PrepareToSaveChanges(options);
var entriesAffected = 0;
// if there are no changes to save, perform fast exit to avoid interacting with or starting of new transactions
if (ObjectStateManager.HasChanges())
{
if (executeInExistingTransaction)
{
entriesAffected = SaveChangesToStore(options, null, startLocalTransaction: false);
}
else
{
var executionStrategy = DbProviderServices.GetExecutionStrategy(Connection, MetadataWorkspace);
entriesAffected = executionStrategy.Execute(
() => SaveChangesToStore(options, executionStrategy, startLocalTransaction: true));
}
}
ObjectStateManager.AssertAllForeignKeyIndexEntriesAreValid();
return entriesAffected;
}
So your below code will internally wrapped inside a transaction which you can validate in SQL Profiler..
using(var context = new Context())
{
var entity = context.Entities.First();
entity.A = "TST";
entity.B = "WrongValue";
context.SaveChanges();
}
However, SQL profiler does not start logging transaction so you need to configure that in trace setting. See the below screenshot of SQL profiler new Trace setting, here, I have checked Show All events. After that Transaction category is being displayed. You can subscribe for Begin Tran, Commit Tran and Rollback Tran events to validate transaction statements. When you will run your scenario, you can see that Begin and Rollback should be logged.
I have landed up in a situation here.
I am loading the User from a custom function here :
public User GetUserById(int Id)
{
var User = DBContext.Users.SingleOrDefault(x=>x.Id == Id);
return User;
}
The problem is in the controller where I call this!
var User = GetUserById(Id);
User.Language = UserModel.Language;
//UpdateModel(User); //I tried this but not working.
DBContext.SaveChanges();
The database is not being updated.
I tried loading the User in the controller directly with linq and things go fine.
But, Why is it not working the other way?
Is there any workaround to make it work with this function?
Isn't there any function like DBContext.Users.Update(User). I remember using something similar, but am not able to recollect!
As EBrown said, they need to be the same instance of DbContext. I'm betting your service uses one context and the controller another. So now when it gets to the controller it is disconnected and not tracked by EF.
The solution is to reconnect it to the context used by the controller (there are other ways to achieve this as well).
var User = GetUserById(Id); // returns a disconnected user
User.Language = UserModel.Language;
// Attach back to context and tell EF it is updated
DBContext.Users.Attach(User);
DBContext.Entity(User).State=EntityState.Modified;
DBContext.SaveChanges();
If this is your postback code, you could just as well write aUserUpdate function:
public void UpdateUser(UserModel userViewModel)
{
var userEntity = DBContext.Users.Find(userViewModel.Id); // Get your user from database
Mapper.Map(userViewModel, userEntity); // Use Automapper to move the changed fields into your entity
DbContext.SaveChanges();
}
Then your controller POST is simply:
if (ModelState.IsValid)
{
UpdateUser(UserModel);
// redirect to list or where ever...
}
I'm having some trouble using a helper method to perform an update to a set of model objects. The table uses a lookup table to hold 5 records per agent/user. If I want to save the record for the agent, I need to save that record onto the AgentTransmission table, and up to 5 other records on the RelationshipCodeLookup table.
Since I have to do this five times per agent, and we must do the process in the Create and Edit methods, I created a helper method to save the records. This works fine during the create process as we're simply doing a DbContext.Add(). However when I need to perform an update, I get the error message
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
I think this has to do with the fact I'm passing the model object to my helper method, and therefore the DbContext thinking that it has two separate objects to keep track of. I say this because the lines of code that are commented out work just fine and allow me to save the object. Passing the object to the helper method, however, gets the above error.
Does anyone know of a way around this (using a helper method to perform an update)?
Controller Action
//Save relationship codes in lookup table
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode1))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode1, agenttransmission.ID);
}
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode2))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode2, agenttransmission.ID);
}
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode3))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode3, agenttransmission.ID);
}
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode4))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode4, agenttransmission.ID);
}
if (AgentTransmissionValidator.ValidateRelationshipCode(agenttransmission.RelationshipCode5))
{
//db.Entry(agenttransmission.RelationshipCode1).State = EntityState.Modified;
//db.SaveChanges();
SaveRelationshipCodes(agenttransmission.RelationshipCode5, agenttransmission.ID);
}
Helper Method
public void SaveRelationshipCodes(RelationshipCodeLookup relCode, int id)
{
if (relCode.AgentId == 0) relCode.AgentId = id;
relCode.LastChangeDate = DateTime.Now;
relCode.LastChangeId = Security.GetUserName(User);
//Check to see if record exists and if not add it
if (db.RelationshipCodeLookup.Find(id, relCode.RelCodeOrdinal) != null)
{
db.Entry(relCode).State = EntityState.Detached;
}
else
{
if(relCode.RelCodeOrdinal == 0) relCode.RelCodeOrdinal = FindOrdinal(relCode);
db.RelationshipCodeLookup.Add(relCode);
}
db.SaveChanges();
}
EDIT
After scouring the web I attempted to save via this method
//Check to see if record exists and if not add it
if (db.RelationshipCodeLookup.Find(id, relCode.RelCodeOrdinal) != null)
{
db.Entry(relCode).CurrentValues.SetValues(relCode);
}
else
{
Member 'CurrentValues' cannot be called for the entity of type 'RelationshipCodeLookup because the entity does not exist in the context. To add an entity to the context call the Add or Attach method of DbSet<RelationshipCodeLookup>
However.... doing that only puts me back at the start with the following error on db.RelationshipCodeLookup.Attach(relCode);
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
Try this:
db.RelationshipCodeLookup.Attach(relCode);
db.Entry(relCode).State = EntityState.Modified;
For updates you want to attach the detached object then set it's state to modified.
The issue here seems to be that the Entity Framework cannot track two objects of the same kind at the same time. Because of that I find the solution to this problem more than a little weird. By calling .Find() on the DbContext and instantiating a second copy of the model object I was finally able to save. Seems to break all the rules the EF was laying out for me in the error messages, but hey it works.
public void SaveRelationshipCodes(int id, RelationshipCodeLookup relCode)
{
if (relCode.AgentId == 0) relCode.AgentId = id;
relCode.LastChangeDate = DateTime.Now;
relCode.LastChangeId = Security.GetUserName(User);
//Check to see if record exists and if not add it
if (db.RelationshipCodeLookup.Find(id, relCode.RelCodeOrdinal) != null)
{
//Need to call .Find to get .CurrentValues method call to work
RelationshipCodeLookup dbRelCode = db.RelationshipCodeLookup.Find(id, relCode.RelCodeOrdinal);
db.Entry(dbRelCode).CurrentValues.SetValues(relCode);
}
else
{
if(relCode.RelCodeOrdinal == 0) relCode.RelCodeOrdinal = FindOrdinal(relCode);
db.RelationshipCodeLookup.Add(relCode);
}
db.SaveChanges();
}