If you are working with NH you know that NH likes POCOs and you must have a default constructor without parameters. Starting from today that is the past.
The domain
The implementation of Invoice is:
public class Invoice : IInvoice
{
private readonly IInvoiceTotalCalculator calculator;
public Invoice(IInvoiceTotalCalculator calculator)
{
this.calculator = calculator;
Items = new List<InvoiceItem>();
}
#region IInvoice Members
public string Description { get; set; }
public decimal Tax { get; set; }
public IList<InvoiceItem> Items { get; set; }
public decimal Total
{
get { return calculator.GetTotal(this); }
}
public InvoiceItem AddItem(Product product, int quantity)
{
var result = new InvoiceItem(product, quantity);
Items.Add(result);
return result;
}
#endregion
}
Are you observing something strange ?
- There is not a property for the Id
- There is not a default constructor without parameter
The Invoice entity are using an injectable behavior to calculate the total amount of the invoice; the implementation is not so important…
public class SumAndTaxTotalCalculator : IInvoiceTotalCalculator
{
#region Implementation of IInvoiceTotalCalculator
public decimal GetTotal(IInvoice invoice)
{
decimal result = invoice.Tax;
foreach (InvoiceItem item in invoice.Items)
{
result += item.Product.Price * item.Quantity;
}
return result;
}
#endregion
}
The full mapping:
<class name="Invoice" proxy="IInvoice">
<id type="guid">
<generator class="guid"/>
</id>
<property name="Description"/>
<property name="Tax"/>
<list name="Items" cascade="all">
<key column="InvoiceId"/>
<list-index column="pos"/>
<composite-element class="InvoiceItem">
<many-to-one name="Product"/>
<property name="Quantity"/>
</composite-element>
</list>
</class>
<class name="Product">
<id name="Id" type="guid">
<generator class="guid"/>
</id>
<property name="Description"/>
<property name="Price"/>
</class>
The Test
[Test]
public void CRUD()
{
Product p1;
Product p2;
using (ISession s = sessions.OpenSession())
{
using (ITransaction tx = s.BeginTransaction())
{
p1 = new Product {Description = "P1", Price = 10};
p2 = new Product {Description = "P2", Price = 20};
s.Save(p1);
s.Save(p2);
tx.Commit();
}
}
var invoice = DI.Container.Resolve<IInvoice>();
invoice.Tax = 1000;
invoice.AddItem(p1, 1);
invoice.AddItem(p2, 2);
Assert.That(invoice.Total, Is.EqualTo((decimal) (10 + 40 + 1000)));
object savedInvoice;
using (ISession s = sessions.OpenSession())
{
using (ITransaction tx = s.BeginTransaction())
{
savedInvoice = s.Save(invoice);
tx.Commit();
}
}
using (ISession s = sessions.OpenSession())
{
invoice = s.Get<Invoice>(savedInvoice);
Assert.That(invoice.Total, Is.EqualTo((decimal) (10 + 40 + 1000)));
}
using (ISession s = sessions.OpenSession())
{
invoice = (IInvoice) s.Load(typeof (Invoice), savedInvoice);
Assert.That(invoice.Total, Is.EqualTo((decimal) (10 + 40 + 1000)));
}
using (ISession s = sessions.OpenSession())
{
IList<IInvoice> l = s.CreateQuery("from Invoice").List<IInvoice>();
invoice = l[0];
Assert.That(invoice.Total, Is.EqualTo((decimal) (10 + 40 + 1000)));
}
using (ISession s = sessions.OpenSession())
{
using (ITransaction tx = s.BeginTransaction())
{
s.Delete("from Invoice");
s.Delete("from Product");
tx.Commit();
}
}
}
In the previous week I tried to pass the test without change NH’s code-base. The first result was that I found a bug in NH and probably in Hibernate3.2.6, the second result was that it is completely possible to use NH with “fat” entities, without default constructor and using an IoC framework to inject behavior to an entity. After that work I realize that some little “relax” are needed in NH-code-base (NH-1587,NH-1588,NH-1589).
How pass the test
A very simple solution, to use an IoC with NH, is write a custom implementation of IInterceptor and use the Instantiate method to create an entity instance using an IoC container. The problem with this solution is that you still need a default constructor and… well… you must use the same interceptor for all sessions.
Another possible solution, for NH2.1 (trunk), is the use of a custom <tuplizer> for EntityMode.POCO. Probably I will write another blog-post about it.
If you are using the ReflectionOptimizer (used by default) there is a simple short-cut: I can write a IBytecodeProvider implementation based on Castle.Windsor container. The BytecodeProvider is another injectable piece of NH, trough the NHibernate.Cfg.Environment, before create the configuration. The BytecodeProvider has two responsibility: provide the ProxyFactoryFactory and provide the ReflectionOptimizer.
public class BytecodeProvider : IBytecodeProvider
{
private readonly IWindsorContainer container;
public BytecodeProvider(IWindsorContainer container)
{
this.container = container;
}
#region IBytecodeProvider Members
public IReflectionOptimizer GetReflectionOptimizer(Type clazz, IGetter[] getters, ISetter[] setters)
{
return new ReflectionOptimizer(container, clazz, getters, setters);
}
public IProxyFactoryFactory ProxyFactoryFactory
{
get { return new ProxyFactoryFactory(); }
}
#endregion
}
In this case, obviously, the ProxyFactoryFactory class is
NHibernate.ByteCode.Castle.ProxyFactoryFactory.
Now the ReflectionOptimizer (using the fresh NH’s trunk):
public class ReflectionOptimizer : NHibernate.Bytecode.Lightweight.ReflectionOptimizer
{
private readonly IWindsorContainer container;
public ReflectionOptimizer(IWindsorContainer container, Type mappedType, IGetter[] getters, ISetter[] setters)
: base(mappedType, getters, setters)
{
this.container = container;
}
public override object CreateInstance()
{
if (container.Kernel.HasComponent(mappedType))
{
return container.Resolve(mappedType);
}
else
{
return container.Kernel.HasComponent(mappedType.FullName)
? container.Resolve(mappedType.FullName)
: base.CreateInstance();
}
}
protected override void ThrowExceptionForNoDefaultCtor(Type type)
{
}
}
As last, a quick view to the configuration:
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory name="EntitiesWithDI">
<property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
<property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
<property name="connection.connection_string">
Data Source=localhost\SQLEXPRESS;Initial Catalog=BlogSpot;Integrated Security=True
</property>
</session-factory>
</hibernate-configuration>
As you can see the configuration is minimal and, in this case, I don’t need to configure the “proxyfactory.factory_class” property because I will inject the whole BytecodeProvider.
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
ConfigureWindsorContainer();
Environment.BytecodeProvider = new BytecodeProvider(container);
cfg = new Configuration();
cfg.AddAssembly("EntitiesWithDI");
cfg.Configure();
cfg.Interceptor = new WindsorInterceptor(container);
new SchemaExport(cfg).Create(false, true);
sessions = (ISessionFactoryImplementor) cfg.BuildSessionFactory();
}
The BytecodeProvider injection is the line after the configuration of Windsor container.
The configuration of the container is very simple:
protected override void ConfigureWindsorContainer()
{
container.AddComponent<IInvoiceTotalCalculator, SumAndTaxTotalCalculator>();
container.AddComponentLifeStyle(typeof (Invoice).FullName,
typeof (IInvoice), typeof (Invoice), LifestyleType.Transient);
}
Conclusions
- The default ctor without parameter constraint was removed.
- Use an IoC to inject behavior to an entity is possible and easy.
NOTE: Even if is possible to write an entity without the Id, the feature is not fully supported.
Code available here.