Wednesday, September 9, 2009

Entity Framework and Plain Old CSharp Objects

Introduction

This will be a new feature on .NET 4.0 but for now we can still get a glimpse off it with this adapter written by one of the EF developers. To use POCOs you still need to write the metadata file(s) that represent the storage scheme, the conceptual scheme and the mappings between them. In .NET 4.0 it will also be possible to use only code to create the metadata (if this is good or bad is out of scope :) ).

When we create a EF model for a given database the developer tools create a EDMX file and use a code generator to output code for what is modeled in the EDMX file. This includes the business entities. This is really cool to make those quick demos we see everywhere in the Internet but on large applications things tend to get complicated. The idea of the domain model is that you create a rich object model to represent the domain and that is not tied to any technical or architectural detail of the implementation. It is as close as object can be to the real world equivalents. But the original implementation of EF lead to domain models that where tied and coupled to the details of Object to Relational Mapping.

One good example I use all the time to illustrate how things can get ugly when ORMs mess with the domain model is the number of times I got to write the Customer class. There is a Customer class with attributes for one ORM, another one has some methods that contain mapping code, others derive from a base class that is tied to the ORM and some put constraints to how you write the class code such as forcing things to be virtual. Wouldn’t it be perfect if I could just write the Customer class using my own way of doing things and could tell the ORM how to map it without having to do any change on my code? The EFPocoAdapter is about that.

Inside the EDMX file there are actually three models. Originally they where separated files and it is still possible to keep them separated. This is the strategy I like the most and the one used in this article. They have the extensions ssdl, csdl and msl respectively storage schema description language, conceptual schema description language and mapping schema language.

In this article I will use SQL Server 2008 R2 and AdventureWorks for SQL Server 2008 as the database. We will map the products table and count the rows on it using POCOs and LINQ.

Implementing a sample application with EF and POCOs

Our demo application will follow the following architecture. We will place our business objects inside a business objects assembly with no further dependencies. The code that connects those objects to the entity framework is named on the diagram as Business Objects Adapter. It is built on top of EF POCO Adapter who is built on top of Entity Framework.

entity framework with POCO architecture

If we would like to divide our domain in more than one assembly then there would be one adapter per business object assembly.

The first step is writing the business objects for the application and our business object could not be simpler:

   1: public class Product
   2: {
   3:     public string Name
   4:     {
   5:         get;
   6:         set;
   7:     }
   8:  
   9:     public int ProductNumber
  10:     {
  11:         get;
  12:         set;
  13:     }
  14: }

The next step is to write the EF metadata that describes this object, the storage table that is associated with it and how they map to each other. Starting by the conceptual model (the Product business object):

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <Schema Namespace="AdventureWorks" Alias="Self" xmlns="http://schemas.microsoft.com/ado/2006/04/edm"
   3:         xmlns:objectmapping="http://code.msdn.microsoft.com/EFPocoAdapter/ObjectMapping.xsd"
   4:         >
   5:   <EntityContainer Name="AdventureWorksEntities">
   6:     <EntitySet Name="Products" EntityType="AdventureWorks.Product" />
   7:   </EntityContainer>
   8:  
   9:   <EntityType Name="Product">
  10:     <Key>
  11:       <PropertyRef Name="ProductNumber"/>
  12:     </Key>
  13:     <Property Name="ProductNumber" Type="Int32" Nullable="false" />
  14:   </EntityType>
  15:   
  16: </Schema>

Next comes the storage model:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <Schema Namespace="AdventureWorks.Store" Alias="Self" xmlns="http://schemas.microsoft.com/ado/2006/04/edm/ssdl" Provider="System.Data.SqlClient" ProviderManifestToken="2005">
   3:   <EntityContainer Name="dbo">
   4:     <EntitySet Name="Product" EntityType="AdventureWorks.Store.Product" Schema="Production" />
   5:   </EntityContainer>
   6:   
   7:   <EntityType Name="Product">
   8:     <Key>
   9:       <PropertyRef Name="ProductID" />
  10:     </Key>
  11:     <Property Name="ProductID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
  12:   </EntityType>
  13:   
  14: </Schema>

And finally the mapping:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <Mapping Space="C-S" xmlns="urn:schemas-microsoft-com:windows:storage:mapping:CS">
   3:   <EntityContainerMapping
   4:     StorageEntityContainer="dbo"
   5:     CdmEntityContainer="AdventureWorksEntities">
   6:     <EntitySetMapping Name="Products">
   7:       <EntityTypeMapping TypeName="AdventureWorks.Product">
   8:         <MappingFragment StoreEntitySet="Product">
   9:           <ScalarProperty Name="ProductNumber" ColumnName="ProductID"/>
  10:         </MappingFragment>
  11:       </EntityTypeMapping>
  12:     </EntitySetMapping>
  13:   </EntityContainerMapping>
  14: </Mapping>

There are subtle differences between the C# object Product and the Product table. For instance I decided to call ProductNumber to the ProductId table. The entity frameworks supports much more than this simple column mapping but I am trying to build a working demo that is as simple as possible to help on getting started.

The EFPocoAdapter contains a small command line code generator that will output the adapter files for us. I made a small batch file to do this in order to user relative file paths. In my source tree integrating the commands in the pre-build event as suggested in the EFPocoAdapter would generate a command that is too long for the command processor. (One thing I can’t understand is why do we still have limits for paths and commands.)

   1: SET CLASSGEN=C:\prj\source\_ExternalReferences\EFPocoAdapter\EFPocoClassGen\bin\Debug\EFPocoClassGen.exe
   2:  
   3: %CLASSGEN% /verbose "/incsdl:..\AdventureWorks.csdl" 
   4:     "/ref:..\Pedrosal.BusinessObjects\bin\Debug\Pedrosal.BusinessObjects.dll" 
   5:     "/outputfile:PocoAdapter.cs" /map:AdventureWorks=Pedrosal.BusinessObjects
   6:  
   7: %CLASSGEN% /verbose "/incsdl:..\AdventureWorks.csdl" 
   8:     "/ref:..\Pedrosal.BusinessObjects\bin\Debug\Pedrosal.BusinessObjects.dll" 
   9:     "/outputfile:AdventureWorksEntities.cs" /map:AdventureWorks=Pedrosal.BusinessObjects 
  10:     /mode:PocoContainer
  11:  
  12: PAUSE

Finally the test client code:

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         using(AdventureWorksEntities ent = new AdventureWorksEntities())
   6:         {
   7:             // test 1 - count the products
   8:  
   9:             var productCount = ent.Products.Count();
  10:             Console.WriteLine("You have {0} products in the database.", productCount.ToString());
  11:         }
  12:     }
  13: }

The connection to the database is made because on the generated code the Entity Framework is instructed to read it from the App.config file connection named AdventureWorksEntities. On that connection we must tell EF where the metadata files are and the easiest way is to have them copied to the output path.

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <connectionStrings>
   4:     <add name="AdventureWorksEntities" connectionString="metadata=AdventureWorks.csdl|AdventureWorks.ssdl|AdventureWorks.msl;provider=System.Data.SqlClient;provider connection string=&quot;Data Source=.;Initial Catalog=AdventureWorks2008;Integrated Security=True;MultipleActiveResultSets=True&quot;" providerName="System.Data.EntityClient" />
   5:   </connectionStrings>
   6: </configuration>

Download sample here.

Have fun,

No comments: