Comments are in the code...
Domain Model
public class Product : IEquatable<Product>
{
protected internal virtual int Id { get; set; }
public virtual bool Equals(Product other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Id == Id;
}
#region Implementation of IEquatable
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof (Product)) return false;
return Equals((Product) obj);
}
public override int GetHashCode()
{
return Id;
}
public static bool operator ==(Product left, Product right)
{
return Equals(left, right);
}
public static bool operator !=(Product left, Product right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class Transaction : IEquatable<Transaction>
{
private IList<TransactionDetail> details;
// This is declared protected because it is an implementation
// detail that does not belong in the public interface of the
// domain model. It is declared internal so the fluent mapping
// can see it.
protected internal virtual int Id { get; set; }
public virtual double TotalAmount { get; set; }
// This is declared as a IList even though it is recommended
// to use ICollection for a Bag because the the Testing Framework
// passes a HashSet to NHibernate and NHibernate attempts to cast
// it to a List since it is declared a Bag in the mapping.
public virtual IList<TransactionDetail> Details
{
// I lazily initialize the collection because I do not like
// testing for nulls all through my code but you may see
// issues with this if you use Cascade.AllDeleteOrphan in
// the mapping.
get { return details ?? (details = new List<TransactionDetail>()); }
set { details = value; }
}
#region Implementation of IEquatable
// Do not forget to declare this function as virtual or you will
// get a mapping exception saying that this class is not suitable
// for proxying.
public virtual bool Equals(Transaction other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Id == Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof(Transaction)) return false;
return Equals((Transaction)obj);
}
public override int GetHashCode()
{
return Id;
}
public static bool operator ==(Transaction left, Transaction right)
{
return Equals(left, right);
}
public static bool operator !=(Transaction left, Transaction right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class TransactionDetail : IEquatable<TransactionDetail>
{
protected internal virtual int Id { get; set; }
public virtual double Amount { get; set; }
#region Implementation of IEquatable
public virtual bool Equals(TransactionDetail other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Id == Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof (TransactionDetail)) return false;
return Equals((TransactionDetail) obj);
}
public override int GetHashCode()
{
return Id;
}
public static bool operator ==(TransactionDetail left, TransactionDetail right)
{
return Equals(left, right);
}
public static bool operator !=(TransactionDetail left, TransactionDetail right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class ProductTransaction : Transaction, IEquatable<ProductTransaction>
{
public virtual double Discount { get; set; }
// This is declared 'new' because C# does not support covariant
// return types until v4.0. This requires clients to explicitly
// cast objects of type Transaction to ProductTransaction before
// invoking Details. Another approach would be to change the
// property's name (e.g., ProductDetails) but this also requires
// casting.
public virtual new IList<ProductTransactionDetail> Details
{
get { return base.Details.OfType<ProductTransactionDetail>().ToList(); }
set { base.Details = null == value ? null : value.Cast<TransactionDetail>().ToList(); }
}
#region Implementation of IEquatable
public virtual bool Equals(ProductTransaction other)
{
return base.Equals(other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return Equals(obj as ProductTransaction);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator ==(ProductTransaction left, ProductTransaction right)
{
return Equals(left, right);
}
public static bool operator !=(ProductTransaction left, ProductTransaction right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
public class ProductTransactionDetail : TransactionDetail, IEquatable<ProductTransactionDetail>
{
public virtual Product Product { get; set; }
#region Implementation of IEquatable
public virtual bool Equals(ProductTransactionDetail other)
{
return base.Equals(other);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return Equals(obj as ProductTransactionDetail);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator ==(ProductTransactionDetail left, ProductTransactionDetail right)
{
return Equals(left, right);
}
public static bool operator !=(ProductTransactionDetail left, ProductTransactionDetail right)
{
return !Equals(left, right);
}
#endregion Implementation of IEquatable
}
Fluent Mapping
internal sealed class ProductMap : ClassMap<Product>
{
internal ProductMap()
{
Table("Product")
;
LazyLoad()
;
Id(x => x.Id)
.Column("ProductId")
.GeneratedBy.Identity()
;
}
}
internal sealed class TransactionMap : ClassMap<Transaction>
{
internal TransactionMap()
{
// The table name is surrounded by back ticks because
// 'Transaction' is a reserved word in SQL. On SQL Server,
// this translates to [Transaction].
Table("`Transaction`")
;
LazyLoad()
;
Id(x => x.Id)
.Column("TransactionId")
.GeneratedBy.Identity()
;
Map(x => x.TotalAmount)
.Column("TotalAmount")
.Not.Nullable()
;
// You should consider treating TransactionDetail as a value
// type that cannot exist outside a Transaction. In this case,
// you should mark the relation as Not.Inverse and save or
// update the transaction after adding a detail instead of
// saving the detail independently.
HasMany(x => x.Details)
.KeyColumn("TransactionID")
.Cascade.All()
.Not.Inverse()
.AsBag()
;
// You have a Type column in your example, which I took to
// mean that you wanted to use the Table Per Hierarchy
// strategy. It this case you need to inform NHibernate
// which column identifies the subtype.
DiscriminateSubClassesOnColumn("Type")
.Not.Nullable()
;
}
}
internal sealed class TransactionDetailMap : ClassMap<TransactionDetail>
{
internal TransactionDetailMap()
{
Table("TransactionDetail")
;
LazyLoad()
;
Id(x => x.Id)
.Column("TransactionDetailId")
.GeneratedBy.Identity()
;
Map(x => x.Amount)
.Column("Amount")
.Not.Nullable()
;
}
}
internal sealed class ProductTransactionMap : SubclassMap<ProductTransaction>
{
internal ProductTransactionMap()
{
KeyColumn("TransactionId")
;
// I recommend giving the discriminator column an explicit
// value for a subclass. Otherwise, NHibernate uses the fully
// qualified name of the class including the namespace. If
// you later move the class to another namespace or rename
// the class then you will have to migrate all of the data
// in your database.
DiscriminatorValue("TransactionKind#product")
;
Map(x => x.Discount)
.Column("Discount")
.Nullable()
;
// Do not map the over-ridden version of
// the Details property. It is handled
// by the base class mapping.
}
}
internal sealed class ProductTransactionDetailMap : SubclassMap<ProductTransactionDetail>
{
internal ProductTransactionDetailMap()
{
// There was no Type column in your example for this table,
// whcih I took to mean that you wished to use a Table Per
// Class strategy. In this case, you need to provide the
// table name even though it is a subclass.
Table("ProductTransactionDetail")
;
KeyColumn("TransactionDetailId")
;
References(x => x.Product)
.Column("ProductId")
.Not.Nullable()
;
}
}
Unit Tests
[TestClass]
public class UnitTest1
{
private static ISessionFactory sessionFactory;
private static Configuration configuration;
[TestMethod]
public void CanCorrectlyMapTransaction()
{
using (var dbsession = OpenDBSession())
{
var product = new Product();
dbsession.Save(product);
new PersistenceSpecification<Transaction>(dbsession)
.CheckProperty(t => t.TotalAmount, 100.0)
.CheckComponentList(
t => t.Details,
new[] {
new TransactionDetail {
Amount = 75.0,
},
new ProductTransactionDetail {
Amount = 25.0,
Product = product,
},
}
)
.VerifyTheMappings()
;
}
}
private static Configuration Configuration
{
get
{
return configuration ?? (
configuration = forSQLite().Mappings(
m => m.FluentMappings
.Conventions.Setup(x => x.Add(AutoImport.Never()))
.Add(typeof(ProductMap))
.Add(typeof(ProductTransactionMap))
.Add(typeof(ProductTransactionDetailMap))
.Add(typeof(TransactionMap))
.Add(typeof(TransactionDetailMap))
)
.BuildConfiguration()
);
}
}
private static ISessionFactory SessionFactory
{
get { return sessionFactory ?? (sessionFactory = Configuration.BuildSessionFactory()); }
}
private static ISession OpenDBSession()
{
var session = SessionFactory.OpenSession();
// Ideally, this would be done once on the database
// session but that does not work when using SQLite as
// an in-memory database. It works in all other cases.
new SchemaExport(configuration)
.Execute(
true, // echo schema to Console
true, // create schema on connection
false, // just drop do not create
session.Connection, // an active database connection
null // writer for capturing schema
);
return session;
}
private static FluentConfiguration forSQLite()
{
return Fluently.Configure()
.Database(
SQLiteConfiguration
.Standard
.InMemory()
.ShowSql()
);
}
}