In Part 1 of this short series, I discussed how to build a simple ASP.NET MVC application using Entity Spaces. If you have any Object Oriented background you will have noticed that that application was what is called, "tightly coupled". That means that one set of objects depends on another. This is generally a bad idea as it makes your application less flexible and extendable.
In this article, I will discuss what changes are necessary to make the application "loosely coupled", which in turn will make it easier to extend.
"Coding to an interface, rather than to an implementation, makes your software easier to extend."
"By encapsulating what varies, you make your application more flexible, and easier to change."
"The best way to get good requirements is to understand what a system is supposed to do."
"Great software is easy to change and extend, and does what the customer wants it to do."
"Analysis helps you ensure your system works in a real-world context."
"A loosely coupled application means that your objects are independent of each other, in other words, changes to one object don't require you to make a bunch of changes to other objects."
I just got finished saying that I read the Head First book on Object Oriented Analysis and Design, and one of the mantra's in it was the last quote above. And I violated it by creating the INorthwindRepository interface as such:
using System;
using System.Collections.Generic;
using System.Text;
namespace ESNorthwind.MVC.Data
{
public interface INorthwindRepository
{
CategoriesCollection GetCategories();
ProductsCollection GetProducts();
ProductsCollection GetProductsByCategoryName(string categoryName);
Products GetProductById(int id);
SuppliersCollection GetSuppliers();
void SubmitChanges(Products product);
}
}
The CategoriesCollection is an Entity Spaces specific collection! So right off the bat I've limited the extendability of this application to... er... Entity Spaces. Which isn't really a limitation to me, since Entity Spaces is all I would use, but for anyone else who would want to download this application and is not an Entity Spaces user, this would be very restricting and hard to adapt.
This instantly prevents the developer from easily plugging in their own data provider and having it work.
So to make it easier for non-Entity Spaces users, this is what I need to do:
using System;
using System.Collections.Generic;
using System.Text;
namespace ESNorthwind.MVC.Data
{
public interface INorthwindRepository
{
IList<Category> GetCategories();
IList<Product> GetProducts();
IList<Product> GetProductsByCategoryName(string categoryName);
Product GetProductById(int id);
IList<Supplier> GetSuppliers();
void SubmitChanges(Product product);
}
}
"Code to an interface, not an implementation".
By using IList<Category>, it now allows the application to be provider agnostic. It could care less who the data provider is. This did, though, require a bit more coding to make it work as it should.
For those who don't know Entity Spaces, built-into the Entity Spaces framework is the option to build all classes and maintain hierarchical relationships between database tables. In a normal application, not a loosely coupled application, this type of framework is the developers dream.
Let's say we are returning a Products entity and we need to get the CategoryName from the Categories entity. Using Entity Spaces we could do something like this:
Products product = new Products();
product.LoadByPrimaryKey(4);
string categoryName = product.UpToCategoriesByCategoryID.CategoryName;
Entity Spaces can easily create those relationships for the developer. But to have a loosely coupled application, where any layer above the Data layer is NOT dependent on the data provider, this is not a good thing.
So what do we do?
When generating the Entity Spaces classes, I can just not select the option to "Generate Hierarchical Model" so each entity is independent. So now there would be no more, UpToCategoriesByCategoryID, property to the class. This means of course, that we need to manually create or maintain that relationship in code.

This isn't necessarily a bad thing, it's just a slightly more time consuming task, but in the long run it makes the application more flexible.
HOW DOES THIS IMPACT THE ENTIRE APPLICATION?
It impacts it a little, but nothing drastic, especially for an application as small as this one. I was able to refactor the entire application in an afternoon. But this also means that you can remove the Entity Spaces references from the Services Project since there are no dependencies to Entity Spaces in this layer.

