NHibernate: Cannot assign property value in entity

2019-09-13 19:27发布

问题:

When I run the following code, at B's constructor, I got a NullReferenceException. I cannot assign value to B's Number property.

Models:

public class A
{
    public A()
    {
        this.Number = 1;
        if (this.Number != 1)
        {
            throw new Exception("Failed to assign value in A's constructor");
        }
    }
    public virtual int Id { get; private set; }
    public virtual int Number { get; set; }
    public virtual B B { get; set; }
}

public class B
{
    public B()
    {
        this.Number = 1;
        // Throws NullReferenceException: Object reference not set to an instance of an object.
        if (this.Number != 1)
        {
            throw new Exception("Failed to assign value in B's constructor");
        }
    }
    public virtual int Id { get; private set; }
    public virtual int Number { get; set; }
}

Mappings:

public class AMappings : ClassMap<A>
{
    public AMappings()
    {
        Id(x => x.Id);
        Map(x => x.Number);
        References(x => x.B).Cascade.All();
    }
}

public class BMappings : ClassMap<B>
{
    public BMappings()
    {
        Id(x => x.Id);
        Map(x => x.Number);
    }
}

Main method:

class Program
{
    static void Main(string[] args)
    {
        // Create connection string
        string connectionString = new System.Data.SqlClient.SqlConnectionStringBuilder()
                                      {
                                          DataSource = @".\r2",
                                          InitialCatalog = "TestNHibernateMappings",
                                          IntegratedSecurity = true
                                      }.ConnectionString;

        // Create SessionFactory
        ISessionFactory sessionFactory = Fluently.Configure()
        .Database(MsSqlConfiguration
                      .MsSql2008.ConnectionString(connectionString)
                      .ShowSql())
        .Mappings(m => m.FluentMappings
            .Add(typeof(AMappings))
            .Add(typeof(BMappings)))
        .ExposeConfiguration(BuildSchema)
        .BuildConfiguration()
        .BuildSessionFactory();

        // Create test object in DB
        using (var session = sessionFactory.OpenSession())
        {
            using (var trans = session.BeginTransaction())
            {
                var a = new A();
                a.B = new B();
                session.Save(a);
                trans.Commit();
            }
        }

        // Read test object from DB
        using (var session = sessionFactory.OpenSession())
        {
            using (var trans = session.BeginTransaction())
            {
                var a = session.Get<A>(1);
            }
        }
    }

    static void BuildSchema(Configuration cfg)
    {
        new SchemaExport(cfg).Create(false, true);
    }
}

I'm using NHibernate 3.1.0.4000, FluentNHibernate 1.2.0.712.

Any ideas? Thanks.

回答1:

The key here is that Number is virtual.

A.B is being lazy-loaded. NHibernate creates a proxy for B which overrides each virtual property in the class. The first time one of the non-Id properties is accessed, NHibernate will load the data from the database to populate the object.

Since this proxy class is a subclass of B, B's constructor will be called before the proxy constructor. When B's constructor sets the virtual property Number, it is calling the Number property as defined in the proxy subclass, which has not yet been initialized.

For a more thorough discussion of constructors and inheritance, see http://www.yoda.arachsys.com/csharp/constructors.html

To fix this, convert any properties you wish to set in the constructor to use backing fields instead of auto-properties, then set the field instead of the property in the constructor.

public class B
{
    public B()
    {
        _number = 1;
    }

    public virtual int Id { get; private set; }

    private int _number;

    public virtual int Number
    {
        get { return _number; }
        set { _number = value; }
    }
}

It's a bit more verbose, but it effectively avoids touching virtual methods or properties in the constructor.