How to properly define value object with NHibernate? - nhibernate

I want to query with NHibernate Linq by component. The component contains a Date property which is not included in the override Equals. I just want to skip the Date property when asking for equality. At the moment Date property is set in the ctor of the Address class and the query will return 0 rows because of that. I know that this breaks the Value Object concept but I just want to know why the code below does not work properly.
THE QUERY As you can see the Date property is in the query
select user0_.Id as Id0_,
user0_.Name as Name0_,
user0_.Number as Number0_,
user0_.Date as Date0_
from test1 user0_
where (user0_.Number = 1 /* #p0 */
and user0_.Date = '2013-01-28T14:29:47.00' /* #p1 */)
MAIN
class Program
{
private static ISessionFactory _sessionFactory;
private static void CreateSessionFactory()
{
FluentConfiguration config = Fluently
.Configure(new Configuration().Configure())
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<UserMap>());
new SchemaExport(config.BuildConfiguration()).Create(false, true);
_sessionFactory = config.BuildSessionFactory();
}
[STAThread]
static void Main(string[] args)
{
CreateSessionFactory();
using (var session = _sessionFactory.OpenSession())
using (var tx = session.BeginTransaction())
{
var user = new User()
{
Name = "Nik",
Address = new Address(1)
};
session.Save(user);
tx.Commit();
}
using (var session = _sessionFactory.OpenSession())
using (var tx = session.BeginTransaction())
{
session.Query<User>().Where(x => x.Address == new Address(1)).Single();
tx.Commit();
}
}
}
CLASSES AND MAPPINGS
public class Address : IEquatable<Address>, IEqualityComparer<Address>
{
protected Address() { }
public Address(int number)
{
Number = number;
Date = DateTime.Now;
}
public virtual int Number { get; protected set; }
public virtual DateTime Date { get; protected set; }
public override bool Equals(object obj)
{
return Number == ((Address)obj).Number;
}
public override int GetHashCode()
{
return Number.GetHashCode();
}
public bool Equals(Address other)
{
return Number == other.Number;
}
public bool Equals(Address x, Address y)
{
return x.Number == y.Number;
}
public int GetHashCode(Address obj)
{
return obj.GetHashCode();
}
}
public class User
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
public virtual Address Address { get; set; }
}
public class UserMap : ClassMap<User>
{
public UserMap()
{
Table("test1");
Id(x => x.Id).GeneratedBy.GuidNative();
Map(x => x.Name);
Component(x => x.Address, cm =>
{
cm.Map(x => x.Number);
cm.Map(x => x.Date);
});
}
}

You have mapped Address as a component. Therefore, when you ask NHibernate to compare user.Address to some Address instance, it will compare every attribute. NHibernate doesn't analyse compiled code to determine which of the properties are used in the Equals() method.
The code works properly.
You can write the query to compare every attribute separately instead of comparing the entire addresses. Or try to rethink the design to make Address a true value object.

Related

Insert into SQL Server database through NHibernate 4 with Session.Save using mapping-by-code

