25

What I can't understand is if its possible to make changes to the context and get the changes in the same transaction before its commited.

This is what I´m looking for:

using (var scope = new TransactionScope(TransactionScopeOption.Required)) 
{ 
    using (var context = new DbContext()) 
    { 
        //first I want to update an item in the context, not to the db
        Item thisItem = context.Items.First();
        thisItem.Name = "Update name";
        context.SaveChanges(); //Save change to this context

        //then I want to do a query on the updated item on the current context, not against the db
        Item thisUpdatedItem = context.Items.Where(a=>a.Name == "Update name").First();

        //do some more query
    } 

    //First here I want it to commit all the changes in the current context to the db
    scope.Complete(); 
} 

Can someone help me understand and show me a working pattern?

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
Marcus Höglund
  • 16,172
  • 11
  • 47
  • 69

3 Answers3

52

Yes, it's possible to do and it's very useful when you want to insert a entity to database and use the auto-generated id for the next insert or update

using (var context = new DbContext())     
{ 
    using (var transaction = context.Database.BeginTransaction()) {
        var item = new Item();
        context.Items.Insert(item);
        context.SaveChanges(); // temporary insert to db to get back the auto-generated id

        // do some other things
        var otherItem = context.OtherItems.First();
        // use the inserted id
        otherItem.Message = $"You just insert item with id = {item.Id} to database";
        transaction.Commit();
    }
} 

Because your question also asked about a working pattern, here's my working code (with use of FluentApi, DbContext & Transaction). I was having the same issue as you :). Hope it helps you

public class FluentUnitOfWork : IDisposable
{
    private DbContext Context { get; }

    private DbContextTransaction Transaction { get; set; }

    public FluentUnitOfWork(DbContext context)
    {
        Context = context;
    }

    public FluentUnitOfWork BeginTransaction()
    {
        Transaction = Context.Database.BeginTransaction();
        return this;
    }

    public FluentUnitOfWork DoInsert<TEntity>(TEntity entity) where TEntity : class
    {
        Context.Set<TEntity>().Add(entity);
        return this;
    }

    public FluentUnitOfWork DoInsert<TEntity>(TEntity entity, out TEntity inserted) where TEntity : class
    {
        inserted = Context.Set<TEntity>().Add(entity);
        return this;
    }

    public FluentUnitOfWork DoUpdate<TEntity>(TEntity entity) where TEntity : class
    {
        Context.Entry(entity).State = EntityState.Modified;
        return this;
    }

    public FluentUnitOfWork SaveAndContinue()
    {
        try
        {
            Context.SaveChanges();
        }
        catch (DbEntityValidationException dbEx)
        {
            // add your exception handling code here
        }
        return this;
    }

    public bool EndTransaction()
    {
        try
        {
            Context.SaveChanges();
            Transaction.Commit();
        }
        catch (DbEntityValidationException dbEx)
        {
            // add your exception handling code here
        }
        return true;
    }

    public void RollBack()
    {
        Transaction.Rollback();
        Dispose();
    }

    public void Dispose()
    {
        Transaction?.Dispose();
        Context?.Dispose();
    }
}

Sample usage:

var status = BeginTransaction()
                // First Part
                .DoInsert(entity1)
                .DoInsert(entity2)
                .DoInsert(entity3)
                .DoInsert(entity4)
                .SaveAndContinue()
                // Second Part
                .DoInsert(statusMessage.SetPropertyValue(message => message.Message, $"Just got new message {entity1.Name}"))
            .EndTransaction();
Kien Chu
  • 4,735
  • 1
  • 17
  • 31
  • Will this insert the row in the db and then if the transaction is rollbacked will it then delete the row? Or how do you get the ID of the row? @kienct89 – Marcus Höglund Dec 04 '15 at 08:54
  • it won't insert the row in the db until you call transaction.Commit(), the id will be assigned temporarily in the memory – Kien Chu Dec 04 '15 at 08:54
  • ok, Thank you for the example and description. I will test this and get back to you – Marcus Höglund Dec 04 '15 at 08:56
  • 3
    question by TransactionScope answer Database.BeginTransaction. how did he really accepted? Is there any answer by TransactionScope? – Nuri YILMAZ Jul 23 '19 at 19:26
  • Nice Idea! Thinking of DoBulkInsert will be great also https://sensibledev.com/entity-framework-bulk-insert/ – bunjeeb Oct 31 '19 at 12:06
5

If you want to make sure that you only query the local content of your context you can use the "local" collection:

Item thisItem = context.Items.First();  
thisItem.Name = "Update name";    
Item thisUpdatedItem = context.Items.Local.Where(a=>a.Name == "Update name").First();

This will only query the in-memory data of the context and will not hit the database.
The "Local" data is present as soon as you materialize an object in the context by adding it or loading it from the database, i.e. you do not need to call SaveChanges().
SaveChanges() will write the content of the context to your database.

Stephan Keller
  • 1,623
  • 11
  • 13
  • This seems awesome. I have one question; as kienct89 described in his answer it handles the insert with automatic ID generation in the db. Does the "local" has any similar function? – Marcus Höglund Dec 04 '15 at 08:58
  • 1
    For new records "Local" is just a staging area. The ID is supplied by the database when the record is inserted. It is a database function, not an EF function. – Stephan Keller Dec 04 '15 at 09:02
0

In my experience creating the context isn't necessary, I like to simplify as much as I can, so if you require code to be hit in the event of a rollback, surround the transaction with a try catch.

try
{
   using (var scope = new TransactionScope(TransactionScopeOption.Required))
   {
          ...do stuff

          scope.Complete();
   }
}
catch (Exception)
{
  ...do stuff
}
FerrousOxide
  • 644
  • 1
  • 5
  • 11
ARITHABORT
  • 11
  • 4