POCO improves inheritance for LINQ to SQL
Have you ever needed to use inheritance with LINQ To SQL? The designer experience isn’t exactly what your OO professor probably had in mind when it comes to modelling inheritance and to honest the whole process feels wrong. Let me explain.
I don’t like the whole ‘partial’ class approach to inheritance to begin with. I want the freedom to inherit without the need for using Partial classes and better still I don’t want my business objects to be tied to the LINQ To SQL platform. If we consider the following scenario where my LINQ To SQL designer experience looks like this:
Figure 1.0
What figure 1.0 demonstrates is the we have an employee entity, from which three other entities all inherit. SalaryEmployee, CommissionEmployee and ContractEmployee all have employee as their base class. In the case of SalaryEmployee, it distinctly encapsulates a property for the middle initial via the property ‘minit’. Sure the designer looks great but from the perspective of the code, the designer has generated things don’t look so nice to me, as demonstrated in the following code listing.
Code Listing 1.0
[Table(Name="dbo.employee")] [InheritanceMapping(Code="1", Type=typeof(SalaryEmployee)] [InheritanceMapping(Code="2", Type=typeof(CommissionEmployee))] [InheritanceMapping(Code="3", Type=typeof(ContractEmployee))] public partial class employee : INotifyPropertyChanging, INotifyPropertyChanged { //……. } public partial class SalaryEmployee : employee { private System.Nullable<char> _minit; #region Extensibility Method Definitions partial void OnLoaded(); partial void OnValidate(System.Data.Linq.ChangeAction action); partial void OnCreated(); partial void OnminitChanging(System.Nullable<char> value); partial void OnminitChanged(); #endregion public SalaryEmployee() { OnCreated(); } [Column(Storage="_minit", DbType="Char(1)")] public System.Nullable<char> minit { get { return this._minit; } set { if ((this._minit != value)) { this.OnminitChanging(value); this.SendPropertyChanging(); this._minit = value; this.SendPropertyChanged("minit"); this.OnminitChanged(); } } } } public partial class CommissionEmployee: employee { //……. } public partial class ContractEmployee: employee { //……. }
As demonstrated above, the designer generates code that comes decorated with quite a bit of LINQ To SQL infrastructure attributes or noise as I like to call it, along with some partial methods that developers can implement in their own partial classes designed to extend those generated by the designer. I have included the entire code generated for the SalaryEmployee entity for the sake of demonstration and to show that further LINQ To SQL infrastructure code has been used to decorate the entity. Now it’s very important to point out here, that this code should NOT be touched in the IDE and it’s linked inextricably to the designer, so if I just start to add polymorphic behaviours or new members to the entities via the generated code, then the moment I refresh or add anything via the designer, it will blow away any changes made by directly to the code. Microsoft have designed the entity classes in a such a way that by using partial classes we can implement a subsequent file to that provided by the code generator and implement all of our new behaviours and members using our own hand rolled partial class. I could do something like this:
Code Listing 2.0
public interface IEmployee { bool QualifiesAsPrefered(); } public partial class employee : IEmployee { #region IEmployee Members public virtual bool QualifiesAsPrefered() { return false; } #endregion }
I could create this code above in a new code file and thus extend my designer generated entities by using an interface to describe any polymorphic intent against my partial class. This does begin to become unwieldy in my opinion and I consider the use of the partial class a bit of code smell to be blunt and its really only required because of the need to maintain synchronicity between the code generated entity and the designer.
Now this is not to say that developers should not use the designer and take the approach just demonstrated, in fact I should point out that given the right circumstances, such an approach is completely justified. Lets say you work for a small or medium sized project driven business and there is a need to write a data driven application that contains less than significant business logic, has low support requirements, a small user audience and is required in the shortest amount of time possible. Sounds familiar right! Well those are typical times when you might want to do just that. However, what about if I have a team of 6 - 8 developers on a project spanning eight months and the application is driven heavily by data? This is the kind of circumstance that this approach just wont feel comfortable and will cause you headaches because it’s not particularly reusable or portable across a large team, it also leads to business entities getting tied to a Data Access strategy and will have the developer team dragging around all that infrastructure code with them. What is this going to mean for sharing entities within services or what are my serialisation concerns and should I consider breaking my partial classes into separate projects and…….you can see where this is leading, more confusion and pain for all. So what’s the solution?
By using a POCO (Plain Old CLR Object) approach (see DDD) along with LINQ To SQL mapping, we can choose to use our tried and trusted OO skills to describe our classes (entities), cutting down on the number of artifacts it takes to achieve that and in so doing simplify our lives by making the code less ambiguous. So how different would that be? Lets start with the mapping file used to map entities to tables in the database.
Code Listing 3.0
<Database Name="Northwind" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007"> <Table Name="Customers" Member="Customer"> <Type Name="Org.TechAvalanche.Orm.Tests.Domain.Customer"> <Column Name="CustomerID" Member="CustomerID" DbType="NChar(5) NOT NULL" IsPrimaryKey="true" CanBeNull="false" UpdateCheck="Never" /> <Column Name="CompanyName" Member="CompanyName" DbType="NVarChar(40) NOT NULL" CanBeNull="false" UpdateCheck="Never" /> <Column Name="ContactName" Member="ContactName" DbType="NVarChar(30)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="ContactTitle" Member="ContactTitle" DbType="NVarChar(30)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="Address" Member="Address" DbType="NVarChar(60)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="City" Member="City" DbType="NVarChar(15)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="Region" Member="Region" DbType="NVarChar(15)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="PostalCode" Member="PostalCode" DbType="NVarChar(10)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="Country" Member="Country" DbType="NVarChar(15)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="Phone" Member="Phone" DbType="NVarChar(24)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="Fax" Member="Fax" DbType="NVarChar(24)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="CustType" Member="CustomerType" DbType="Int" CanBeNull="false" IsDiscriminator="true" UpdateCheck="Never" /> <Association Name="Customer_Order" Member="Orders" OtherKey="CustomerID" /> <!– These Two Elements used for inheritance –> <Type Name="Org.TechAvalanche.Orm.Tests.Domain.InternalCustomer" InheritanceCode="1" IsInheritanceDefault="true"/> <Type Name="Org.TechAvalanche.Orm.Tests.Domain.ExternalCustomer" InheritanceCode="2" IsInheritanceDefault="false"/> </Type> </Table> <Table Name="Orders" Member="Order"> <Type Name="Org.TechAvalanche.Orm.Tests.Domain.Order"> <Column Name="OrderID" Member="OrderID" DbType="Int NOT NULL IDENTITY" IsPrimaryKey="true" IsDbGenerated="true" CanBeNull="false" UpdateCheck="Never" /> <Column Name="CustomerID" Member="CustomerID" DbType="NChar(5)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="EmployeeID" Member="EmployeeID" DbType="Int" CanBeNull="true" UpdateCheck="Never" /> <Column Name="OrderDate" Member="OrderDate" DbType="DateTime" CanBeNull="true" UpdateCheck="Never" /> <Column Name="RequiredDate" Member="RequiredDate" DbType="DateTime" CanBeNull="true" UpdateCheck="Never" /> <Column Name="ShippedDate" Member="ShippedDate" DbType="DateTime" CanBeNull="true" UpdateCheck="Never" /> <Column Name="ShipVia" Member="ShipVia" DbType="Int" CanBeNull="true" UpdateCheck="Never" /> <Column Name="Freight" Member="Freight" DbType="Money" CanBeNull="true" UpdateCheck="Never" /> <Column Name="ShipName" Member="ShipName" DbType="NVarChar(40)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="ShipAddress" Member="ShipAddress" DbType="NVarChar(60)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="ShipCity" Member="ShipCity" DbType="NVarChar(15)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="ShipRegion" Member="ShipRegion" DbType="NVarChar(15)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="ShipPostalCode" Member="ShipPostalCode" DbType="NVarChar(10)" CanBeNull="true" UpdateCheck="Never" /> <Column Name="ShipCountry" Member="ShipCountry" DbType="NVarChar(15)" CanBeNull="true" UpdateCheck="Never" /> <Association Name="Order_OrderLine" Member="OrderLines" OtherKey="OrderID" /> <Association Name="Customer_Order" Member="Customer" ThisKey="CustomerID" IsForeignKey="true" /> </Type> </Table> <Table Name="[Order Details]" Member="OrderLine"> <Type Name="Org.TechAvalanche.Orm.Tests.Domain.OrderLine"> <Column Name="OrderID" Member="OrderID" DbType="Int NOT NULL" IsPrimaryKey="true" CanBeNull="false" UpdateCheck="Never" /> <Column Name="ProductID" Member="ProductID" DbType="Int NOT NULL" IsPrimaryKey="true" CanBeNull="false" UpdateCheck="Never" /> <Column Name="UnitPrice" Member="UnitPrice" DbType="Money NOT NULL" CanBeNull="false" UpdateCheck="Never" /> <Column Name="Quantity" Member="Quantity" DbType="SmallInt NOT NULL" CanBeNull="false" UpdateCheck="Never" /> <Column Name="Discount" Member="Discount" DbType="Real NOT NULL" CanBeNull="false" UpdateCheck="Never" /> <Association Name="Order_OrderLine" Member="Order" ThisKey="OrderID" IsForeignKey="true" /> </Type> </Table> </Database>
The two entries above that sit directly beneath the comment highlighted in large font demonstrate how to set up mapping XML for regular DotNet CLR objects that are NOT generated by the designer and account for inheritance. The entity code to go with this mapping follows below.
Code Listing 4.0
interface ICustomerData { string Address { get; set; } string City { get; set; } string CompanyName { get; set; } string ContactName { get; set; } string ContactTitle { get; set; } string Country { get; set; } string CustomerID { get; set; } string Fax { get; set; } IList<Order> Orders { get; set; } string Phone { get; set; } string PostalCode { get; set; } string Region { get; set; } int CustomerType { get; set; } } public interface ICustomerLoyaltyDiscount { void LoyaltyDiscount(); } interface ICustomer : ICustomerData, ICustomerLoyaltyDiscount { } public class Customer : ICustomer { public void LoyaltyDiscount() { foreach (Order o in Orders) { o.Freight = 0M; } }
} public class InternalCustomer : Customer { //………etc } public class ExternalCustomer : Customer { //………etc }
If you really must have a designer to view and work with your entities then it’s no sweat to create a class diagram in VS.Net and enjoy a similar design experience as you would with the LINQ To SQL designer however enjoy the benefits of going POCO at the same time.
Figure 2.0
The consumer code would look something like this:
public void SimpleRepositoryFetchingStrategyTest() { Specification<Customer> simonsCoSpec = new Specification<Customer> (c => c.Country == "Germany"); DataContext ctx = GetContext(); Repository<Customer> customerRepository = new Repository<Customer>(ctx); CustomerLoyaltyDiscountFetchingStrategy strategy = new CustomerLoyaltyDiscountFetchingStrategy(); var custs = customerRepository.All(simonsCoSpec, strategy); foreach (var cust in custs) { cust.LoyaltyDiscount(); Console.WriteLine("The Name of the Customer " + "from Germany is {0}", cust.CompanyName); foreach (var order in cust.Orders) { Console.WriteLine("\tOrder Number {0}", order.OrderID); foreach (var orderLine in order.OrderLines) { Console.WriteLine("\t\tProduct ID {0} Amount {1}", orderLine.ProductID, (orderLine.Quantity * orderLine.UnitPrice)); } } } }
private static DataContext GetContext() { //get a stream reader from the mapping file StreamReader sr = new StreamReader(@"D:\Mapping\DomainMapping.xml"); //set the mapping source up XmlMappingSource mapping = XmlMappingSource.FromStream(sr.BaseStream); DataContext db = null; //new up a persistance repository db = new DataContext ("server=localhost;initial catalog=Northwind;" + "user id=sa;password=mrbig", mapping); return db; }
I will shortly be posting a framework that makes working in a POCO and DDD style much simpler. The Framework includes implemented Specification Patterns, Fetching Strategies and Extensible Repositories for your LINQ To SQL pleasure.
1 comment