What I did have to add were classes that would directly act as the Model:
- Category
- Product
- Supplier
I built these classes manually to store the data from the repository that would then be passed to the Controller and then the View. Notice that they are a singular, where the database tables are plural.
using System;
using System.Collections.Generic;
using System.Text;
namespace ESNorthwind.MVC.Data
{
public class Category
{
public int CategoryID { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public string Picture { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace ESNorthwind.MVC.Data
{
public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public int SupplierID { get; set; }
public int CategoryID { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public int UnitsInStock { get; set; }
public Category Category { get; set; }
public Supplier Supplier { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace ESNorthwind.MVC.Data
{
public class Supplier
{
public int SupplierID { get; set; }
public string CompanyName { get; set; }
}
}
What else had to change? The NorthwindRepository class had to change a bit to store the data into the model classes instead of passing those Entity Spaces object up to the view. So I had to do some relationship mapping.
This is the GetProductById() in part one of the application:
/// <summary>
/// Get the product details by productid
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public Products GetProductById(int id)
{
Products product = new Products();
product.LoadByPrimaryKey(id);
return product;
}
And here's the newly refactored method:
/// <summary>
/// Get the product details by productid
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public Product GetProductById(int id)
{
// Get the Products data from the database using Entity Spaces.
Products prod = new Products();
prod.LoadByPrimaryKey(id);
// Instantiate the model Product object to store the
// data from the Entity Spaces objects.
Product product = new Product();
product.ProductID = (int)prod.ProductID;
product.ProductName = prod.ProductName;
product.SupplierID = (int)prod.SupplierID;
product.CategoryID = (int)prod.CategoryID;
product.QuantityPerUnit = prod.QuantityPerUnit;
product.UnitPrice = (decimal)prod.UnitPrice;
product.UnitsInStock = (int)prod.UnitsInStock;
// Load the Entity Spaces object to get the CompanyName.
Suppliers supp = new Suppliers();
supp.LoadByPrimaryKey(product.SupplierID);
// New up a model Supplier object to store the CompanyName.
product.Supplier = new Supplier() { CompanyName = supp.CompanyName };
// Load the Entity Spaces Categories object to get the CategoryName.
Categories cat = new Categories();
cat.LoadByPrimaryKey(product.CategoryID);
// New up the model Category object to store the CategoryName.
product.Category = new Category() { CategoryName = cat.CategoryName };
// Return the model Product object.
return product;
}
You'll see that there's a bit more code, but this has to be done in order to de-couple the model from the data provider.
Here's the SubmitChanges method in the NorthwindRepository class:
Entity Spaces specific method:
public void SubmitChanges(Products product)
{
product.Save();
}
And the loosely coupled method:
/// <summary>
/// Submit the changes to the database.
/// </summary>
/// <param name="product"></param>
public void SubmitChanges(Product product)
{
// Load the Entity Spaces Products object based
// on the ProductID and get the new values
// from the Product model passed in.
Products prod = new Products();
prod.LoadByPrimaryKey(product.ProductID);
prod.ProductName = product.ProductName;
prod.SupplierID = product.SupplierID;
prod.CategoryID = product.CategoryID;
prod.UnitPrice = product.UnitPrice;
prod.Save();
}
Again, as I stated in the first article, I'm learning this as I go and as elated as I was to have the application work where I could pass along the Entity Spaces objects to the Views, when I thought about hooking up SubSonic as another test, I quickly realize that I couldn't with the application in it's present state. Hence the need to refactor to make the application extendable and more flexible.
WHAT ABOUT THE SERVICES LAYER?
That's what's so nice about this pattern, the Service layer was the quickest fix of them all. I just needed to change the return object types from the Entity Spaces specific objects or collections to model references, ex: IList<model>.
This is the GetProductsByCategoryName() method originally:
/// <summary>
/// Return the ProductsCollection based on the Category name.
/// </summary>
/// <param name="categoryName"></param>
/// <returns></returns>
public ProductsCollection GetProductsByCategoryName(string categoryName)
{
ProductsCollection prodColl = _repository.GetProductsByCategoryName(categoryName);
return prodColl;
}
And this is the new method:
/// <summary>
/// Return the ProductsCollection based on the Category name.
/// </summary>
/// <param name="categoryName"></param>
/// <returns></returns>
public IList<Product> GetProductsByCategoryName(string categoryName)
{
IList<Product> prodColl = _repository.GetProductsByCategoryName(categoryName);
return prodColl;
}
The only thing changed was the return types, ProductsCollection to IList<Product>. Now other than maybe adding a few new methods in this class or applying any business logic, this class doesn't need to be modified if we change data providers.
SO WHAT HAPPENS TO THE WEB LAYER?
Let's see how this refactoring impacted the Web site layer.
First understand that the way Entity Spaces works, a reference to Entity Spaces needs to occur in the web site, since the Loader needs to be set in the Global.asax file.
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace ESNorthwind.MVC
{
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
//routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//routes.MapRoute(
// "Default", // Route name
// "{controller}/{action}/{id}", // URL with parameters
// new { controller = "Home", action = "Index", id = "" } // Parameter defaults
//);
routes.MapRoute("mvcroute", "{controller}/{action}/{id}"
, new { controller = "products", action = "Index", id = "" }
, new { controller = @"[^\.]*" });
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
EntitySpaces.Interfaces.esProviderFactory.Factory = new EntitySpaces.LoaderMT.esDataProviderFactory();
}
}
}
So essentially, nothing has changed in this file.
WHAT ABOUT THE CONTROLLER CLASS?
The controller came out pretty much unscathed, only a couple things needed to be changed. Edit and Update were the two methods that changed.
Here's the original methods:
public object Edit(int id)
{
ProductsEditViewData viewData = new ProductsEditViewData();
Products product = service.GetProductById(id);
viewData.Product = product;
if (TempData.ContainsKey("ErrorMessage"))
{
foreach (var item in TempData)
{
ViewData[item.Key] = item.Value;
}
}
ViewData["CategoryID"] = new SelectList(service.GetCategories(), "CategoryID", "CategoryName", ViewData["CategoryID"] ?? product.CategoryID);
ViewData["SupplierID"] = new SelectList(service.GetSuppliers(), "SupplierID", "CompanyName", ViewData["SupplierID"] ?? product.SupplierID);
return View("Edit", viewData);
}
public object Update(int id)
{
Products product = service.GetProductById(id);
if (!IsValid())
{
Request.Form.CopyTo(TempData);
TempData["ErrorMessage"] = "An error occurred";
return RedirectToAction("Edit", new { id = id });
}
BindingHelperExtensions.UpdateFrom(product, Request.Form);
service.SubmitChanges(product);
return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.UpToCategoriesByCategoryID.CategoryName }));
}
And here are the methods from the newly refactored controller class.
public object Edit(int id)
{
//ProductsEditViewData viewData = new ProductsEditViewData();
Product product = service.GetProductById(id);
//viewData.Product = product;
if (TempData.ContainsKey("ErrorMessage"))
{
foreach (var item in TempData)
{
ViewData[item.Key] = item.Value;
}
}
ViewData["CategoryID"] = new SelectList(service.GetCategories(), "CategoryID", "CategoryName", ViewData["CategoryID"] ?? product.CategoryID);
ViewData["SupplierID"] = new SelectList(service.GetSuppliers(), "SupplierID", "CompanyName", ViewData["SupplierID"] ?? product.SupplierID);
//return View("Edit", viewData);
return View("Edit", product);
}
public object Update(int id)
{
Product product = service.GetProductById(id);
if (!IsValid())
{
Request.Form.CopyTo(TempData);
TempData["ErrorMessage"] = "An error occurred";
return RedirectToAction("Edit", new { id = id });
}
BindingHelperExtensions.UpdateFrom(product, Request.Form);
service.SubmitChanges(product);
return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.Category.CategoryName }));
}
Notice that in the Edit method, I moved away from the ProductsEditViewData class to store the data and simply loaded the Product model class from the service.GetProductById(id) method. Then I passed the product model to the View directly.
In the Update method, I had to change the RedirectToRoute line where the anonymous object was called using an Entity Spaces specific hierarchical jump to the CategoryName:
return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.UpToCategoriesByCategoryID.CategoryName
I simply added a property to the Product model object that returns a Category model object and then I'm good to go.
return RedirectToRoute(new RouteValueDictionary(new { Action = "List", ID = product.Category.CategoryName }));
If you remember, the Product model class contains a property called Category, which returns a Category object.
using System;
using System.Collections.Generic;
using System.Text;
namespace ESNorthwind.MVC.Data
{
public class Product
{
public int ProductID { get; set; }
public string ProductName { get; set; }
public int SupplierID { get; set; }
public int CategoryID { get; set; }
public string QuantityPerUnit { get; set; }
public decimal UnitPrice { get; set; }
public int UnitsInStock { get; set; }
public Category Category { get; set; }
public Supplier Supplier { get; set; }
}
}
This Category property is then populated way down in the NorthwindRepository class and the method, GetProductById(id).
/// <summary>
/// Get the product details by productid
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public Product GetProductById(int id)
{
// Get the Products data from the database using Entity Spaces.
Products prod = new Products();
prod.LoadByPrimaryKey(id);
// Instantiate the model Product object to store the
// data from the Entity Spaces objects.
Product product = new Product();
product.ProductID = (int)prod.ProductID;
product.ProductName = prod.ProductName;
product.SupplierID = (int)prod.SupplierID;
product.CategoryID = (int)prod.CategoryID;
product.QuantityPerUnit = prod.QuantityPerUnit;
product.UnitPrice = (decimal)prod.UnitPrice;
product.UnitsInStock = (int)prod.UnitsInStock;
// Load the Entity Spaces object to get the CompanyName.
Suppliers supp = new Suppliers();
supp.LoadByPrimaryKey(product.SupplierID);
// New up a model Supplier object to store the CompanyName.
product.Supplier = new Supplier() { CompanyName = supp.CompanyName };
// Load the Entity Spaces Categories object to get the CategoryName.
Categories cat = new Categories();
cat.LoadByPrimaryKey(product.CategoryID);
// New up the model Category object to store the CategoryName.
product.Category = new Category() { CategoryName = cat.CategoryName }; // Category property populated here.
// Return the model Product object.
return product;
}
Ok, so that's what I did for this refactoring of the application. I'm going to try a couple more things to this application, again as an exercise for myself.
One is to try and implement Extension Methods (MSDN article msdn.microsoft.com/en-us/library/bb383977.aspx and an article by Scott Guthrie weblogs.asp.net/scottgu/archive/2007/03/13/new-orcas-language-feature-extension-methods.aspx) which can help make the application even more flexible and try to hook up SubSonic and see if this application above the Data layer holds up without making any changes to it. That is the real test of extendibility and flexibility.
You can download the new version here. ESNorthwind.MVC_Pt2.zip (2.37 mb)
As always, if you have any questions or comments, good or bad, primarily good, then feel free to post a comment here or in my Forums.
Thanks.