Since I started using IoC containers like Castle Windsor or Spring 18 months or so ago the way I write code has changed quite a lot. Let me give an example. Let's assume I am currently writing the code to create a user account that can be used to login into an application. Let us further assume that I am using the following domain driven architecture:
That is, we have a domain model that is surrounded by a service layer. The latter is accessed by the application. Data moved back and forth between application and service layer comes in the form of data transfer objects. The service layer and domain model have access to the repositories that can be used to access all entities in the domain.
Now that you have an overview of the architecture, let us assume that I am busy writing a class in the service layer that handles account related stuff. Let us call it AccountService. In this case I want to write a method that creates a new account in the system. This method has some additional constraints:
- Only currently logged in administrators may create an account
- The username and email must not already be in use
- The code should run within a single database transaction. This constraint actually is a consequence of the previous one: checking for duplicate user names/email addresses and saving a new account should be atomic.
- We would like to apply some general exception handling policies
Let's have a look at the method filled with pseudo code comments:
1: namespace BeautyOfInjection.Service {
2: public class AccountService {
3: /// <summary>
4: /// Creates a new account.
5: /// </summary>
6: /// <param name="userName">User name for the new account.</param>
7: /// <param name="email">E-mail address for the new account.</param>
8: /// <remarks>
9: /// Only administrators can create new accounts.
10: /// </remarks>
11: public void CreateAccount(string userName, string email) {
12: // start transaction
13: // check whether current user has permission to create an account
14: // check whether user name and email address are unique
15: // create a new account instance
16: // save instance to db
17: // commit transaction
18: // on failure: rollback transaction and handle exception
19: }
20: }
21: }
This should be straight forward so far. Let us begin to write code. First, we start off with the transaction handling, but let us try to think in terms of dependency injection. Personally I think that the burden of transaction handling should not lie on a single method in the service layer. Maybe we want CreateAccount to run within a larger transaction. What we need is the possibility to declare the requirement of a transaction. All transaction pluming should then be handled externally. This is a good example of separation of concerns. To achieve this, let us define an interface that allows us handle transaction requirements.
1: using System;
2:
3: namespace BeautyOfInjection.Persistence {
4:
5: /// <summary>
6: /// Allows the client to declare its requirement for a transaction.
7: /// </summary>
8: public interface ITransactionControl{
9: /// <summary>
10: /// Opens a new transaction context.
11: /// </summary>
12: /// <returns>A new transaction context</returns>
13: ITransactionContext EnsureTransaction();
14: }
15:
16: /// <summary>
17: /// A (possibly nested) transaction scope.
18: /// </summary>
19: public interface ITransactionContext : IDisposable {
20: /// <summary>
21: /// Considers this transaction context a success.
22: /// </summary>
23: /// <remarks>
24: /// If <see cref="VoteRollback"/> is not called by this transaction context or
25: /// any parent or child context, the database transaction will be committed.
26: /// </remarks>
27: void VoteCommit();
28:
29: /// <summary>
30: /// Votes for a rollback on this transaction.
31: /// </summary>
32: /// <remarks>
33: /// Calling this method will finaly cause a rollback of the database transaction.
34: /// </remarks>
35: void VoteRollback();
36: }
37: }
The actual working of those interfaces is not that important here. Let us just assume that we can declare the requirement for a transaction using EnsureTransaction and the returned ITransactionContext can be used to decide whether the transaction should commit or rollback. This design by the way resembles the Active Record transaction management. So, how do we gain access to a ITransactionControl instance? Remember that we are looking for inversion of control with dependency injection. We should therefore declare the need for an ITransactionControl object somewhere the IoC container recognizes. Two common approaches are Constructor Injection and Setter Injection. I chose for setter injection. With the new C# 3.0 auto property syntax it's a snap to declare the property.
1: public class AccountService {
2: // This dependency will be injected by the container
3: public ITransactionControl Transactions { get; set; }
4:
5: /// <summary>
6: /// Creates a new account.
7: /// </summary>
8: /// <param name="userName">User name for the new account.</param>
9: /// <param name="email">E-mail address for the new account.</param>
10: /// <remarks>
11: /// Only administrators can create new accounts.
12: /// </remarks>
13: public void CreateAccount(string userName, string email) {
14: // start transaction
15: using (var txn = Transactions.EnsureTransaction()) {
16: // check whether current user has permission to create an account
17: // check whether user name and email address are unique
18: // create a new account instance
19: // save instance to db
20: // commit transaction
21: txn.VoteCommit();
22: // on failure: rollback transaction
23: // (will be done automatically if VoteCommit is not called before dispose)
24: }
25: // ... and handle exception
26: }
27: }
This was easy. We simply do not care about were we get our ITransactionControl instance from. We simply assume that it will be resolved by the IoC container or explicitly set during a unit test.
Next step, check that the current user has the permission to create the account. I like to have this kind of code in a separate class. It simplifies unit testing a lot. So let us define a new dependency interface:
1: using System.Security;
2:
3: namespace BeautyOfInjection.Security {
4: /// <summary>
5: ///
6: /// </summary>
7: public interface IPermissions {
8: /// <summary>
9: /// Checks that the current user may create a new account.
10: /// </summary>
11: /// <exception cref="SecurityException">Permission was not granted.</exception>
12: void TryCreateAccount();
13: }
14: }
Before I return to CreateMethod, I'll define another dependency interface, regarding exception handling. Resemblance with the Exception Handling Block is no coincidence:
1: using System;
2:
3: namespace BeautyOfInjection.Service {
4: /// <summary>
5: /// Handles exceptions
6: /// </summary>
7: public interface IExceptionHandler {
8: /// <summary>
9: /// Handles the exception and returns whether the original exception should be rethrown.
10: /// </summary>
11: /// <param name="exception">The exception.</param>
12: /// <returns></returns>
13: bool HandleAndCheckForRethrow(Exception exception);
14: }
15: }
Let us see how far we are with CreateUser:
1: using System;
2: using BeautyOfInjection.Persistence;
3: using BeautyOfInjection.Security;
4:
5: namespace BeautyOfInjection.Service {
6: public class AccountService {
7:
8: #region dependencies
9: public ITransactionControl Transactions { get; set; }
10: public IPermissions Permissions { get; set; }
11: public IExceptionHandler ExceptionHandler { get; set; }
12: #endregion
13:
14: /// <summary>
15: /// Creates a new account.
16: /// </summary>
17: /// <param name="userName">User name for the new account.</param>
18: /// <param name="email">E-mail address for the new account.</param>
19: /// <remarks>
20: /// Only administrators can create new accounts.
21: /// </remarks>
22: public void CreateAccount(string userName, string email) {
23: try {
24: // start transaction
25: using (var txn = Transactions.EnsureTransaction()) {
26: // check whether current user has permission to create an account
27: Permissions.TryCreateAccount();
28:
29: // check whether user name and email address are unique
30: // create a new account instance
31: // save instance to db
32:
33: // commit transaction
34: txn.VoteCommit();
35: }
36: } catch (Exception e) {
37: // on error, handle exception
38: if(ExceptionHandler.HandleAndCheckForRethrow(e)) {
39: throw;
40: }
41: }
42: }
43: }
44: }
Not bad, if you ask me. Throw in some more external depencies: we want to access entities using the repository pattern, so let's define a account repository interface. Furthermore, we want accounts to be created using a factory. I think that the factory would also be a useful place to check for uniqueness of user name and email. I am always a bit struggling on this topic: on the one hand I want the domain model to use the repositories as little as possible. On the other hand I do not like it when business rules (i.e. uniqueness of email and user name) are not checked in the domain model. As I consider the factory to be part of the model, checking those rules in the factory seems reasonable to me. Be aware though that if you support changing a user name/email address you will have to check for uniqueness again and this time not in the factory. So you might as well put the uniqueness check into the model class (and skip the factory altogether). Anyway, this is just an example, so let's stick to the factory/repository pair.
Here are the required interfaces:
1: namespace BeautyOfInjection.Model {
2: /// <summary>
3: /// An account
4: /// </summary>
5: public interface IAccount {
6: string UserName { get; }
7: string EmailAddress { get; }
8: }
9:
10: /// <summary>
11: ///
12: /// </summary>
13: public interface IAccountFactory {
14: /// <summary>
15: /// Creates a new account instance.
16: /// </summary>
17: /// <param name="userName">Name of the user.</param>
18: /// <param name="email">The email.</param>
19: /// <returns>
20: /// The newly created account.
21: /// </returns>
22: /// <remarks>
23: /// The method validates the user name and email and ensures that both are unique.
24: /// </remarks>
25: /// <exception cref="ValidationException">
26: /// The user name and/or email were not valid or not unique.
27: /// </exception>
28: IAccount Create(string userName, string email);
29: }
30:
31: /// <summary>
32: ///
33: /// </summary>
34: public interface IAccountRepository {
35: void Save(IAccount account);
36: IAccount GetAccountByUserName(string userName);
37: IAccount GetAccountByEmail(string email);
38: }
39: }
ValidationException would be a custom exception type that is used for business rule violations.
Now we are ready to present the final version of CreateAccount:
1: using System;
2: using BeautyOfInjection.Model;
3: using BeautyOfInjection.Persistence;
4: using BeautyOfInjection.Security;
5:
6: namespace BeautyOfInjection.Service {
7: public class AccountService {
8:
9: #region dependencies
10: public ITransactionControl Transactions { get; set; }
11: public IPermissions Permissions { get; set; }
12: public IExceptionHandler ExceptionHandler { get; set; }
13: public IAccountFactory Factory { get; set; }
14: public IAccountRepository Repository{ get; set; }
15: #endregion
16:
17: /// <summary>
18: /// Creates a new account.
19: /// </summary>
20: /// <param name="userName">User name for the new account.</param>
21: /// <param name="email">E-mail address for the new account.</param>
22: /// <remarks>
23: /// Only administrators can create new accounts.
24: /// </remarks>
25: public void CreateAccount(string userName, string email) {
26: try {
27: // start transaction
28: using (var txn = Transactions.EnsureTransaction()) {
29: // check whether current user has permission to create an account
30: Permissions.TryCreateAccount();
31:
32: var account = Factory.Create(userName, email);
33: // the previous line would have thrown an exception
34: // if userName and/or email had been invalid/not unique
35:
36: // add instance to repository
37: Repository.Save(account);
38:
39: // vote for transaction commit
40: txn.VoteCommit();
41: }
42: } catch (Exception e) {
43: // on error, handle exception
44: if(ExceptionHandler.HandleAndCheckForRethrow(e)) {
45: throw;
46: }
47: }
48: }
49: }
50: }
You see that the service layer class becomes a mediator between all the external dependencies. What are the advantages of this design?
- Separation of concerns to the max, every aspect is neatly extracted to a separate interface
- High (unit-) testability. As we can mock/stub any of the external dependencies we have full control over their behavior. In this case we can easily test whether the method correctly requests a transaction, votes commit on success, handles exceptions, checks the required permissions etc. by simply passing mock objects with the right expectations.
- The code in the service layer gets simplified. We can concentrate on the core functionality of CreateAccount instead of cluttering the code with loads of unrelated code.
- We do not need to worry about how dependencies get injected. Just add the dependencies as e.g. properties (or constructor arguments) and assume that they will be provided externally. Of course you ultimately have to setup your IoC container or provide mocks/stubs. But while writing AccountService you just don't care.
Note: you will want to extract an interface from AccountService (say: IAccountService). This can then be used to define dependencies on this service in other layers, for example in your controllers (if you use an MVC framework like MonoRail).