Pergunta

I have the need to persist a calculated property with an aggregate root. The calculation is based on child entities. I am using the root to add/remove the children via domain methods, and these methods update the calculate property.

A child entity can be added to a particular root by multiple users of the system. For example, UserA can add a child to Root123, and UserB can also add a child to Root123.

How can I ensure that this calculated property is persisted accurately when more than one user may be adding child entities to the same root in different transactions? In my particular case, the calculated property is used to ensure that some limit is not exceeded, as set by another property on the root.


Here is a more concrete example of the problem:

public class RequestForProposal : AggregateRoot {
    ...
    private ISet<Proposal> _proposals = new HashedSet<Proposal>();

    public virtual int ProposalLimit { get; set; }
    public virtual int ProposalCount { get; protected set; }

    public virtual IEnumerable<Proposal> Proposals {
        get { return _proposals; }
    }
    ...

    public virtual void AddProposal(User user, Content proposalContent) {
        if (ProposalCount >= ProposalLimit) {
            throw new ProposalLimitException("No more proposals are being accepted.");
        }

        var proposal = new Proposal(user, proposalContent);
        _proposals.Add(proposal);
        ProposalCount++;
    }

    public virtual void RemoveProposal(Proposal proposalToRemove) {
        _proposals.Remove(proposalToRemove);
        ProposalCount--;
    }
}

What if 2 users are submitting their proposals at roughly the same time? The UI sees that the limit has not yet been reached and shows the Web page to submit a proposal for both users. When the first user submits, all is well. Now, the second user will be okay as long as the first user submits before the second one, so that when the second user submits, the data is retrieved from the DB and the limit will be accurate.

Is this a moot point? Should I rely on constraints in the DB (ProposalLimit >= ProposalCount) for those rare cases where 2 users submit nearly simultaneously?

Foi útil?

Solução

If you were to put the Business Rule Check (ie limit check) inside a transaction, it will be fine. That is

  1. User Clicks button that fires the Add Proposal Command
  2. Code starts a new transaction. See here for how I suggest you use transactions
  3. Load the RequestForProposal object from the db, or Refresh it. I suggest you use an upgrade lock.
  4. Add the new proposals to the root. Check the limit constraint, throw an exception it fails.
  5. Commit the transaction

Doing it this way you are using the databases concurrency controls. I don't think there is any other way of doing it.

This will create some contention, but you can take a few steps to minimize that. Namely make sure in Step 3 that the db column you select by has an index on it. This will cause a row lock, instead of a page lock.

If you use an Upgrade lock in step 3, this will avoid deadlocks. Basically when the second users submits a proposal for the same Aggregate Root the db will not let you read it until the first transaction has committed.

You should also consider adding a db index on Proposal.RequestForProposalId, it will help performance as that is the column the Proposal's are loaded on. I'm not 100% if it helps minimize the scope of any locks on that table, but it might ...

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top