Building the Data Model - A Practical Example - ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

ASP.NET MVC 5 with Bootstrap and Knockout.js (2015)

Part IV. A Practical Example

Chapter 14. Building the Data Model

Once again, Entity Framework will be used as the ORM of choice for fetching and saving data for the shopping cart. This chapter will create the Code-First data models, instantiate some sample data, and create the necessary ViewModels that will be used by the MVC application.

Code-First Models

I am envisioning five models that will be required for the shopping cart:

Author model

This model will contain information about the book’s author.

Book model

This model will contain information about the book being sold, including things like the price, an author foreign key, and a category foreign key.

Category model

This model will contain an ID and name of the category. Each book will belong to one category (for simplicity).

Cart model

This model will contain a unique identifier of the user who owns the shopping cart. Each visitor to the site will be associated to a Cart model.

CartItem model

This model will contain which book and how many are being purchased. This object is a child to the Cart model.

The following examples contain these model definitions, as well as their inter-relationships with each other. Inside the Models folder, one file per model can be created. I’ve named each file the same as the class name.

Example 14-1 contains the Author model, which introduces a new feature of Entity Framework with the attribute NotMapped. I have created a FullName variable, which concantenates the first and last name into a single variable. By tagging the property with the attribute, EF knows that this property should not be persisted to the database. The FullName property will be used by the ViewModel that will be created in a later section.

Example 14-1. Author model

using System.Collections.Generic;

using System.ComponentModel.DataAnnotations.Schema;

namespace ShoppingCart.Models

