I am wondering whether the following inheritance pattern is a good idea. I noticed that Sales Orders, Sales Quotes and Sales Invoices all follow a similar Master/ Detail pattern ( i.e a Header with Lines ) so I wanted to be able to inherit from base classes that encapsulated their common properties.
I will start with my test to explain it.
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestCreateSalesOrderFromQuote()
{
using (var ctx = new CrmDbContext())
{
var customer = new Customer {Name = "test"};
ctx.Customers.Add(customer);
var quote = new SalesQuote {Customer = customer, CreatedOn = DateTime.Now};
quote.Lines.Add(new SalesQuoteLine
{
Description = "Box of widgets",
Price = (decimal) 100.50
});
ctx.SalesQuotes.Add(quote);
var oc = ((IObjectContextAdapter) ctx).ObjectContext;
var order = Functions.CloneSalesObject<SalesOrder, SalesOrderLine>(oc, quote) as SalesOrder;
Assert.AreEqual(order.Customer.Id, quote.Customer.Id);
Assert.AreEqual(order.Lines[0].Description, quote.Lines[0].Description);
}
}
}
The function used in the test
public static BaseSalesHeader CloneSalesObject<T1, T2>( ObjectContext oc, BaseSalesHeader baseSale) where T1 : BaseSalesHeader where T2 : BaseSalesLine
{
var obj = oc.CreateObject<T1>();
obj.CreatedOn = DateTime.Now;
obj.Customer = baseSale.Customer;
foreach (var line in baseSale.CopyOfLines)
{
var newLine = oc.CreateObject<T2>();
// clone properties needed from BaseSalesLine
newLine.Price = line.Price;
newLine.Description = line.Description;
obj.AddLine(newLine);
}
return obj ;
}
Next the context
public class CrmDbContext : DbContext
{
public CrmDbContext()
: base("name=ConnectionString")
{
}
public DbSet<ModuleInfo> ModulesInfo { get; set; }
public DbSet<SalesOrder> SalesOrders { get; set; }
public DbSet<SalesQuote> SalesQuotes { get; set; }
public DbSet<Customer> Customers { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
modelBuilder.Entity<SalesOrder>().HasRequired(c => c.Customer);
modelBuilder.Entity<SalesQuote>().HasRequired(c => c.Customer);
modelBuilder.Entity<SalesOrder>().HasMany(p => p.Lines).WithRequired(t => t.SalesOrder).WillCascadeOnDelete(true);
modelBuilder.Entity<SalesQuote>().HasMany(p => p.Lines).WithRequired(t => t.SalesQuote).WillCascadeOnDelete(true);
}
}
and now the business classes
public abstract class BaseSalesHeader : BasicBo
{
public DateTime CreatedOn { get; set; }
public virtual Customer Customer { get; set; }
public string CustomerReference { get; set; }
[Notmapped] // needed to avoid a BaseSalesHeader being created
public abstract List<BaseSalesLine> CopyOfLines { get; }
public abstract void AddLine(BaseSalesLine line);
}
public abstract class BaseSalesLine : BasicBo
{
[NotMapped] // needed to avoid a BaseSalesLine table being created
public abstract BaseSalesHeader SalesHeader { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
public abstract class BasicBo
{
public virtual int Id { get; set; }
}
public class Customer : BasicBo
{
public string Name { get; set; }
}
public class SalesOrder : BaseSalesHeader
{
public SalesOrder()
{
Lines = new List<SalesOrderLine>();
}
public virtual List<SalesOrderLine> Lines { get; set; }
public override List<BaseSalesLine> CopyOfLines => Lines.Cast<BaseSalesLine>().ToList();
public override void AddLine(BaseSalesLine line)
{
Lines.Add(line as SalesOrderLine);
}
}
public class SalesOrderLine : BaseSalesLine
{
public virtual SalesOrder SalesOrder { get; set; }
public override BaseSalesHeader SalesHeader
{
get { return SalesOrder; }
set { SalesOrder = (SalesOrder) value; }
}
}
public class SalesQuote : BaseSalesHeader
{
public SalesQuote()
{
Lines = new List<SalesQuoteLine>();
}
public List<SalesQuoteLine> Lines { get; set; }
public override List<BaseSalesLine> CopyOfLines => Lines.Cast<BaseSalesLine>().ToList();
public override void AddLine(BaseSalesLine line)
{
Lines.Add(line as SalesQuoteLine);
}
}
public class SalesQuoteLine : BaseSalesLine
{
public virtual SalesQuote SalesQuote { get; set; }
public override BaseSalesHeader SalesHeader
{
get { return SalesQuote; }
set { SalesQuote = (SalesQuote)value; }
}
}
My test passes, but is my idea sound? [Update1] Here is a database diagram of the database I get. The reason I have the [NotMapped] attribute is to prevent Entity Framework from creating the base tables. I want TPT not TPH
Thus the purpose of the base classes are for code re-use.
BaseSalesHeader
should inherit fromBaseSalesLine
, otherwise it wouldn't compile since theoverride
doesn't match anything. \$\endgroup\$BaseSalesHeader
that requiresSalesOrder
values doesn't make sense. Only theSalesOrder
property should be enough. This probably indicates some flaw in the inheritance scheme. Also, this will introduce mapping problems. And that's the problem with reviewing this, we don't see all the moving parts. The EF mappings, including the inheritance implementation, can't be omitted. \$\endgroup\$