6

I'm thinking of the options in regards to implementing a single unit of work for dealing with multiple datasources - Entity framework. I came up with a tentative approach - for now dealing with a single context - but it apparently isn't a good idea.

If we were to analyze the code below, would you consider it a bad implementation? Is the lifetime of the transaction scope a potential problem?

Of course if we wrap the transaction scope with different contexts we'd be covered if the second context.SaveChanges() failed...

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Transactions;

    namespace ConsoleApplication2
    {
        class Program
        {
            static void Main(string[] args)
            {
                using(UnitOfWork unitOfWork = new UnitOfWork())
                {

                    var repository = new EmployeeRepository(unitOfWork);

                    var employee = repository.CreateOrGetEmployee("Whatever Name");

                    Console.Write(employee.Id);

                    unitOfWork.SaveChanges();
                }
            }
        }

        class UnitOfWork : IDisposable
        {
            TestEntities _context;
            TransactionScope _scope;
            public UnitOfWork()
            {
                _scope = new TransactionScope();
                _context = new TestEntities();
            }

            public void SaveChanges()
            {
                _context.SaveChanges();
                _scope.Complete();
            }

            public TestEntities Context
            {
                get
                {
                    return _context;
                }
            }

            public void Dispose()
            {
                _scope.Dispose();
                _context.Dispose();
            }
        }

        class EmployeeRepository
        {
            UnitOfWork _unitOfWork;

            public EmployeeRepository(UnitOfWork unitOfWork)
            {
                _unitOfWork = unitOfWork;
            }

            public Employee GetEmployeeById(int employeeId)
            {
                return _unitOfWork.Context.Employees.SingleOrDefault(e => e.Id == employeeId);
            }

            public Employee CreateEmployee(string fullName)
            {
                Employee employee = new Employee();
                employee.FullName = fullName;
                _unitOfWork.Context.SaveChanges();
                return employee;
            }

            public Employee CreateOrGetEmployee(string fullName)
            {
                var employee = _unitOfWork.Context.Employees.FirstOrDefault(e => e.FullName == fullName);
                if (employee == null)
                {
                    employee = new Employee();
                    employee.FullName = fullName;
                    this.AddEmployee(employee);
                }
                return employee;
            }

            public Employee AddEmployee(Employee employee)
            {
                _unitOfWork.Context.Employees.AddObject(employee);
                _unitOfWork.Context.SaveChanges();
                return employee;
            }
        }
    }
johnildergleidisson
  • 2,087
  • 3
  • 30
  • 50

1 Answers1

7

Why do you start TransactionScope in constructor? You need it only for saving changes.

public void SaveChanges()
{
    // SaveChanges also uses transaction which uses by default ReadCommitted isolation
    // level but TransactionScope uses by default more restrictive Serializable isolation
    // level 
    using (var scope = new TransactionScope(TransactionScopeOption.Required,
                                            new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
    {
        _context.SaveChanges();
        scope.Complete();
    }
}

If you want to have unit of work with more contexts you will simply wrap all those context in the same unit of work class. Your SaveChanges will become little bit more complicated:

public void SaveChanges()
{
    using (var scope = new TransactionScope(TransactionScopeOption.Required,
                                            new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
    {
        _contextA.SaveChanges(SaveOptions.DetectChangesBeforeSave);
        _contextB.SaveChanges(SaveOptions.DetectChangesBeforeSave);
        scope.Complete();
        _contextA.AcceptAllChanges();
        _contextB.AcceptAllChanges(); 
    }
}

This version separate saving operation from reseting inner state of the context. The reason is that if the first context successfully saves changes but the second fires exception the transaction will be rolled back. Because of that we don't want the first context to have already cleared all changes as accepted (we would lose information about performed changes and we will not be able to save them again).

Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • The idea of starting the TransactionScope in the unit of work constructor is to when you add an object to the context, you can retrieve it from the context even before you commit the changes to the database. So for example if I call `unitOfWork.Context.Employees.AddObject(employee)`, then call `context.SaveChanges()` and then without completing the unit of work `context.Employees.ToList()` would return the just inserted employee even though it doesn't exist in the DB. – johnildergleidisson May 24 '12 at 13:04
  • 3
    That is pretty bad idea because if you call SaveChanges the record is inserted to database and that table become locked until you commit or rollback transaction so other thread will not be able to read or write to that table. – Ladislav Mrnka May 24 '12 at 19:46
  • +1 @LadislavMrnka Would `TransactionScope` only be required if saving to the databases has to be done in a particular order? In other words, if my multiple contexts are not related, would it be ok to just call `_contextA.SaveChanges(); _contextB.SaveChanges();` in my UoW? – GFoley83 Jun 10 '13 at 23:57
  • 2
    @GFoley83: It depends if you want those calls to be atomic transaction. If you use `TransactionScope` both calls must succeed. If the second call fails the first one is rolled back. Without transaction scope the first call will modify the database even if the second call fails. – Ladislav Mrnka Jun 11 '13 at 07:52
  • @LadislavMrnka: Awesome, exactly what I needed. You're like the Entity Framework pope here on stack! Thank you. – GFoley83 Jun 11 '13 at 16:09
  • In this situation if both contexts each one with your one connection, the Microsoft Distributed Transaction Coordinator (MDTC) would be required for transaction coordination envolving transaction within two different connections, be carefull with this approach. – will Oct 08 '20 at 12:42