{

public class Author

{

public int Id { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public string Biography { get; set; }

[NotMapped]

public string FullName

{

get

{

return FirstName + ' ' + LastName;

}

}

public virtual ICollection<Book> Books { get; set; }

}

}

Example 14-2 contains the Book model. This model has been expanded from the earlier version used to include pricing information, as well as a Boolean to indicate whether the book will be featured on the home page.

Example 14-2. Book model

namespace ShoppingCart.Models

{

public class Book

{

public int Id { get; set; }

public int AuthorId { get; set; }

public int CategoryId { get; set; }

public string Title { get; set; }

public string Isbn { get; set; }

public string Synopsis { get; set; }

public string Description { get; set; }

public string ImageUrl { get; set; }

public decimal ListPrice { get; set; }

public decimal SalePrice { get; set; }

public bool Featured { get; set; }

public virtual Author Author { get; set; }

public virtual Category Category { get; set; }

}

}

Example 14-3 contains the Category model, which contains an ID, name, and a collection of books that are associated with this category.

Example 14-3. Category model

using System.Collections.Generic;

namespace ShoppingCart.Models

{

public class Category

{

public int Id { get; set; }

public string Name { get; set; }

public virtual ICollection<Book> Books { get; set; }

}

}

The Cart model (shown in Example 14-4) is much like the Category model in that it contains only an Id, SessionId, and a collection of the associated cart items. The SessionId is the unique identifier that will be used to identify who owns the cart.

Example 14-4. Cart model

using System.Collections.Generic;

using System.ComponentModel.DataAnnotations;

using System.ComponentModel.DataAnnotations.Schema;

namespace ShoppingCart.Models

{

public class Cart

{

public int Id { get; set; }

[Index(IsUnique=true)]

[StringLength(255)]

public string SessionId { get; set; }

public virtual ICollection<CartItem> CartItems { get; set; }

}

}

The SessionId is also decorated with two EF attributes. The first attribute identifies that this property should be created as a unique index. Because this field will be searched on for each page load to find the user’s cart, this is a good performance improvement. The second attribute defines a maximum string length. By default, if no string length is identified, EF will create string fields as nvarchar(max), and an index is not compatible with this type of field.

And finally, Example 14-5 contains the CartItem model definition. Apart from tracking the quantity of books being purchased, this table contains nothing but relationships to the cart and the book being purchased.

Example 14-5. CartItem model

namespace ShoppingCart.Models

{

public class CartItem

{

public int Id { get; set; }

public int CartId { get; set; }

public int BookId { get; set; }

public int Quantity { get; set; }

public virtual Cart Cart { get; set; }

public virtual Book Book { get; set; }

}

}

Defining the DbContext and Initializing Data

Before the data models will be used, several additional Entity Framework setup steps are required. Just like I did in Chapter 4, I have created a new DAL (Data Access Layer) folder and created my ShoppingCartContext file that defines my five datasets (as shown in Example 14-6).

Example 14-6. ShoppingCartContext

using ShoppingCart.Models;

using System.Data.Entity;

using System.Data.Entity.ModelConfiguration.Conventions;

namespace ShoppingCart.DAL

{

public class ShoppingCartContext : DbContext

{

public DbSet<Category> Categories { get; set; }

public DbSet<Book> Books { get; set; }

public DbSet<Author> Authors { get; set; }

public DbSet<Cart> Carts { get; set; }

public DbSet<CartItem> CartItems { get; set; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)

{

modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

base.OnModelCreating(modelBuilder);

}

}

}

To ensure that I have some data to test out my shopping cart with, I have a created a DataInitialization class (also inside the DAL folder) that will create some books, authors, and categories as shown in Example 14-7.

Example 14-7. DataInitialization

using ShoppingCart.Models;

using System.Collections.Generic;

using System.Data.Entity;

namespace ShoppingCart.DAL

{

public class DataInitialization :

DropCreateDatabaseIfModelChanges<ShoppingCartContext>

{

protected override void Seed(ShoppingCartContext context)

{

var categories = new List<Category>

{

new Category {

Name = "Technology"

},

new Category {

Name = "Science Fiction"

},

new Category {

Name = "Non Fiction"

},

new Category {

Name = "Graphic Novels"

}

};

categories.ForEach(c => context.Categories.Add(c));

var author = new Author

{

Biography = "...",

FirstName = "Jamie",

LastName = "Munro"

};

var books = new List<Book>

{

new Book {

Author = author,

Category = categories[0],

Description = "...",

Featured = true,

ImageUrl =

"http://ecx.images-amazon.com/images/I/51T%2BWt430bL._AA160_.jpg",

Isbn = "1491914319",

ListPrice = 19.99m,

SalePrice = 17.99m,

Synopsis = "...",

Title = "Knockout.js: Building Dynamic Client-Side Web Applications"

},

new Book {

Author = author,

Category = categories[0],

Description = "...",

Featured = true,

ImageUrl = "http://ecx.images-amazon.com/images/I/51AkFkNeUxL._AA160_.jpg",

Isbn = "1449319548",

ListPrice = 14.99m,

SalePrice = 13.99m,

Synopsis = "...",

Title = "20 Recipes for Programming PhoneGap"

},

new Book {

Author = author,

Category = categories[0],

Description = "...",

Featured = false,

ImageUrl = "http://ecx.images-amazon.com/images/I/51LpqnDq8-L._AA160_.jpg",

Isbn = "1449309860",

ListPrice = 19.99m,

SalePrice = 16.99m,

Synopsis = "...",

Title = "20 Recipes for Programming MVC 3: Faster, Smarter Web Development"

},

new Book {

Author = author,

Category = categories[0],

Description = "...",

Featured = false,

ImageUrl = "http://ecx.images-amazon.com/images/I/41JC54HEroL._AA160_.jpg",

Isbn = "1460954394",

ListPrice = 14.99m,

SalePrice = 13.49m,

Synopsis = "...",

Title = "Rapid Application Development With CakePHP"

}

};

books.ForEach(b => context.Books.Add(b));

context.SaveChanges();

}

}

}

I’ve created four categories, one author, and four books. The four books are all related to the first category created, as well as the one author created. All of these objects are added to the corresponding EF dataset prior to calling SaveChanges to save the nine objects in the database.

To ensure that the database is initialized upon first start, the Global.asax.cs file inside the root of the project requires updating, as shown in Example 14-8, to initialize the database.

Example 14-8. Global.asax.cs

using ShoppingCart.DAL;

using System;

using System.Collections.Generic;

using System.Data.Entity;

using System.Linq;

using System.Web;

using System.Web.Http;

using System.Web.Mvc;

using System.Web.Optimization;

using System.Web.Routing;

namespace ShoppingCart

{

public class MvcApplication : System.Web.HttpApplication

{

protected void Application_Start()

{

AreaRegistration.RegisterAllAreas();

GlobalConfiguration.Configure(WebApiConfig.Register);

FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

RouteConfig.RegisterRoutes(RouteTable.Routes);

BundleConfig.RegisterBundles(BundleTable.Bundles);

var dbContext = new ShoppingCartContext();

Database.SetInitializer(new DataInitialization());

dbContext.Database.Initialize(true);

}

protected void Session_Start(object sender, EventArgs e)

{

HttpContext.Current.Session.Add("__MyAppSession", string.Empty);

}

}

}

Example 14-8 also includes a new Session_Start function. Because the SessionId string in the Cart model will contain that user’s HTTP Session ID, ASP.NET requires that the session be initialized with something. Typically, this would be accomplished if some user information were saved and retrieved on each request from the session; however, no data needs to be stored in the session, so instead, I just initialize with an empty string.

This appears to be a minor flaw in ASP.NET, because if this isn’t done, the SessionId appears to be reset at random points.

The ViewModels

The ViewModels are almost identical to the models with a few minor adjustments to some of them. Just like the models, there are five ViewModels that serve much the same as their counterparts. The following examples contain the five different ViewModels. I have created a new folder called ViewModels and have named the files the same as their class names. In each case, it is the name of the model postfixed by ViewModel. This helps separate them when the controller needs to work with both the data models and the ViewModels.

Example 14-9 contains the AuthorViewModel. You will notice that there are no fields for the first and last name, just the concantenated full name. This is a good example where the ViewModel only contains the full name because that is how it will always be used by the views created later.

Example 14-9. AuthorViewModel

using Newtonsoft.Json;

namespace ShoppingCart.ViewModels

{

public class AuthorViewModel

{

[JsonProperty(PropertyName="id")]

public int Id { get; set; }

[JsonProperty(PropertyName = "fullName")]

public string FullName { get; set; }

[JsonProperty(PropertyName = "biography")]

public string Biography { get; set; }

}

}

The BookViewModel is shown in Example 14-10. For the most part, the fields match identically to the Book model with the exception of a calculated field called SavePercentage. This field performs a math calculation that will determine the difference (in percentage) between the sale and list price of the book. This will allow a view to list a savings percentage to the user.

Example 14-10. BookViewModel

using Newtonsoft.Json;

namespace ShoppingCart.ViewModels

{

public class BookViewModel

{

[JsonProperty(PropertyName = "id")]

public int Id { get; set; }

[JsonProperty(PropertyName = "title")]

public string Title { get; set; }

[JsonProperty(PropertyName = "isbn")]

public string Isbn { get; set; }

[JsonProperty(PropertyName = "synopsis")]

public string Synopsis { get; set; }

[JsonProperty(PropertyName = "description")]

public string Description { get; set; }

[JsonProperty(PropertyName = "imageUrl")]

public string ImageUrl { get; set; }

[JsonProperty(PropertyName = "listPrice")]

public decimal ListPrice { get; set; }

[JsonProperty(PropertyName = "salePrice")]

public decimal SalePrice { get; set; }

[JsonProperty(PropertyName = "featured")]

public bool Featured { get; set; }

[JsonProperty(PropertyName = "savePercentage")]

public int SavePercentage

{

get

{

return (int)(100 - (SalePrice / ListPrice * 100));

}

}

[JsonProperty(PropertyName = "author")]

public virtual AuthorViewModel Author { get; set; }

[JsonProperty(PropertyName = "category")]

public virtual CategoryViewModel Category { get; set; }

}

}

Example 14-11 contains the CategoryViewModel that has been stripped down from its Category model to not include the list of books because the view that uses this ViewModel does not need to display the books.

Example 14-11. CategoryViewModel

using Newtonsoft.Json;

namespace ShoppingCart.ViewModels

{

public class CategoryViewModel

{

[JsonProperty(PropertyName = "id")]

public int Id { get; set; }

[JsonProperty(PropertyName = "name")]

public string Name { get; set; }

}

}

The CartViewModel (shown in Example 14-12) does not include the SessionId because this is the server’s unique identifier that does not need to be exposed publicly.

Example 14-12. CartViewModel

using Newtonsoft.Json;

using System.Collections.Generic;

namespace ShoppingCart.ViewModels

{

public class CartViewModel

{

[JsonProperty(PropertyName = "id")]

public int Id { get; set; }

[JsonProperty(PropertyName = "cartItems")]

public virtual ICollection<CartItemViewModel> CartItems { get; set; }

}

}

And finally, Example 14-13 contains the CartItemViewModel, which is almost identical to its data model with the exception that data validation has been added to the Quantity property. The Range attribute forces the property to be within a min and max value. I have specified 1 for the minimum and the max value for an Int32. I’ve also specified a custom error message that will be displayed to the users if they do not enter a valid quantity range.

Example 14-13. CartItemViewModel

using Newtonsoft.Json;

using System;

using System.ComponentModel.DataAnnotations;

namespace ShoppingCart.ViewModels

{

public class CartItemViewModel

{

[JsonProperty(PropertyName = "id")]

public int Id { get; set; }

[JsonProperty(PropertyName = "cartId")]

public int CartId { get; set; }

[JsonProperty(PropertyName = "bookId")]

public int BookId { get; set; }

[JsonProperty(PropertyName = "quantity")]

[Range(1, Int32.MaxValue, ErrorMessage="Quantity must be greater than 0")]

public int Quantity { get; set; }

[JsonProperty(PropertyName = "book")]

public BookViewModel Book { get; set; }

}

}

Summary

The shopping cart project is starting to come together nicely. The database has been fully designed, created, and populated with some initial seed data. The next chapter will begin to create the layout for the shopping cart, including several items that will appear on every page of the site.