Tuesday, December 7, 2010

Entity Framework Pattern

In ASP.NET, we use the Entity Framework (EF) to connect to do all data manipulation. Through much trial and error, we've settled on the following pattern for implementing a successful EF project.

First, create a project where the EF's EDMX file will reside. We'll call this project MLS.SampleEF.Data.

Add a new item to that project called ADO.NET Entity Data model. Call it DataModel.edmx. Once you have gone through the wizard and created an edmx file based on your database schema, add a new class to the project called Context.cs. Add the following to it:


public static DataModelEntities getContext() {
return new DataModelEntities();
}


This class will be used in every EF query. You can modify it to provide a specific connection string if desired. Otherwise, it uses what's stored in the .config file of your executing assembly.

We have another project that works as the "business" layer. In this example, it'll be called MLS.Sample.Business. In this project we create classes that will be our business object representation of the data.

For example, assuming we have a Customer table. We create a CustomerBO class which will contain properties that our GUI will access and use:


public class CustomerBO {
public int customerId {get; set;}
public string firstName {get; set;}
public string lastName {get; set;}
public string fullName {
get {
return string.Format("{0}, {1}", lastName, firstName); }
}
}
}


In each of our business classes, we have a "wrapper" which takes an IQueryable object (which comes from the EF), and maps it over to the business object. For example, assume we have this static method which retrieves a customer list and returns a List<CustomerBO>. In the sample below, the datamodel contains the definition for the Customer class, while CustomerBO is our business object that will be returned to the GUI.


public static List<CustomerBO> getCustomers() {
using (var context = Context.getContext()) {
var lst = from c in context.Customers
select c;

// Wrap the EF results to a List<Customer>
return toCustomerList(lst);

}
}

/// Wraps a queryable object to a List<Customer>
private static List<CustomerBO> toCustomerList(IQueryable<Customer> queryCustomerData) {
var data = from c in queryCustomerData
select new CustomerBO() {
customerId = c.customerId,
firstName = c.firstName,
lastName = c.lastName
};

return data.ToList();
}


A few notes about the toCustomerList method. Recall that EF queries don't actually hit the database when they are "created" - instead, the actual query occurs at the time the data is accessed. In our sample above, the database isn't queried until the return data.ToList() line in the toCustomerList method.

We also make sure that every method which returns data to the GUI goes through the toCustomerList method. It ensures us that the CustomerBO object will always be populated with the same data regardless of the EF query used to create the data. It also lets us populate BO object properties that wouldn't normally be selected in a query. For example, let's say we added an OrderCount property to our bo and wanted to make sure that it was always populated for the GUI, we could change the toCustomerList method to do this:


public int orderCount {get; set;}

/// Wraps a queryable object to a List<Customer>
private static List<CustomerBO> toCustomerList(IQueryable<Customer> queryCustomerData) {
var data = from c in queryCustomerData
select new CustomerBO() {
customerId = c.customerId,
firstName = c.firstName,
lastName = c.lastName,
orderCount = c.CustomerOrders.Count()
};

return data.ToList();
}


Now if we add a new method to retrieve a single customer and wrap it using our toCustomerList method, we don't have to make sure to explicitely set the orderCount in our getCustomer method because it will be handled for us in the "wrapper":


public static CustomerBO getCustomer(int customerId) {
using (var context = Context.getContext()) {
var lst = from c in context.Customers
where c.customerId == customerId
select c;

// Wrap the EF results to a List<Customer> and return the first item (be sure to check for nulls!)
return toCustomerList(lst)[0];
}

}