Using Entity Framework to build the model in an ASP.NET MVC 2 application

Introduction

For a database driven web application we can use the MVC approach to develop an API based interface to the database, including encapsulation of business logic and validation.Keeping access to the data as simple as possible is a desirable goal, and this can be achieved by providing a procedural interface to the database.

Scenario

For this dev.note we will use an example of a very simple bookshop system based on the database schema presented in the .NET LINQ tutorial. The schema is shown is the diagram below:

 

Database Schema for Bookshop

Establishing requirements

We will assume that our application only requires a simple set of actions against the database. These are:

  • Add a new book
  • Add a new publisher
  • List all books
  • List books for a particular publisher
  • Get a book by its ISBN
  • Get a publisher by its ID.
  • Delete a book
  • Delete a publisher
  • List books by a particular author
  • Commit changes to database

Stages

The question is - how do we get from our database to the functional interface we require for the rest of our application?

We can break this down into several stages:

  • Use the O/RM features of the Entity Framework to generate a set of classes for accessing the data
  • Build an interface class to define the actions we need from the model
  • Implement the interface to provide a repository of methods for working with the data.
  • Extend the classes to provide validation for the basic objects.

Creating the Entity Data Model Classes

This is simply a metter of adding a new item into our models folder of type ADO.NET Entity Data Model. We can then drag the table onto the design surface and save the file. This process is dealt with in the .NET LINQ tutorial. If we save our file as Bookshop.dbml we will end up with three classes in our project: Book, Publisher and a bookshopEntities class. See the LINQ tutorial for more details on this process. The basic class model for our database looks like this:

Entity model for bookshop

Building the interface to the model

Now we have our O/RM classes we can make use of them to define the software interface to the model. Using the functional list above we can create our interface class as follows:

using System;
using System.Collections.Generic;

namespace BookshopMVC.Models
{
  interface IBookshopRepository
  {
    void AddBook(Book book);
    void AddPublisher(Publisher publisher);
    IEnumerable<Book> ListAllBooks();
    IEnumerable<Book> ListBooksForPublisher(int id);
    Book GetBook(string isbn);
    Publisher GetPublisher(int id);
    void DeleteBook(Book book);
    void DeletePublisher(Publisher publisher);
    IEnumerable<Book> ListBooksForAuthor(string author);
    void Save();
  }
}

Note how the functions in our interface map directly onto the requirements of our application.

Implementing our class from the interface

We can now create a class based on the interface and think about the details of the implementation. The bookshopEntities class provides a raw interface to our database, so we simply need to add appropriate code to the functions in our interface to perform the functions. The code, minus validation, looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace BookshopMVC.Models
{
  public class BookshopRepository : IBookshopRepository
  {
    bookshopEntities db = new bookshopEntities();
    public void AddBook(Book book)
    {
      db.Books.AddObject(book);
    }
    public void AddPublisher(Publisher publisher)
    {
      db.Publishers.AddObject(publisher);
    }
    public IEnumerable<Book> ListAllBooks()
    {
      return db.Books;
    }
    public IEnumerable<Book> ListBooksForPublisher(int id)
    {
      return db.Books.Where(b => b.PublisherID == id);
    }
    public Book GetBook(string isbn)
    {
      return db.Books.Single(b => b.ISBN == isbn);
    }
    public Publisher GetPublisher(int id)
    {
      return db.Publishers.Single(p => p.PublisherID == id);
    }
    public void DeleteBook(Book book)
    {
      db.Books.DeleteObject(book);
    }
    public void DeletePublisher(Publisher publisher)
    {
      db.Publishers.DeleteObject(publisher);
    }
    public IEnumerable<Book> ListBooksForAuthor(string author)
    {
      return db.Books.Where(b => b.Author == author);
    }
    public void Save()
    {
      db.SaveChanges();
    }

  }
}

In our simple example there isn't much actual code required to implement our interface, however, in more complex applications we could end up with much more code, especially if we need to perform some sort of business logic in the model.

Extending our Entity Data Model classes to add validation

One feature of the Entity Data Model mapping is the way the code for our classes is constructed. Each class is generated as a partial class. This means we are free to extend any of the classes within the model without impacting on the initial Entity Data Model mapping. This is important because the basic classes are automatically generated form our selected tables and procedures, so if we change the database in any way, e.g. by adding a new table, the classes will be re-created, overwriting any manual coding we may have done.

Looking at our model classes (in the diagram above) note how the Book class has a property Publisher which is a reference to the full pulisher record, and the Publisher class has a property Books which is a reference to the list of books published by the corresponding publisher. This makes accessing data extremely straightforward.

The Book, Publisher and bookshopEntities classes are all defined as partial classes, for example:

public partial class bookshopEntities : ObjectContext {
...
}
public partial class Book : EntityObject {
...
}
public partial class Publisher : EntityObject {
...
}

This means, for example, we are free to create a new class file for the Book class and extend it by adding new properties and methods which will be compiled into the class when we build the application. This new class file remains separate from the automatically generated class, so it will persist across multiple changes to the underlying database model. For example:

namespace BookshopMVC
{
  public partial class Book
  {
...
  }
}

The Entity Data Model classes do a good job of enforcing data consistency between the classes and the underlying database, though this is seldom sufficient for validation in an application. There may be more complex requirements. With MVC2 you can provide Data Annotations to your classes which allow you to specify more complex validations. For example:

using System.ComponentModel.DataAnnotations;
...
public class Book {
  [Required(ErrorMessage = "Title is required"]
  public string Title { get; set; }
}

However, we cannot add these directly to our Entity Data Model, as they would be overwritten next time we generate the model from the database. This is where the partial class concept comes in. We can create a partial class and attach a 'buddy' class to it which specifies all the validation logic.

using System.ComponentModel.DataAnnotations;
...
[MetadataType(typeof(Book_Validation))]
public partial class Book {
}

public class Book_Validation
{
  [Required(ErrorMessage = "ISBN is required")]
  [StringLength(15, ErrorMessage =
         "ISBN may not be longer that 15 characters"]
  public string ISBN { get; set; }
  [Required(ErrorMessage = "Title is required")]
  [StringLength(150, ErrorMessage =
         "Title may not be longer that 150 characters")]
  public string Title { get; set; }
  [Required(ErrorMessage = "Author is required")]
  [StringLength(150, ErrorMessage =
         "Author may not be longer that 150 characters")]
  public string Author { get; set; }
  [Required(ErrorMessage = "Publisher is required")]
  public string Publisher { get; set; }
  [Required(ErrorMessage = "Published date is required")]
  public string PubDate { get; set; }
  [Required(ErrorMessage = "Price is required")]
  [Range(0.01, 250.0, ErrorMessage =
         "Price must be in range £0.01 and £250")]
  public string Price { get; set; }
  [Required(ErrorMessage = "Stock level is required")]
  [Range(0, 1000, ErrorMessage =
         "Stock level cannot be negative or greater than 1000")]
  public string Price { get; set; } 
}

The NerdDinner model from Guthrie et al makes use of an enhanced validation design pattern to allow a more structured approach to validation, so that information can be passed via the controller to the user interface. It also makes use of a feature in Entity Data Model classes which provides access to validation errors within the controller.

Valid XHTML 1.0! | Valid CSS! | WCAG Approved AA
Page design by: John P Scott - Hosting with: Netcetera