7

I've been tried to get my head around this for a couple of days and there are lots of tutorials around unit of work and around TransactionScope but I can't find anything that talks about the two together. Any help much appreciated!

I am using Entity Framework with the Unit Of Work pattern and a repository per type. As per the simple code below I have a Member and MembershipDefinition entity. I want to create a Membership entity that links the two but when I create the Membership object I want to query the DB for a max value based on some business logic. Therefore, I need to use some kind of DB transaction to prevent another thread from incrementing the value in the db before my thread has written the Membership object back to the DB.

If I was using stored procs, this would be pretty straightforward but I can't figure out how to do it using pure c#...

The code below creates 100 Membership entities in the database with duplicated MembershipNumbers. I need to get this use to use transactions to ensure that all the Membership Numbers generated in the c# code are unique.

class Program
{
    static void Main(string[] args)
    {
        var p = new Program();
        p.Go();;
    }

    public void Go()
    {
        long memberId;
        long membershipDefId;

        using(var unitOfWork = new UnitOfWork())
        {
            // Setup - create test club and member entities

            var testUsername = ("TestUserName" + Guid.NewGuid()).Substring(0, 29);
            var member = new Member()
                             {
                                 UserName = testUsername
                             };

            var testmemebrshpDefName = ("TestMembershipDef" + Guid.NewGuid()).Substring(0, 29);
            var membershipDefinition = new ClubMembershipDefinition()
            {
                ClubId = 1,
                Name = testmemebrshpDefName
            };

            unitOfWork.MemberRepository.Add(member);
            unitOfWork.MembershipDefinitionRepository.Add(membershipDefinition);

            unitOfWork.Save();

            memberId = member.Id;
            membershipDefId = membershipDefinition.Id;
        }

        Task[] tasks = new Task[100];

        // Now try to add a membership to the Member object, linking it to the test Club's single Club Definition
        for (int i = 0; i < 100; i++)
        {
            var task = new Task(() => CreateMembership(memberId, membershipDefId));
            tasks[i] = task;
            task.Start();
        }
        Task.WaitAll(tasks);
    }

    private void CreateMembership(long memberId, long membershipDefId)
    {
        using (var unitOfWork = new UnitOfWork())
        {
            var member = unitOfWork.MemberRepository.GetById(memberId);
            var membershipDef = unitOfWork.MembershipDefinitionRepository.GetById(membershipDefId);

            var membership = new ClubMembership()
                                    {
                                        ClubMembershipDefinition = membershipDef
                                    };

            membership.MembershipNumber = (unitOfWork.MembershipRepository.GetMaxMembershipNumberForClub(membershipDef.ClubId) ?? 0) + 1;

            member.ClubMemberships.Add(membership);
            unitOfWork.Save();
        }
    }

}

public class UnitOfWork : IUnitOfWork, IDisposable
{
    internal ClubSpotEntities _dbContext = new ClubSpotEntities();
    internal MemberRepository _memberRepository;
    internal MembershipRepository _membershipRepository;
    internal MembershipDefinitionRepository _membershiDefinitionpRepository;

    public MemberRepository MemberRepository
    {
        get
        {
            if (_memberRepository == null)
                _memberRepository = new MemberRepository(_dbContext);

            return _memberRepository; ;
        }
    }

    public MembershipRepository MembershipRepository
    {
        get
        {
            if (_membershipRepository == null)
                _membershipRepository = new MembershipRepository(_dbContext);

            return _membershipRepository; ;
        }
    }

    public MembershipDefinitionRepository MembershipDefinitionRepository
    {
        get
        {
            if (_membershiDefinitionpRepository == null)
                _membershiDefinitionpRepository = new MembershipDefinitionRepository(_dbContext);

            return _membershiDefinitionpRepository; ;
        }
    }

    public virtual int Save()
    {
        return _dbContext.SaveChanges();

    }

    private bool _disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!this._disposed)
        {
            if (disposing)
            {
                _dbContext.Dispose();
            }
        }
        this._disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

public class MembershipRepository
{
    ClubSpotEntities _dbContext = new ClubSpotEntities();

    public MembershipRepository(){}

    public MembershipRepository(ClubSpotEntities dbContext)
    {
        _dbContext = dbContext;
    }

    public IEnumerable<ClubMembership> GetAll()
    {
        return _dbContext.Set<ClubMembership>().ToList<ClubMembership>();
    }

    public ClubMembership GetById(long id)
    {
        return _dbContext.ClubMemberships.First(x => x.Id == id);
    }

    public long? GetMaxMembershipNumberForClub(long clubId)
    {
        return _dbContext.ClubMemberships.Where(x => x.ClubMembershipDefinition.ClubId == clubId).Max(x => x.MembershipNumber);
    }

    public ClubMembership Add(ClubMembership entity)
    {
        return _dbContext.Set<ClubMembership>().Add(entity);
    }

