4

I have an asp.net core project that is divided into the following structure

  • Controller
    • Store
      • Repository

The controller receives requests, and calls the correct Stores that are responsible for actions against the database (Create Users, Update Records, etc) The way I see it the controller should be agnostic to the database - it should not control the transactions, or even be aware of their existence.

The store creates a transaction, and calls the required repositories, and their required CRUD methods.

This is pretty straight forward. However, often I find myself repeating code - because of the transaction management.

Sometimes I want to add users, and add them to a group - So If I had a GroupsStore, and a UsersStore - I now have a GroupsUsersStore that does exactly the same as those store, but by putting that in one transaction.

This is obviously wrong, but I can't think of a better solution - since transactions can't be nested in sql server, and I want those stores to work independently as well.

How do you solve such issue?

  • This can become slightly easier by using tools like EntityFramework for data access. Do you use any ORM? The way I handled a similar problem was to take the transaction logic out of the repositories, and handle them a layer higher - in services. I would start calling methods on the repositories, and when I'm done, I call db.SaveChanges(). That way, it all happens in a single transaction. The downside is that you always need to open and commit a transaction outside of the repository methods. – Ivo Coumans Dec 1 '16 at 14:02
  • 2
    Why would you want to nest transactions? You can do these actions in one transaction only. Also handling these are usually wrapped into a Unit Of Work module. On another note I wrote an answer about a similar problem on SO, read it and help yourself by writing better code :) – kayess Dec 1 '16 at 14:02
  • These methods are independent, and they are a level higher than repositories - AddUser doesn't just add a user column, it adds other info, and so does AddGroup. The Idea is that I want to be able to call them from anywhere (via dependency injection), without calling db.SaveChanges() or even knowing that such a connection to db exists. I could create an even higher level like a service, or something - although I don't like that Idea either. – gilmishal Dec 1 '16 at 14:09
  • That sounds like you are on the right track, go read my linked A and it's links toward others, also organizing your code adhere to CQS can help. – kayess Dec 1 '16 at 14:11
1

Your Stores sound like a business layer, correct? I would assume that a given single method in the stores might call multiple repos to achieve its work, with everything being rollbackable in the event of an error. If that's the case, then your store should initiate the sql connection and transaction, and hand them down as a dependencies to the repos.

Inside FooStore.cs:

public StoreResult<ResultThing> DoStuff(int stuff) 
{
     using (var connection = GetOpenConnection())
     using (var transaction = connection.BeginTransaction())
     {
          var fooRepo = new FooRepo(connection, transaction);
          fooRepo.DoThing(stuff); // calls connection.Execute(...)
          // add more repo calls here
          transaction.Commit();
          return new StoreResult<ResultThing>();
     }
}

Now, your StoreResult will need to return some kind of success/fail/messaging data to indicate back to the controller that called it what kind of result occurred.

  • My problem is that I have gotten to a point where DoStuff does some things, and DoOtherStuff does another thing - both in their own transactions and I have DoBothStuff that basically does both stuff just one after the other, and it needs to be in the same transaction. Also, in the future, I might want different transaction management - one would rollback the entire transaction on rollback, and in another situation I would only rollback a certain part. Nesting transactions gives you exactly that option. – gilmishal Dec 4 '16 at 15:01
  • If DoStuff and DoOtherStuff are called in different places but need to be rolled back together, then they should not be Public functions from the store, and should accept their connection/transaction as parameters. Rather than nested transactions, i would have perhaps a try/catch layer in the public-level Store method, where throwing different kinds of exceptions can trigger different levels of rollback & other work. – Graham Dec 5 '16 at 14:34
1

Give your "Store" classes an optional boolean constructor parameter autocommit to control if they shall manage the transactions by themselves (i.e. committing automatically after their work is done), or if they leave this to the caller. The default value for this parameter might be true.

So your GroupsStore will still commit automatically by default, and your UsersStore store, too, when used on its own.

Now, for having a combined "Groups & Users" transaction, you will probably still create a separate GroupsUsersStore class, but this one does not need to contain any duplicated code any more. Instead, it can reuse a GroupsStore and a UsersStore, calling them both with autocommit=false.

1

Sometimes I want to add users, and add them to a group - So If I had a GroupsStore, and a UsersStore - I now have a GroupsUsersStore that does exactly the same as those store, but by putting that in one transaction.

This is obviously wrong, but I can't think of a better solution - since transactions can't be nested in sql server, and I want those stores to work independently as well.

How do you solve such issue?

I think you are tackling a problem from the opposite end. Transaction control should be excersised on one application layer only. So, if you need to control transactions on a level higher, than you do now, you have several choices:

  • Refactor stores (actually, they are services it seems) so that each could execute all operations without going out of its bounds. You might want to merge stores for instance. But watch out for SRP obidience.
  • Construct higher-level layer of your application (orchestration layer), that will control transactions and call stores when needed to execute one bussiness operation.

So If I had a GroupsStore, and a UsersStore

I would say, that you need UserManager (or whatever you call it), that can really manage users as a bussiness entity: assign users to groups etc. Because currently your GroupsStore looks nothing more than a slighly god-like repository.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.