Currently experiencing issues with Session.Save() not inserting records and producing the following exception:
null id in NHModels.Domain.Activity entry (don't flush the Session after an exception occurs)
at NHibernate.Event.Default.DefaultFlushEntityEventListener.CheckId(Object obj, IEntityPersister persister, Object id, EntityMode entityMode)
at NHibernate.Event.Default.DefaultFlushEntityEventListener.GetValues(Object entity, EntityEntry entry, EntityMode entityMode, Boolean mightBeDirty, ISessionImplementor session)
at NHibernate.Event.Default.DefaultFlushEntityEventListener.OnFlushEntity(FlushEntityEvent event)
at NHibernate.Event.Default.AbstractFlushingEventListener.FlushEntities(FlushEvent event)
at NHibernate.Event.Default.AbstractFlushingEventListener.FlushEverythingToExecutions(FlushEvent event)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
at NHibernate.Impl.SessionImpl.Flush()
at NHibernate.Transaction.AdoTransaction.Commit()
at NHUnitOfWork.Dispose() in NHUnitOfWork.cs:line
at DatabaseActivityOperations.<WriteActivity>d__6.MoveNext() in DatabaseActivityOperations.cs:line 240
My class and mapping look like this.
Activity (For simplicity sake, I've removed several ILists related to this class)
public class Activity {
public Activity() {
Activityschema = new List<ActivitySchema>();
}
public virtual int ActivityKey { get; set; }
public virtual string Activityname { get; set; }
public virtual string Activitydescription { get; set; }
public virtual DateTime Averageactivitytime { get; set; }
public virtual int Averagenumberpeople { get; set; }
public virtual string Worktype { get; set; }
public virtual bool? Canautocomplete { get; set; }
public virtual IList<ActivitySchema> Activityschema { get; set; }
}
ActivityMap
public class ActivityMap : ClassMapping<Activity> {
public ActivityMap() {
Schema("dbo");
Lazy(true);
Id(x => x.ActivityKey, map => { map.Generator(Generators.Identity); });
Property(x => x.Activityname, map => { map.NotNullable(true); map.Length(50); });
Property(x => x.Activitydescription, map => { map.NotNullable(true); map.Length(100); });
Property(x => x.Averageactivitytime, map =>
{
map.NotNullable(true);
map.Type(NHibernateUtil.Time);
});
Property(x => x.Averagenumberpeople, map => { map.NotNullable(true); map.Precision(10); });
Property(x => x.Worktype, map => { map.NotNullable(true); map.Length(50); });
Property(x => x.Canautocomplete);
Bag(x => x.Activityschema, colmap => { colmap.Key(x => x.Column("ActivityKey")); colmap.Inverse(true); }, map => { map.OneToMany(); });
}
}
Finally, here's the Unit of Work class I have:
public class NHUnitOfWork : IDisposable
{
public static string ConnectingString { get; private set; } = #"data source=nh;initial catalog=db;MultipleActiveResultSets=True;";
protected static Configuration _config;
protected static NHibernate.ISessionFactory _sessionFactory;
public NHibernate.ISession Session { get; private set; }
protected NHibernate.ITransaction Transaction { get; set; }
private const System.Data.IsolationLevel ISOLATION_LEVEL = System.Data.IsolationLevel.ReadUncommitted;
private bool RollBack { get; set; } = false;
public NHUnitOfWork(string databaseConnectionString)
{
if (_config == null)
{
var cfg = new Configuration();
cfg.DataBaseIntegration(db =>
{
db.Driver<NHibernate.Driver.SqlClientDriver>();
db.ConnectionString = #"data source=nh;initial catalog=db;MultipleActiveResultSets=True;";
//db.ConnectionString = databaseConnectionString;
db.Dialect<MsSql2012Dialect>();
db.BatchSize = 500;
})
.AddAssembly(typeof(Activity).Assembly)
.SessionFactory()
.GenerateStatistics();
var mapper = new ModelMapper();
mapper.AddMappings(typeof(ActivityMap).Assembly.GetTypes());
cfg.AddMapping(mapper.CompileMappingForAllExplicitlyAddedEntities());
_config = cfg;
_sessionFactory = _config.BuildSessionFactory();
}
Session = _sessionFactory.OpenSession();
Transaction = Session.BeginTransaction(ISOLATION_LEVEL);
RollBack = false;
}
public void Commit()
{
Transaction.Commit();
}
public void Rollback()
{
if (Transaction.IsActive) Transaction.Rollback();
}
public void Dispose()
{
if (RollBack)
{
Transaction.Rollback();
}
else
{
Transaction.Commit();
}
Session.Close();
}
}
By this point, I believe my configuration for this is correct. I'm successfully used Session.Query to read data and that doesn't present issues. The problem comes when I write something like this to add a new record:
var activity = new Activity
{
Activityname = "TestActivity",
Activitydescription = "This is a test",
Averagenumberpeople = 1,
Worktype = "Test",
Canautocomplete = false,
Averageactivitytime = new DateTime(1, 1, 1, 0, 55, 55)
};
using (var uow = new NHUnitOfWork(NHUnitOfWork.ConnectingString))
{
uow.Session.Save(activity); // Produces exception here
//This also produces an exception
//uow.Session.Save(activity, Generators.Identity);
}
I think this has something to do with how I'm mapping the ID in ActivityMap and the generator isn't working as expected. I've tried to change it to several other types and get the same exception, or one stating that it's not able to convert to SystemInt32. I've also tried changing the ID to long and specifiying a data type, but no luck. What do I seem to be doing wrong here?
Literally minutes after I posted this, I figured out that the issue was actually with how I was setting the date time.
new DateTime(1, 1, 1, 0, 55, 55)
It didn't like the "1/1/0001" part, so that seemed to be causing the issue, I'm only concerned with the time piece. Changing the year to something like 2001 fixed the insert and it's working fine, the message was just not very descriptive.

Fluent Nhibernate: Trying to create entity with composite key that is also the keys for two references

The references are unidirectional. The table (StoreProduct) for this entity is actually a join table that has these fields:
Store_id
Product_id
ExtraBit
So I went with an entity having a compoundID (store_id and product_id) and the ExtraBit is just a string:
public class StoreProduct
{
protected StoreProduct():this(null,null,null){ }
public StoreProduct(Store c_Store, Product c_Product, String c_ExtraBit)
{
Store = c_Store;
Product = c_Product;
ExtraBit = c_ExtraBit;
}
public virtual int Product_id { get; set; }
public virtual int Store_id { get; set; }
public virtual Store Store { get; set; }
public virtual Product Product { get; set; }
public virtual String ExtraBit { get; set; }
public override int GetHashCode()
{
return Store.GetHashCode() + Product.GetHashCode();
}
public override bool Equals(object obj)
{
StoreProduct obj_StoreProduct;
obj_StoreProduct = obj as StoreProduct;
if (obj_StoreProduct == null)
{
return false;
}
if (obj_StoreProduct.Product != this.Product && obj_StoreProduct.Store != this.Store)
{
return false;
}
return true;
}
}
And the mapping:
public class Order_DetailMap : ClassMap<StoreProduct>
{
public Order_DetailMap()
{
Table("StoreProduct");
LazyLoad();
CompositeId().KeyProperty(x => x.Store_id).KeyProperty(x => x.Product_id);
References(x => x.Store).ForeignKey("Store_id").Cascade.All();
References(x => x.Product).ForeignKey("Product_id").Cascade.All();
Map(x => x.ExtraBit);
}
}
It doesn't work though, when I tried saving the StoreProduct and its newly created Store and product. Can anyone help? Here is some output:
Unhandled Exception: System.ArgumentOutOfRangeException: Index was out of range.
Must be non-negative and less than the size of the collection.
Parameter name: index
at System.ThrowHelper.ThrowArgumentOutOfRangeException()
at System.Data.SQLite.SQLiteParameterCollection.GetParameter(Int32 index)
at System.Data.Common.DbParameterCollection.System.Collections.IList.get_Item
(Int32 index)
at NHibernate.Type.Int32Type.Set(IDbCommand rs, Object value, Int32 index) in
d:\CSharp\NH\NH\nhibernate\src\NHibernate\Type\Int32Type.cs:line 60
at NHibernate.Type.NullableType.NullSafeSet(IDbCommand cmd, Object value, Int
32 index) in d:\CSharp\NH\NH\nhibernate\src\NHibernate\Type\NullableType.cs:line
180
at NHibernate.Type.NullableType.NullSafeSet(IDbCommand st, Object value, Int3
2 index, ISessionImplementor session) in d:\CSharp\NH\NH\nhibernate\src\NHiberna
te\Type\NullableType.cs:line 139
at NHibernate.Type.ComponentType.NullSafeSet(IDbCommand st, Object value, Int
32 begin, ISessionImplementor session) in d:\CSharp\NH\NH\nhibernate\src\NHibern
ate\Type\ComponentType.cs:line 221
at NHibernate.Persister.Entity.AbstractEntityPersister.Dehydrate(Object id, O
bject[] fields, Object rowId, Boolean[] includeProperty, Boolean[][] includeColu
mns, Int32 table, IDbCommand statement, ISessionImplementor session, Int32 index
) in d:\CSharp\NH\NH\nhibernate\src\NHibernate\Persister\Entity\AbstractEntityPe
rsister.cs:line 2418
Edit: Thanks to the help bellow I seem to have a decent solution:
Store Mapping and Class:
namespace compoundIDtest.Domain.Mappings
{
public class StoreMap : ClassMap<Store>
{
public StoreMap()
{
Id(x => x.Id).Column("Store_id");
Map(x => x.Name);
HasMany(x => x.Staff)
.Inverse()
.Cascade.All();
HasManyToMany(x => x.Products)
.Cascade.All()
.Table("StoreProduct");
}
}
}
namespace compoundIDtest.Domain.Entities
{
public class Store
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual IList<Product> Products { get; set; }
public virtual IList<Employee> Staff { get; set; }
public virtual IList<StoreProduct> StoreProducts { get; set; }
public Store()
{
Products = new List<Product>();
Staff = new List<Employee>();
}
public virtual void AddProduct(Product product)
{
product.StoresStockedIn.Add(this);
Products.Add(product);
}
public virtual void AddEmployee(Employee employee)
{
employee.Store = this;
Staff.Add(employee);
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
public override bool Equals(object obj)
{
Store obj_Store;
obj_Store = obj as Store;
if (obj_Store == null)
{
return false;
}
if (obj_Store.Name != this.Name)
{
return false;
}
return true;
}
}
}
Product Mapping And Class
namespace compoundIDtest.Domain.Mappings
{
public class ProductMap : ClassMap<Product>
{
public ProductMap()
{
Id(x => x.Id).Column("Product_id");
Map(x => x.Name);
Map(x => x.Price);
HasManyToMany(x => x.StoresStockedIn)
.Cascade.All()
.Inverse()
.Table("StoreProduct");
}
}
}
namespace compoundIDtest.Domain.Entities
{
public class Product
{
public virtual int Id { get; private set; }
public virtual string Name { get; set; }
public virtual double Price { get; set; }
public virtual IList<Store> StoresStockedIn { get; set; }
public virtual IList<StoreProduct> StoreProducts { get; set; }
public Product()
{
StoresStockedIn = new List<Store>();
StoreProducts = new List<StoreProduct>();
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
public override bool Equals(object obj)
{
Product obj_Product;
obj_Product = obj as Product;
if (obj_Product == null)
{
return false;
}
if (obj_Product.Name != this.Name)
{
return false;
}
return true;
}
}
}
And StoreProduct
namespace compoundIDtest.Domain.Mappings
{
public class Order_DetailMap : ClassMap<StoreProduct>
{
public Order_DetailMap()
{
Table("StoreProduct");
LazyLoad();
CompositeId().KeyReference(x => x.Store, "Store_id").KeyReference(x => x.Product, "Product_id");
References(x => x.Store, "Store_id").Not.Update().Not.Insert().Cascade.All();
References(x => x.Product, "Product_id").Not.Update().Not.Insert().Cascade.All();
Map(x => x.ExtraBit);
}
}
}
namespace compoundIDtest.Domain.Entities
{
public class StoreProduct
{
public StoreProduct(){}
public virtual Store Store { get; set; }
public virtual Product Product { get; set; }
public virtual String ExtraBit { get; set; }
public override int GetHashCode()
{
if (this.ExtraBit != null)
{
return Store.GetHashCode() + Product.GetHashCode() + ExtraBit.GetHashCode();
}
return Store.GetHashCode() + Product.GetHashCode();
}
public override bool Equals(object obj)
{
StoreProduct obj_StoreProduct;
obj_StoreProduct = obj as StoreProduct;
if (obj_StoreProduct == null)
{
return false;
}
if (obj_StoreProduct.Product != this.Product && obj_StoreProduct.Store != this.Store && obj_StoreProduct.ExtraBit != this.ExtraBit)
{
return false;
}
return true;
}
}
}
And here is code for an app to test the above:
using System;
using System.IO;
using compoundIDtest.Domain.Entities;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
using FluentNHibernate.Conventions;
namespace compoundIDtest
{
class Program
{
private const string DbFile = "firstProgram.db";
static void Main()
{
// create our NHibernate session factory
var sessionFactory = CreateSessionFactory();
using (var session = sessionFactory.OpenSession())
{
// populate the database
using (var transaction = session.BeginTransaction())
{
// create a couple of Stores each with some Products and Employees
var barginBasin = new Store { Name = "Bargin Basin" };
var superMart = new Store { Name = "SuperMart" };
var CornerShop = new Store { Name = "Corner Shop" };
var potatoes = new Product { Name = "Potatoes", Price = 3.60 };
var fish = new Product { Name = "Fish", Price = 4.49 };
var milk = new Product { Name = "Milk", Price = 0.79 };
var bread = new Product { Name = "Bread", Price = 1.29 };
var cheese = new Product { Name = "Cheese", Price = 2.10 };
var waffles = new Product { Name = "Waffles", Price = 2.41 };
var poison = new Product { Name = "Poison", Price = 1.50 };
var daisy = new Employee { FirstName = "Daisy", LastName = "Harrison" };
var jack = new Employee { FirstName = "Jack", LastName = "Torrance" };
var sue = new Employee { FirstName = "Sue", LastName = "Walkters" };
var bill = new Employee { FirstName = "Bill", LastName = "Taft" };
var joan = new Employee { FirstName = "Joan", LastName = "Pope" };
var storeproduct = new StoreProduct { Store = CornerShop, Product = poison, ExtraBit = "Extra Bit"};
//session.SaveOrUpdate(CornerShop);
//session.SaveOrUpdate(poison);
session.Save(storeproduct);
// add products to the stores, there's some crossover in the products in each
// store, because the store-product relationship is many-to-many
AddProductsToStore(barginBasin, potatoes, fish, milk, bread, cheese);
AddProductsToStore(superMart, bread, cheese, waffles);
// add employees to the stores, this relationship is a one-to-many, so one
// employee can only work at one store at a time
AddEmployeesToStore(barginBasin, daisy, jack, sue);
AddEmployeesToStore(superMart, bill, joan);
// save both stores, this saves everything else via cascading
session.SaveOrUpdate(barginBasin);
session.SaveOrUpdate(superMart);
//session.SaveOrUpdate(CornerShop);
//session.SaveOrUpdate(poison);
//session.SaveOrUpdate(storeproduct);
transaction.Commit();
}
}
using (var session = sessionFactory.OpenSession())
{
// retreive all stores and display them
using (var transaction = session.BeginTransaction())
{
var products = session.CreateCriteria(typeof(Product))
.List<Product>();
foreach (var product in products)
{
product.Price = 100;
session.SaveOrUpdate(product);
}
var storeproducts = session.CreateCriteria(typeof(StoreProduct)).List<StoreProduct>();
foreach (StoreProduct storeproduct in storeproducts)
{
if (storeproduct.Store.Name == "SuperMart")
{
storeproduct.ExtraBit = "Thank you, come again";
}
session.SaveOrUpdate(storeproduct);
}
transaction.Commit();
}
}
Console.ReadKey();
}
private static ISessionFactory CreateSessionFactory()
{
return Fluently.Configure()
.Database(SQLiteConfiguration.Standard
.UsingFile(DbFile))
.Mappings(m =>
m.FluentMappings.AddFromAssemblyOf<Program>())
.ExposeConfiguration(BuildSchema)
.BuildSessionFactory();
}
private static void BuildSchema(Configuration config)
{
// delete the existing db on each run
if (File.Exists(DbFile))
File.Delete(DbFile);
// this NHibernate tool takes a configuration (with mapping info in)
// and exports a database schema from it
new SchemaExport(config)
.Create(false, true);
}
private static void WriteStorePretty(Store store)
{
Console.WriteLine(store.Name);
Console.WriteLine(" Products:");
foreach (var product in store.Products)
{
Console.WriteLine(" " + product.Name);
}
Console.WriteLine(" Staff:");
foreach (var employee in store.Staff)
{
Console.WriteLine(" " + employee.FirstName + " " + employee.LastName);
}
Console.WriteLine();
}
public static void AddProductsToStore(Store store, params Product[] products)
{
foreach (var product in products)
{
store.AddProduct(product);
}
}
public static void AddEmployeesToStore(Store store, params Employee[] employees)
{
foreach (var employee in employees)
{
store.AddEmployee(employee);
}
}
}
}
I had a mapping pretty much identical to this and the way I ended up mapping it was like this:
public class Order_DetailMap : ClassMap<StoreProduct>
{
public Order_DetailMap()
{
Table("StoreProduct");
CompositeId()
.KeyReference(x => x.Store, "Store_id")
.KeyReference(x => x.Product, "Product_id");
Map(x => x.ExtraBit);
}
}
Inside of my Store and Product classes I have add and remove methods that make the creation of this middle class almost invisible. Example below:
public class Store
{
public IList<StoreProduct> StoreProducts { get; set; }
//Other properties and Constructors
public virtual void AddProduct(Product productToAdd, string extraBit)
{
StoreProduct newStoreProduct = new StoreProduct(this, productToAdd, extraBit);
storeProducts.Add(newStoreProduct);
}
}
In addition to the above I had HasMany's to a StoreProduct collection in my Store and Product classes that are set to Cascade.AllDeleteOrphan()
I was never able to be able to map the StoreProduct such that when it was saved by itself it would create a new Store and a new Product. I had to eventually map it like the above. So your Store or Product will need to exist before you actually create the relationship (StoreProduct) between them depending on which side you are creating your new StoreProduct from.
Edit:
You may also be able to map it like this to achieve what you are wanting:
public class Order_DetailMap : ClassMap<StoreProduct>
{
public Order_DetailMap()
{
Table("StoreProduct");
CompositeId()
.KeyReference(x => x.Store, "Store_id")
.KeyReference(x => x.Product, "Product_id");
References(x => x.Store, "Store_id")
.Not.Update()
.Not.Insert()
.Cascade.All();
References(x => x.Product, "Product_id")
.Not.Update()
.Not.Insert()
.Cascade.All();
Map(x => x.ExtraBit);
}
}

Using discriminator with Fluent NHibernate

I'm trying to create a discriminator column. This column would hold one of the many statuses available. Like my code will show, each status has a name as well as a background color. Each status shares the same base class.
Here is my code:
public class Item
{
public virtual int Id { get; set; }
public virtual Status ItemStatus { get; set; }
}
public abstract class Status
{
private readonly int _id;
public static readonly Status Foo = new FooStatus(1);
public static readonly Status Bar = new BarStatus(2);
public Status()
{
}
protected Status(int id)
{
_id = id;
}
public virtual int Id { get { return _id; } }
public abstract string Name { get; }
public abstract string BackgroundColor { get; }
}
public class FooStatus : Status
{
public FooStatus()
{
}
public FooStatus(int id)
: base(id)
{
}
public override string Name
{
get { return "Foo Status"; }
}
public override string BackgroundColor
{
get { return "White"; }
}
}
public class BarStatus : Status
{
public BarStatus()
{
}
public BarStatus(int id)
: base(id)
{
}
public override string Name
{
get { return "Bar Status"; }
}
public override string BackgroundColor
{
get { return "Black"; }
}
}
And here is my mapping:
public class ItemMap : ClassMap<Item>
{
public ItemMap()
{
Id(x => x.Id).GeneratedBy.Identity();
DiscriminateSubClassesOnColumn<int>("ItemStatus", 0).AlwaysSelectWithValue();
}
}
Essentially, what I'd like is that if I set ItemStatus to Status.Foo then the ItemStatus column would have a value of 1. What I have now doesn't throw any exceptions, but it always inserts ItemStatus as 0.
This is the inserting code I'm using:
using (var session = sessionFactory.OpenSession())
using (var transaction = session.BeginTransaction())
{
var item = new Item
{
ItemStatus = Status.Foo
};
session.Save(item);
transaction.Commit();
var firstItem = session.Get<Item>(1);
Console.WriteLine(firstItem.ItemStatus.Name);
}
Where can I read up on this topic using FNH?
Before anyone suggests be to check on Google I did search several things but nowhere can I find a full example.
Your SubclassMap would look something like this:
public class FooStatusMap : SubclassMap<FooStatus>
{
public FooStatusMap()
{
DiscriminatorValue(1);
}
}
This is called "table-per-class-hierarchy," and you're right it doesn't look like there are many resources on it out there.
I believe if you don't call DiscriminatorValue in a SubclassMap, NHibernate attempts to discriminate by looking at the name of the subclass being mapped and seeing if it matches up with the value in the discriminator column.
I wouldnt write submaps for all the subclasses you can just do this instead
public class FooMap: ClassMap<T>
{
//other mapping
DiscriminateSubClassesOnColumn("DiscriminatorColumn")
.SubClass<Foo1>(m => { })
.SubClass<Foo2>(m => { })
.SubClass<Foo3>(m => { });
}
Hope that helps
If you're open to the Discriminator column having the class names of the derived classes, you can implement this via automapping.
In your session factory:
private static ISessionFactory CreateSessionFactory()
{
var cfg = new MyMappingConfiguration();
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(c => c.FromConnectionStringWithKey("MyConnectionKey")).FormatSql().ShowSql()
)
.Mappings(m => m.AutoMappings.Add(AutoMap.AssemblyOf<Status>(cfg)
.IncludeBase<Status>()
.Conventions.Add<PrimaryKeyConvention>()))
.BuildSessionFactory();
}
Then add the MyMappingConfiguration override:
public class MappingConfiguration : DefaultAutomappingConfiguration
{
public override bool IsId(Member member)
{
return member.Name == member.DeclaringType.Name + "Id";
}
public override bool IsDiscriminated(Type type)
{
return true;
}
}
Hope that h

NHibernate: Cannot assign property value in entity's constructor

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.
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.

Fluent NHibernate automap inheritance with subclass relationship

I am having an issue with using Fluent NHibernate automapping with Inheritance. Below is my entity setup (abbreviated for simplicity). I have configured Fluent NHibernate to create 1 class for the hierarchy with a discriminator column. The automapping appears to be working correctly as when I generate a database, one table is created named "AddressBase" with a discriminator column that signals what type of address each row is.
The problem lies in the face that when I call the method "GetPrimaryBillingAddress()" on the UserAccount class, instead of just querying Billing Addresses, NHibernate is creating a query that looks at both Billing and Shipping Addresses. It doesn't take into account the discriminator at all. I am assuming there is some sort of configuration I can set but have not been able to find anything.
public abstract class AddressBase : ActiveRecord<AddressBase>
{
public virtual long Id { get; set; }
public virtual string Address1 { get; set; }
}
public class AddressBilling : AddressBase
{
public class TypedQuery : ActiveRecordQuery<AddressBilling> { }
public virtual bool IsPrimary { get; set; }
}
public class AddressShipping : AddressBase
{
public class TypedQuery : ActiveRecordQuery<AddressShipping> { }
[Display(Name = "Is Primary")]
public virtual bool IsPrimary { get; set; }
}
public class UserAccount : ActiveRecord<UserAccount>
{
public virtual long Id { get; set; }
public virtual IList<AddressBilling> BillingAddresses { get; set; }
public virtual IList<AddressShipping> ShippingAddresses { get; set; }
public UserAccount()
{
BillingAddresses = new List<AddressBilling>();
ShippingAddresses = new List<AddressShipping>();
}
public virtual AddressBilling GetPrimaryBillingAddress()
{
if (BillingAddresses.Any(x => x.IsPrimary))
{
return BillingAddresses.Single(x => x.IsPrimary);
}
return BillingAddresses.FirstOrDefault();
}
public virtual AddressShipping GetPrimaryShippingAddress()
{
if (ShippingAddresses.Any(x => x.IsPrimary)) {
return ShippingAddresses.Single(x => x.IsPrimary);
}
return ShippingAddresses.FirstOrDefault();
}
}
UPDATE:
Here is the Mapping override functions used in the automapping:
private static FluentConfiguration GetFluentConfiguration(string connectionStringName = "CS")
{
var autoMapping = AutoMap
.AssemblyOf<Product>(new Mapping.AutoMappingConfiguration())
.Conventions.Setup(c =>
{
c.Add<Mapping.ForeignKeyConvention>();
c.Add<Mapping.DiscriminatorConvention>();
})
.IgnoreBase<AddressBilling.TypedQuery>()
.IgnoreBase<AddressShipping.TypedQuery>()
.IncludeBase<AddressBase>();
return Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2005.ConnectionString(c => c.FromConnectionStringWithKey(connectionStringName)))
.Mappings(m => m.AutoMappings.Add(autoMapping));
}
public class AutoMappingConfiguration : DefaultAutomappingConfiguration
{
public override bool ShouldMap(Type type)
{
var isStatic = type.IsAbstract && type.IsSealed;
return type.Namespace == typeof(Entities.Product).Namespace && !isStatic;
}
public override bool IsDiscriminated(Type type)
{
if (type == (typeof(Entities.AddressBase))) {
return true;
}
return false;
}
public override string GetDiscriminatorColumn(Type type)
{
return "Type";
}
public class DiscriminatorConvention : ISubclassConvention
{
public void Apply(ISubclassInstance instance)
{
//Address
if (instance.Name == typeof(AddressBilling).AssemblyQualifiedName)
{
instance.DiscriminatorValue(Enums.AddressType.BillingAddress);
}
else if (instance.Name == typeof(AddressShipping).AssemblyQualifiedName)
{
instance.DiscriminatorValue(Enums.AddressType.ShippingAddress);
}
}
}
Thanks!
Please, try to change your class UserAccount like this:
public class UserAccount : ActiveRecord<UserAccount>
{
public virtual IList<AddressBase> Addresses { get; set; }
public virtual IList<AddressBilling> BillingAddresses { get {return this.Addresses.OfType<AddressBilling>();} }
public virtual IList<AddressShipping> ShippingAddresses { get {return this.Addresses.OfType<AddressShipping>();} }
// ...
}
Of course, only Addresses property should be mapped here.