MVC Starter Kits

MVC Starter Kits for ASP.NET

About the author

King Wilder, I'm an ASP.NET developer and I run and own a small web hosting company called Gizmo Beach.
E-mail me Send mail

Pages

Recent comments

Authors

Categories


Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2008

ASP.NET MVC Northwind Demo using Entity Spaces - Part Deux

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.

 

 

 

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Admin on Wednesday, June 04, 2008 3:49 AM
Permalink | Comments (10) | Post RSSRSS comment feed

Related posts

Comments

GlenH us

Sunday, June 15, 2008 4:04 AM

GlenH

These to articles were great! I'd love to see you make it work with Subsonic, or NHibernate to compare and contrast. Thanks!

King Wilder

Monday, June 16, 2008 4:06 AM

King Wilder

Glen,

I plan on it soon, I just have to get some small paying gigs out of the way. And I would probably just create a SubSonic version, since I've never used NHibernate, I've just seen it in action.

I'll try to get the SubSonic version up by the week of June 22, 2008.

Thanks,

King Wilder

Michael Freidgeim au

Tuesday, June 17, 2008 2:24 AM

Michael Freidgeim

I feel that your Part 2 changes to make the application "loosely coupled" defeat the essential benefit of ORM(and probably ASP.NET MVC implementation)- minimize custom code.

I agree that in good design "changes to one object don't require you to make a bunch of changes to other objects."
But how often in a real application you change the architecture of data layer(e.g. from EntitySpaces to Subsonic, or NHibernate or LinqToSql)? Not often, unless you found that ORM, you are using,is not good,

More often change in a real application is to add a new column to the table(e.g. to Product) . In your Part 1 example your re-generate classes in /Generated folders for the Entity Spaces classes, and you've done.



In part 2 with extra Business layer classes you additionally will need MANUALLY add new property to Product class,GetProductById and SubmitChanges methods- extra boring task.



To prove that the extra Business layer classes architecture is good for rapid development and maintainability, it will be good to make these new classes and methods also generated(directly from database tables or from previously generated data layer classes using reflection). It should not be too hard using MyGeneration or CodeSmith- I am sure that there are some similar templates on their forums.

King Wilder

Tuesday, June 17, 2008 4:00 AM

King Wilder

Michael,

You are correct, partly. Even if you keep the "tightly coupled" ORM relationships with the View, you would still need to modify it slightly, based on your example of adding a new column to a table. You would probably need to add that field to the View since it's added to the table, so a minor modification would need to be done, whether or not it's loosely coupled.

I just wanted to give other developers a clearer look at an ASP.NET MVC application that wasn't using Linq or Linq to Sql. And in Part Deux, I wanted to apply some Design Patterns which is good practice.

The extra Business Layer you mention, I assume you're talking about the Service layer, is simply a Facade business layer, that the View directly works with. It should never, in good design, talk to a layer that directly talks to the database. This way, I'm sure you already know, you can easily change your database from, say SQL Server, to Oracle, or MySql with no impact to the View or even the Facade layer.

In regards to using MyGeneration or Codesmith to generate Business Layer classes, that's an excellent point! I've been doing that (using MyGeneration) for a short time now and it drastically reduces time it takes to build my Business Layer.

These posts are just to give developers who don't know about patterns or best practices, a little insight on how you can make your application flexible and easy to extend, which is something every developer needs to think about before diving into code.

You are always free to develop the applications any way you deem easiest for your needs.

Thanks for your comments.

King Wilder

Michael Freidgeim au

Wednesday, June 18, 2008 9:26 AM

Michael Freidgeim

1. By extra Business Layer I meant manually created Model classes Category,Product,Supplier (by the way, I prefer to add prefix to the class ,e.g. ModelProduct, rather than use different namespaces, as you done in part 3)

2. Ability to change your database from, say SQL Server, to Oracle, is included in good ORM tool(e.g EntitySpaces, but not Linq to SQL),so it should not be a reason to create extra ORM-agnostic layer.

3. I understand your wish to demonstrate the use of the Design Patterns, but, again, I am not convinced that part 2 architecture is better than part 1. Blindly follow the patterns lead to over-engineering instead of simple, but sufficient, architecture.

King TEst

Thursday, June 19, 2008 8:35 AM

King TEst

Michael,

My two versions of the application wasn't to promote any particular pattern, it was just to give some insight to the constraints of one and the practices of the other. The way each developer plans to build their own application is completely up to them.

Re 1) Adding the prefix of Model to the classes is a good idea.

Re 2) You are quite right. If you know you will never change your ORM tool, then pointing to a different data source is a non-issue. But for those Linq to Sql users, they would have a problem.

Re 3) I am not convinced that part 2 is bettern than part 1 either. I'm just presently a pattern-friendly way to loosely couple the application layers. Again, if you know you would never be using anything but the ORM tool you created the application with, then there probably would be no reason you can use the method in part 1.

I'm just trying to give the community options. I get so frustrated when I see applications built that are quick and simple, but it is also stated that you shouldn't built a production application similar to the simple example.

I wanted to convert the simple application into something that would reflect a more production oriented approach to application building.

Thanks for you comments. It's always good to have a dialogue.

Mike Griffin us

Friday, June 20, 2008 2:45 AM

Mike Griffin

Excellent article King, you really covered the topic well, very easy to follow with good code samples. I know you're focusing on MVC and thus the interfaces, so this is not to take away from that. However, Michael makes a good point, most likely a person would need to swap out to another database rather than a different architecture. And of course, with EntitySpaces you can run the same binary executable against many databases which is probably a more likely scenario. I'm not a big fan of facades and layer up on layer as it adds up to a lot of maintenance (which you can tell if you use EntitySpaces, we don't even have the concept of two layers, the data layer no longer exists, or has been combined if you will). Anyway, excellent part two. For anybody trying to learn ASP.NET MVC these articles will be a very good starting point.

King Wilder

Friday, June 20, 2008 3:10 AM

King Wilder

Mike,

I'm glad you liked it. My goal with this part wasn't to make it so Entity Spaces specific as Pattern specific (since I just finished reading the Head First Design Patterns book, I might have gotten a little over zealous).

I see a lot of good in patterns and I wanted to see if I could incorporate them in this simple application, primarily for my own sake, just to see if I could. But I completely agree with you and Michael in that if you know you are going to use Entity Spaces, or any other ORM tool as your data provider, then much of this is moot. But I still wanted to apply some patterns to the architecture.

I don't see myself using anything for the data provider other than Entity Spaces, so I would probably build my application more like Part 1, but again, this was all an exercise for me too and I wanted to prove to myself that patterns work and that I know how to use them, if need be.

That's one of the things I love about Entity Spaces, is the ease of changing data stores. I had to build an ASP.NET application for a client that used MySql, and found that is was simple using Entity Spaces, and I didn't have to change anything in the way I wrote my code.

I don't know if I'm ready to use ASP.NET MVC in a real-world application just yet, because there aren't enough examples on how to do things manually, that was a simple drag-and-drop control with WebForms. But I'm trying to stay on top of it, so when it is released, I have a better understanding than if I was starting from scratch.

todd us

Sunday, July 20, 2008 3:42 PM

todd

Hey King,

Great article. I am just looking at Entity Spaces now and like what i see so far. There are alot of questions in my mind on how to do things but im sure ill find examples etc.

Based on your experience with ES so far is there anything you come across the it hasnt been able to do? Im looking to use it in some projects for a few clients but still doing my homework at this moment.

Admin us

Monday, July 21, 2008 3:10 AM

Admin

Todd,
I've been using Entity Spaces for almost two years and I can't think of anything that you can't do with it. And I've built over 8 applications, large and small with it.

I would be a basket case if I had to back to writing all the data access code using ADO.NET. Entity Spaces has literally removed this part of the application from the equation for me.

There's lots of code examples in the Support section of the Entity Spaces site and any other questions you have can be answered in the Forums.

Have fun!

Add comment


(Will show your Gravatar icon)  

  Country flag

[b][/b] - [i][/i] - [u][/u]- [quote][/quote]



Live preview

Thursday, August 28, 2008 1:47 PM