Using Entity Framework to build the model in an ASP.NET MVC 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
Entity Framework tutorial. The schema is shown is the diagram below:
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 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 Framework Classes
This is simply a metter of adding a new item into our models folder of type
ADO.NET Entity Data Model. We can use the wizard to automatically create model
based on our database. The resulting model (.edmx) file represents the tables as
classes with cross-refernced properties matching the releationship in the
database. The basic entity model for our database looks like this:
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;
using entitydemo;
interface IBookshopRepository
{
void AddBook(Book book);
void AddPublisher(Publisher publisher);
IEnumerable<entitydemo.Book> ListAllBooks();
IEnumerable<entitydemo.Book> ListBooksForPublisher(int id);
Book GetBook(string isbn);
Publisher GetPublisher(int id);
void DeleteBook(Book book);
void DeletePublisher(Publisher publisher);
IEnumerable<entitydemo.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 BookshopDataContext 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.Collections.Generic;
using System.Linq;
using entitydemo;
public class BookshopRepository : IBookshopRepository
{
BookshopDataContext db = new BookshopDataContext();
public void AddBook(Book book)
{
db.Books.InsertOnSubmit(book);
Save();
}
public void AddPublisher(Publisher publisher)
{
db.Publishers.InsertOnSubmit(publisher);
Save();
}
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.DeleteOnSubmit(book);
Save();
}
public void DeletePublisher(Publisher publisher)
{
db.Publishers.DeleteOnSubmit(publisher);
Save();
}
public IEnumerable<Book> ListBooksForAuthor(string author)
{
return db.Books.Where(b => b.Author == author);
}
public void Save()
{
db.SubmitChanges();
}
}
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 LINQ to SQL classes to add validation
One feature of the LINQ to SQL O/RM 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 LINQ to SQL 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.
If we look at the class model you will see how the LINQ to SQL mapping has built the classes:
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 BookshopDataContext classes are all defined as partial classes, for example:
public partial class BookshopDataContext : System.Data.Linq.DataContext {
...
}
public partial class Book : INotifyPropertyChanging, INotifyPropertyChanged {
...
}
public partial class Publisher : INotifyPropertyChanging, INotifyPropertyChanged {
...
}
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 linq2sqldemo
{
public partial class Book
{
public bool isValid()
{
return true; // replace with code to validate the record
}
}
}
We now have an enhanced Book class and we can take advantage of this within the code for our software interface. For example, we can put a validation check in the AddBook method as follows:
public void AddBook(Book book)
{
if (book.isValid())
{
db.Books.InsertOnSubmit(book);
Save();
}
}
We aren't just limited to validation, and we can make use of any design patterns we want to enhance our basic LINQ to SQL model. 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 LINQ to SQL which provides an OnValidate partial method which is called anytime the LINQ to SQL classes are about to update the database. We can provide an OnValidate method as follows:
partial void OnValidate(ChangeAction action) {
if (!isValid())
throw new ApplicationException("Data record is not valid");
}