Search code examples
c#asp.netnhibernatenhibernate-mapping

Nhibernate: Pull back most derived type in "Table-per-class hierarchy"


I'm working through an issue regarding a couple <subclass> elements in a "Table-per-class hierarchy" setup in an NHibernate config. I'm trying to fetch the "most-derived-type" of the parent class when I hit the DB. I.e. When I fetch an EnergySource object, I want the underlying type to be Grid or Primary depending on the <discriminator...>.

Everything actually works as expected if I add the attribute lazy="false" to the EnergySource class config. E.g. I can successfully cast with EnergySource as Grid & I can use reflection on the EnergySource & if it matches the discriminator, I can run GetType() & it relays:

UnderlyingSystemType: { Name = "Grid" ...

But with lazy-loading, I instead get a failed cast (only ever null) &:

UnderlyingSystemType: { Name = "EnergySourceProxy" ...

What's going on here? Is the underlying issue caused by lazy loading in the first place?

I've got my config set up like this (it's an old application):

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
  <class name="Domain.EnergySource, Domain" table="library_EnergySource" lazy="true">
  
    <cache usage="read-write" />
    <id name="Id" column="EnergySourceID" unsaved-value="0">
      <generator class="identity" />
    </id>
    
    <discriminator formula="case when EnergySourceTypeID in (1,2,3) then 1 else 4 end" />

    <property name="Name" />
    <many-to-one name="Type" column="EnergySourceTypeID" not-null="true" insert="false" update="false" />
    
    <subclass name="Domain.Grid, Domain"
              extends="Domain.EnergySource, Domain"
              discriminator-value="1">
    </subclass>

    <subclass name="Domain.PrimaryEnergy, Domain"
              extends="Domain.EnergySource, Domain"
              discriminator-value="4">
    </subclass>
  </class>
</hibernate-mapping>

And the classes are just:

namespace Domain
{
    public class Grid : EnergySource { }
    public class Primary : EnergySource { }
    
    public class EnergySource 
    {
        public virtual string Name { get; set; }
        public virtual EnergySourceType Type { get; set; }
    }
    
    public class EnergySourceType
    {
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
    }
}

Solution

  • Essentially, yes, the issue is that the class is "lazy-loaded." When I try and access EnergySource from another object where it's a property (e.g. item.EnergySource) I get NHibernate's "proxy", which is there to facilitate lazy-loading, as:

    In short... [what we're working with] is a lazy loaded object, NHibernate, at this point in time, has no idea what it is. But since it must return something, it returns a proxy of [our object], which will load the actual instance when needed. [But] that leads to some problems when you want to down cast the value.

    See the link for "The problem" for more details. But in essence, once we have a proxied object, we can't just cast it to a subclass of the type it's proxying unfortunately... Here are a few workarounds, taken from the links below:

    • Don't let the proxy exist to begin with by disabling lazy-loading using lazy="false"
    • On the class that uses the parent object, add the attribute lazy="no-proxy" (in my case, this didn't work, but it may be due to the old version of NHibernate or a mistake in my understanding of the docs; this is certainly what it was designed for):
    <many-to-one name="EnergySource" lazy="no-proxy"/>
    
    • Use an NHibernate specific method to "unproxy" the object, creating a new instance of it with the correct type, allowing the cast operation:
      return Session.GetSessionImplementation()
             .PersistenceContext.Unproxy(EnergySource) as Grid;
    
    • Diegose proposes an interesting workaround, which is to add a property on the parent object that exposes this, which works by "leaking a reference to the actual object". This is similar to the above option, but doesn't need anything NH specific to work:
    public virtual object Actual { get { return this; } }
    ...
    return item.EnergySource.Actual as Grid //Works
    

    They also provide code for a generic method to simplify this process further. And some caveats, which likely applies to the Unproxy approach as well.

    public virtual T As<T>() where T : Entity
    {
        return this as T;
    }
    ...
    
    Animal animalProxy = catLover.Pet;
    Cat cat = animalProxy.As<Cat>();
    
    // Cat will be the actual object of type Cat
    // Or null if animalProxy isn't one 
    

    This is a backdoor to the underlying object that NHibernate is managing. It should only be used to access properties of the derived classes. Use polymorphism for behavior. You should never pass the retrieved object to NHibernate methods like Update or Delete.

    Sources: