Try fast search NHibernate

26 November 2008

Entities behavior injection

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

image

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.



kick it on DotNetKicks.com

20 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. As I can guess optional identifier make sense for read-only or insert-only entities, isn't it?

    And about test case...

    Can we call session.Update(invoice, (Guid) someGuid)?
    And what about session.Update(savedInvoice, (Guid) someGuid)?

    ReplyDelete
  3. @Anonymous
    You can call session.Update(entity) for any entity loaded in the same session. BTW you can load an entity, change it, and simply call session.Flush.

    ReplyDelete
  4. Can I load entity serialize it to xml, change some porps, than deserialize it back to object and update it if mapping doesn't defined identifier property?

    ReplyDelete
  5. As I said at the bottom of article, the feature is not fully supported that mean that, probably, there are various things you can't do.
    BTW who don't define an ID is not the mapping but the class implementation. In the mapping the "id" tag is required (the id should be only for persistence stuff)

    ReplyDelete
  6. Not a fan of DI into entities but nice to be able to use a non-default constructor.

    On a DDD front the other major issues I find with NHibernate are:

    1) Value objects - We really want to re-validate them when reloading from persistence.
    2) Aggregates - When you modify one part of the aggregate you really want NHibernate to give you a hook that lets you ensure the whole aggregate is valid. If not none of the changes should be saved.
    3) Version - It'd be cool if we could, without having to do any messiness in the model, assign a single version to an entire aggregate.

    Anyway keep up the great work, every version of NHibernate is better than the last and as a user I'm massively impressed.

    ReplyDelete
  7. @Colin
    1) The PostLoadEventListener is not enough ?
    2) Do you mean an "observable" collection ? In NHV we are validating the whole graph on demand and I really like this approach instead validate at each property change.
    3) Yes, a version for the whole graph involved in the same cascade would be interesting even if i'm not so sure it is efficient... probably I need a specific little example, of a possible real case, to study all matters involved in NH.

    ReplyDelete
  8. Fabio,

    Is there an example of usage of PostLoadEventListener - I cannot seem to find it?

    ReplyDelete
  9. For PostLoad no but we have an example for others events. Check for "Soft Deletes" in www.nhforge.com

    ReplyDelete
  10. @Fabio
    Thanks for replying, put in some more details below:

    1) Ta for reference to PostLoadEventListener, never heard of it before. I was just meaning the issue discussed here: (http://groups.google.com/group/nhusers/browse_thread/thread/f10a2328dd4b11eb/938bb50c534e8fee?lnk=gst&q=Greg+Young#938bb50c534e8fee). Will definitely look at the post load event to see if it lets me get the behavior I want.


    2) So I'm thinking that if an Order is the root of an aggregate containing OrderLineItems then if I change any of those line items I want it to automatically validate the entire Order. I used to think interceptors were the solution but right now they aren't because I quite probably have no way to get from an OrderLineItem back to the Order. So I agree you don't want it per property change but just before persistence would be cool.

    3) I guess I'm thinking of the coarse grained locking idea: http://martinfowler.com/eaaCatalog/coarseGrainedLock.html. I've blogged about it before but that post is showing its age: http://colinjack.blogspot.com/2007/05/nhibernate-and-coarse-grained-locking.html.

    ReplyDelete
  11. 3) I know the pattern (it is an open issue in H3.3 too)

    ReplyDelete
  12. For sure some will use it to inject services in entities, but that's their problem... until you have to come back on their code !

    It's still very usefull to apply the strategy pattern as you did in you example.

    NH is really becoming a powerfull and flexible ORM.

    ReplyDelete
  13. @jeremie.chassaing,

    Depending of the type of service, it can be *very* useful to inject "services" into entities. As long as transaction boundaries and session lifecycle are out of entity's scope, I don't think it's a problem to inject stuff into entities.

    ReplyDelete
  14. I agree with those who approves services injection into entities (via domain interfaces). I think it's an anamic domain model deterrent, also it simplifiy infrastructural related stuff bounded to entity behaviour and domain events.

    ReplyDelete
  15. @Fabio
    I just discovered that "PocoInstantiator" checks for "generateFieldInterceptionProxy" before passing the instantiation job to reflection optimizer. So if I have an entity with "lazy=true" for one of it's properties, it wouldn't pass the control to reflection optimizer. It seems logical it needs its own proxy to handle lazy-loading tasks. But what's the solution if I want to have dependencies being injected to entities?

    ReplyDelete