I've been asked a couple of times how to give a domain object access to a dependency injection (DI) container, such as Castle Windsor, if the domain object is loaded via an ORM, such as NHibernate. There are two apparent ways to do this: 1) you can manually give the domain object access to the DI via "setter injection" from your controller or service layer, or 2) you can use an NHibernate IInterceptor to preset the DI container when the object is loaded from the database. The latter is much cleaner and avoids having to pass service object references through middlemen classes.
Disclaimer: I feel I should add a disclaimer to this post that this technique should not be used as standard practice. Giving domain objects direct access to the DI container can be very powerful but also enables the domain layer to take on responsibilities that it should not have. Furthermore, having access to the DI container from the domain layer creates, at the very least, a virtual dependency on everything available via the DI container; consequently, additional maintenance issues may have to be considered. Finally, the DI container itself presents another layer of indirection which can be difficult to easily understand during the maintenance phase of a project. With that said...
Assume you want to show who a customer was referred by but can't set it up in a nice HBM mapping. The only way you can do it, for some reason or another, is by having the Customer instance make an explicit call to the database. Our goals are 1) to keep Customer only dependent on DAO interfaces and 2) to automatically inject the DAO dependency to Customer as soon as its loaded from the database even if it's loaded via an ORM. (Obviously, this example is a little strange to begin with but illustrates the technique, nonetheless. The technique is equally applicable for providing other services, such as an email utility.)
If you'd like to walk through modifying an application, already wired for Castle Windsor, to accommodate doing this, download the "enterprise" sample app found within the MVP with ASP.NET article on CodeProject.com and follow the steps below. (Alternatively, you can just download the sample at the end of this post to see the finished product.) You'll need to follow the steps found within the article to get it working with your SQL Server/Northwind database. The modification steps to the sample app are as follows:
1) First, since we always take a test-driven approach (right?), amend MvpSample.Tests.Data.CustomerDaoTests to reflect the following:
[TestFixture]
[Category("NHibernate Tests")]
public class CustomerDaoTests
{
/// <summary>
/// Initializes the NHibernate session bound to
/// HttpContext. This TestFixtureSetUp could be
/// moved to a NHibernateUnitTest class so you
/// don't have to
/// copy/past it into every unit test class.
/// </summary>
[TestFixtureSetUp]
public void Setup() {
NHibernateSessionManager.Instance
.RegisterInterceptor(
new NHibernateInterceptor());
NHibernateSessionManager.Instance
.BeginTransaction();
}
/// <summary>
/// Tests successful dependency injection
/// from within the domain layer.
/// </summary>
[Test]
public void TestGetReferrer() {
IDaoFactory daoFactory =
new NHibernateDaoFactory();
ICustomerDao customerDao =
daoFactory.GetCustomerDao();
Customer foundCustomer =
customerDao.GetById("CHOPS", false);
Customer referrer =
foundCustomer.GetReferrer();
Assert.AreEqual("Folies gourmandes",
referrer.CompanyName);
}
... remainder of pre-existing tests...
In the above code, you can see that we're doing two new things. First, we've added the call to RegisterInterceptor to the Setup method. (Since you have to register interceptors before starting a transaction, the order is important here.) So, as the code implies, we can wire up container DI via a custom, NHibernate Interceptor. The second thing to note is the new test. We'll be adding the method GetReferrer, which will use the DI container, to the Customer class.
2) If you try to compile the above change, VS will initially complain about the missing NHibernate interceptor class. So add a new class called NHibernateInterceptor and at it to the MvpSample.Data package. Since it's specific to the data layer - and to NHibernate for that matter - the data layer is the appropriate place to put it. The class is defined as follows:
namespace MvpSample.Data
{
public class NHibernateInterceptor : IInterceptor
{
public bool OnLoad(object entity, object id,
object[] state, string[] propertyNames,
IType[] types)
{
// Only use "setter injection" to give objects
// that implement IUsesDependencyInjection a
// reference to the DI container.
if (typeof(IUsesDependencyInjection)
.IsInstanceOfType(entity)) {
((IUsesDependencyInjection) entity)
.DependencyInjectionContainer =
new WindsorContainer(new XmlInterpreter());
}
// Even though we gave the object it's DI
// container, return the fact that we
// didn't modify the persistent properties
return false;
}
public bool OnFlushDirty(object entity,
object id, object[] currentState,
object[] previousState, string[] propertyNames,
IType[] types)
{
// Return the fact that we didn't
// modify the entity
return false;
}
public bool OnSave(object entity, object id,
object[] state, string[] propertyNames,
IType[] types)
{
// Return the fact that we didn't
// modify the entity
return false;
}
public void OnDelete(object entity, object id,
object[] state, string[] propertyNames,
IType[] types) {}
public void PreFlush(ICollection entities) {}
public void PostFlush(ICollection entities) {}
public object IsUnsaved(object entity) {
return null;
}
public int[] FindDirty(object entity, object id,
object[] currentState, object[] previousState,
string[] propertyNames, IType[] types) {
return null;
}
public object Instantiate(Type type, object id) {
return null;
}
}
}
The only bit of interesting code is found within the method OnLoad. This method first checks to see if the persistent object being loaded implements the interface IUsesDependencyInjection and then sets the publicly accessible property to give the object a reference to the Castle Windsor container. This is also called "setter injection."
3) The next logical step is to define the IUsesDependencyInjection interface. This will be implemented by our persistent business objects which should have a reference to the DI container when they're loaded from the database. The interface will be added to the MvpSample.Core project since the business objects will be implementing it. The simple interface is as follows:
namespace MvpSample.Core.DataInterfaces
{
public interface IUsesDependencyInjection
{
IWindsorContainer DependencyInjectionContainer {
set;
get;
}
}
}
4) Now modify MvpSample.Core.Domain.Customer to implement this interface. It'll immediatly complain that Custom needs to implement the getter/setter from the interface. You have a choice here; you can either implement the code in the Customer class itself, or, you can add it to the persistent object super class called DomainObject. (I.e. All persistent objects inherit from this class in the sample project.) I chose to put the implementation details into DomainObject for two reasons: A) it's now reusable without any duplicated code and exposes a very handy public setter for all persistent classes and B) you can place pre-condition code in the getter to ensure that the DI container has been set before it's used. The latter point will avoid any unexpected "object reference was null" exceptions. The downside to putting it into DomainObject is that it exposes a lot more power to a lot more classes. So this should be avoided if it's only going to be used by a couple of objects. (Regardless, DomainObject shouldn't inherit from IUsesDependencyInjection, only Customer should, at this time; otherwise, the interceptor will automatically give every persistent object a reference to the container...which might not be a terrible thing in some situations.) So in the DomainObject class, we add:
/// <summary>
/// Publicly accessible to allow the dependency
/// injection container to be given to the object.
/// If you have your domain object implement
/// <see cref="IUsesDependencyInjection" />,
/// it'll automatically be wired up with the DI
/// container when loaded with the ORM. If
/// someone has switched out the ORM or
/// disabled this, then the container can still
/// be set via this property.
/// </summary>
public IWindsorContainer DependencyInjectionContainer {
set {
dependencyInjectionContainer = value;
}
get {
if (dependencyInjectionContainer == null)
throw new ApplicationException(
"dependencyInjectionContainer has not " +
"yet been initialized");
return dependencyInjectionContainer;
}
}
5) The only remaining piece, to get the unit test working, is to add the GetReferrer method to Customer:
/// <summary>
/// This is an example of using DI from
/// the domain layer itself.
/// </summary>
public Customer GetReferrer() {
// Obviously, hard-coding "FOLIG" is
// unrealistic...but it'll work to illustrate
// the use of a DI container from within the
// domain layer.
return ((IDaoFactory)
DependencyInjectionContainer[typeof(IDaoFactory)])
.GetCustomerDao().GetById("FOLIG", false);
}
6) The NUnit test that we wrote previously should now compile and run successfully. (Be sure to copy the web's Config folder into the MvpSample.Tests/bin/Debug so the unit test will find it. A post-compilation script would be good for doing this on future builds.)
7) Now that we have the unit test working successfully, all we have to do is modify the NHibernateSessionModule (an IHttpModule) to register the interceptor when the transaction-per-web-request is started:
/// <summary>
/// Opens a session within a transaction at the
/// beginning of the HTTP request. This doesn't
/// actually open a connection to the database
/// until needed.
/// </summary>
private void BeginTransaction(object sender, EventArgs e) {
// This needs to be called BEFORE begin
// transaction since an interceptor can't
// be registered after a session has been
// created.
NHibernateSessionManager.Instance
.RegisterInterceptor(new NHibernateInterceptor());
NHibernateSessionManager.Instance
.BeginTransaction();
}
Obviously, you don't have to use a transaction for this to work, but it shows how an interceptor works when a transaction is being used.
8) To see it all in action, add a lblReferredBy label to the web project page found at /Views/EditCustomerView.ascx and set its text value to the referrer from within the set property of CustomerToUpdate, found within its code-behind page:
public Customer CustomerToUpdate {
get { ... }
set {
...
lblReferredBy.Text =
value.GetReferrer().CompanyName;
...
}
}
That's it in a nutshell. The process goes as follows:
-
Web request gets sent to server.
-
NHibernateSessionModule creates the interceptor, registers it with the Open-Session-in-View session and begins a transaction.
-
Page loads a persistent object from the database.
-
NHibernateInterceptor checks to see if the object being loaded implements IUsesDependencyInjection and sets its DI container, accordingly.
-
Other code calls the method GetReferrer, found on the Customer object. GetReferrer uses the DI container to load another Customer from the database via a data-access object. But before it's loaded, the getter method of the DI container ensures that the DI container is available for use and throws an exception if not.
This tutorial has shown how to give a domain object a reference to a DI container if that object was loaded from the database using NHibernate. The attached sample is a working version of all the steps mentioned above.
As always, please let me know if you have any comments, questions or suggestions.
Billy