    public void Delete(ClubMembership membership)
    {
        _dbContext.Set<ClubMembership>().Remove(membership);
    }

    public void Save()
    {
        _dbContext.SaveChanges();
    }
}


public partial class ClubMembership
{
    public long Id { get; set; }
    public long MembershipDefId { get; set; }
    public Nullable<long> MemberId { get; set; }
    public Nullable<long> MembershipNumber { get; set; }

    public virtual ClubMembershipDefinition ClubMembershipDefinition { get; set; }
    public virtual Member Member { get; set; }
}

public partial class ClubMembershipDefinition
{
    public ClubMembershipDefinition()
    {
        this.ClubMemberships = new HashSet<ClubMembership>();
    }

    public long Id { get; set; }
    public long ClubId { get; set; }
    public string Name { get; set; }

    public virtual ICollection<ClubMembership> ClubMemberships { get; set; }
}

public partial class Member
{
    public Member()
    {
        this.ClubMemberships = new HashSet<ClubMembership>();
    }

    public long Id { get; set; }
    public string UserName { get; set; }

    public virtual ICollection<ClubMembership> ClubMemberships { get; set; }
}
Matt Austin
  • 219
  • 1
  • 3
  • 13
  • are you sure a transaction is enough ? – Felice Pollano Aug 16 '12 at 07:59
  • You can call Stored Procedures via Entity Framework, so why over complicate the work you have to – podiluska Aug 16 '12 at 08:03
  • I see the question as a more general one: how to lower the complexity of system in such way that we can reason about transactional correctness of the system. In this case, we could solve the problem for the MAX(), but how can we be sure that the system works expectedly, does not deadlock, and does not run slowly. Even with a serializable transaction, you can't be sure that your system behaves well when running operations in parallel. – Steven Aug 16 '12 at 08:21
  • Could you show your SP code? And which isolation level do you use? – oryol Aug 16 '12 at 08:31
  • 1
    @Steven: Yes this is exactly what I was trying to get at. None of the Unit Of Work patterns that I have seen deals with Transactions but I assume that lots of people must be dealing with this kind of problem. Is there a "standard" way to address this with the existing patterns? I am keen to keep code is one place rather than spreading business logic between the c# code and the Db but as podiluska points out it seems to be much simpler to move code out to sps... – Matt Austin Aug 16 '12 at 08:34

1 Answers1

2

You can create transaction scope when instantiate new UnitOfWork, and commit it on completion. This not full exmaple:

class UnitOfWork
{
     ClubSpotEntities _dbContext;
     TransactionScope _transaction;

     public UnitOfWork()
     {
         _dbContext = new ClubSpotEntities();
         _transaction = new TransactionScope();
     }

     public void Complete()
     {
         _dbContext.SaveChanges();
         _transaction.Complete();
     }

     ...
}

UPD: As Steven said this is not solution of you problem. And UnitOfWork can't help you, TransactionScope alose not a solution in this case. EF does not support pessimistic locks which you want use, but you can try this solution.

Community
  • 1
  • 1
  • 1
    Running inside a transaction does not solve the OPs problem, since two parallel operations could still insert two members with the same `MembershipNumber` (where one operation will probably fail). – Steven Aug 16 '12 at 08:36
  • You can use serializable isolation level. Of course it can provoke deadlocks, but this is matter about implementation and not depend of choose EF or Stored Procedures. Also if deadlocks are rare you can try to repeat operation. – Kirill Volkov Aug 16 '12 at 08:54
  • 1
    When running in an `SERIALIZABLE` transaction, SQL server will also lock rows that are being read. However, when your query only runs over an index (such as the `MAX()` query of the OP could do), only that index will be locked, not the table itself. So although serializable will in this case help serialize `CreateMembership` operations, it could still fail in conjunction with other operations. – Steven Aug 16 '12 at 09:27
  • 1
    Oh, yes, you right. In this case I think Matt should use stored procedure, or resolve concurrency with 'Monitor' lock if his application is not used distributed a system. But I think the real problem in repository and model. Represented model is to anemic, and logic contains in repository (like service) insted model (rich model). – Kirill Volkov Aug 16 '12 at 13:38
  • You are spot on. I think that DDD is the solution to many complexity problems in software, such as the problem of transactional correctness. – Steven Aug 16 '12 at 14:06
  • Hi Kirill and Steven. Thanks very much for your comments so far. The Models in the example are stipped down to make the question clearer but I am not sure how this preoblem can be resolved with Domain Driven Design? Ultiamtely, I want to increment the MembershipNumber based on some business logic and that can only be done by (somehow) locking the record in the data store. I think from your answers so far, there is no pattern to accomodate this. (Sorry if I have not got your point correctly Kirill) – Matt Austin Aug 16 '12 at 14:22