Here’s some interesting NHibernate behaviour that’s been tripping me up. Say we have a base class ‘Content’ and a sub class ‘Menu’, the following tests pass:
[Test] public void Get_returns_a_proxy_of_the_correct_type() { InSession(session => { var content = session.Get<Content>(menu1Id); var menu = content as Menu; menu.ShouldNotBeNull("menu is null"); }); } [Test] public void Load_does_not_get_return_a_proxy_of_the_correct_type() { InSession(session => { var content = session.Load<Content>(menu1Id); var menu = content as Menu; menu.ShouldBeNull(); }); }
That’s right, don’t expect ‘as’ to work on lazily loaded entities.
Get eagerly loads, so it runs some SQL against the database:
SELECT content0_.ContentId as ContentId15_0_, content0_.Name as Name15_0_, content0_.Position as Position15_0_, content0_.IsActive as IsActive15_0_, content0_.UrlName as UrlName15_0_, content0_.ParentContentId as ParentCo7_15_0_, content0_.ContentTypeId as ContentT8_15_0_, content0_.Controller as Controller15_0_, content0_.Action as Action15_0_, content0_.Text as Text15_0_, content0_.ContentType as ContentT2_15_0_ FROM "Content" content0_ WHERE content0_.ContentId=@p0;@p0 = 1 [Type: Int32 (0)]
This includes the discriminator column, ContentType, so NHibernate knows that this item should be created as a menu.
Load, lazy loads, so for this example no SQL is run against the database. The only thing NHibernate knows about this entity is that we’ve asked for ‘Content’ via the type parameter of the Load method, so it returns a proxy class that inherits from Content. When we use the ‘as’ operator to cast it to a menu it fails (returns null).
Interestingly, the type is still incorrect after we examine some properties so that the proxy is populated with a call to the db:
[Test] public void Type_is_not_correct_after_the_entity_loaded() { InSession(session => { var content = session.Load<Content>(menu1Id); Console.WriteLine(content.Name); var menu = content as Menu; menu.ShouldBeNull(); }); }
Event though this test causes the select statement to be executed against the DB to populate the entity, the proxy is still of type Content.
So rather than using the ‘as’ operator, I’ve implemented my own CastAs<T> extension method that understands NHibernate proxies:
public static class CastExtensions { public static T CastAs<T>(this object source) where T : class { if(source is INHibernateProxy) { var type = NHibernateUtil.GetClass(source); if (type != typeof (T)) { throw new ApplicationException(string.Format("Cannot cast {0} to {1}", type.Name, typeof (T).Name)); } return ((INHibernateProxy) source).HibernateLazyInitializer.GetImplementation() as T; } return source as T; } }
Now the we get the correct sub type:
[Test] public void Use_CastAs_to_cast_lazy_loaded_entities() { InSession(session => { var content = session.Load<Content>(menu1Id); Console.WriteLine(content.GetType().Name); var menu = content.CastAs<Menu>(); menu.ShouldNotBeNull(); }); }
Update
As Mookid pointed out in the comments, doing this kind of casting, especially of domain classes, is a code smell. What I should be doing instead is using polymorphism or the visitor pattern. Please read his post below which is a very cool alternative take on the problem:
7 comments:
Interesting post, and definitely an interesting solution you got there.
I do think, however, that the need to downcast is a code smell that should generally be avoided - either with polymorphism or by using a visitor.
That way, you can be sure that your code will fail to compile instead of getting nasty InvalidCastExceptions at runtime.
mookid,
That's a very good point. I should probably revisit the code that drove this post :)
@mookid (or anyone else!), I agree about the bad smell. The is statement is similar. My question is how could a visitor help? I'm probably missing something obvious.
The visitor pattern allows code outside of the inheritance hierarchy to handle subclasses individually in a "safe" way - i.e. you get a compilation error if you fail to implement the logic handling one of the subclasses.
I wrote some stuff about the exact same problem as Mike was having here, including an example implementation of a visitor.
mookid,
Great post, I've updated this post to point to it, I hope you don't mind.
I think the Visitor pattern is overkill for this situation.
Why not have an
abstract Type UnderlyingType{get;}
property that you override in the subclass.
Surely this is more than adequate without the need to spawn classes?
Nice post. I agree that using type casting should be avoided if possible though.
I'd like to add that the type comparison should take inheritance into account. For instance, if I try to cast a proxy of B to A, the code currently bails out because type A != B. Replace the line with this to fix that:
if (!typeof(T).IsAssignableFrom(type))
Post a Comment