Monday, 11 September, 2017 UTC


Summary

In my previous post on Entity Framework Core (EF Core) I explained the basics of creating a model class, a context class and using EF Core with a simple ASP.NET Core MVC web application.  This time I’ll pick up where that post left off, looking at relationships to build a more real world data model.  In a future post, I’ll cover migrations which help manage the data model over time and keep it in sync with the database.
At this time, EF Core only knows about Product.  To design a more complete e-commerce solution will require more models.  Now I’ll be ripping out some of the code from the previous post.  But that’s ok, as a real world solution takes shape parts will be replaced.  I’m going to start off by creating an Order model class to replace the current one:
using System.Collections.Generic;

namespace CoreStore.Models
{
    public class Order
    {
        public int OrderID { get; set; }
        public ICollection<OrderLine> OrderLines { get; set; }
    }
}
The main focus here is on the collection of OrderLine objects which record the each product the user purchases and how many of that product.  This class does not exists so I’ll create it as well:
namespace CoreStore.Models
{
    public class OrderLine
    {
        public int OrderLineID { get; set; }

        public int OrderID { get; set; }
        public int ProductID { get; set; }
        public int Quantity { get; set; }

        public Order Order { get; set; }
        public Product Product { get; set; }
    }
}
This class contains what at first look may seem repetitive but it’s actually a pattern that will let EF Core do it’s work. The OrderLineID will be the primary key of the OrderLine table that I’ll add to the context in a minute.  The OrderID and ProductID refer to primary keys of the Order and Product tables.  However, this means more to the database than it does to humans.  Instead, we prefer to think of the Order and Product classes which are what the navigation properties at the bottom are for.  The great thing about EF Core is that it glues all of this together for you if you follow the pattern.  No complicated SQL joins need to be written or deciphered.  The Quantity is obviously the amount ordered of the product.  Given this OrderLine class, I can now go back to the Order class and add a method to total the amount of the Order.
public decimal Total()
{
    decimal total = 0.0m;
    foreach (var orderLine in OrderLines)
    {
        total += orderLine.Product.Price * orderLine.Quantity;
    }
    return total;
}
That will wrap up the data model.  Now to use it in the application.  First I’ll go to the ProductController and remove the Order actions methods.  I’m going to move these to a new OrderController.  This also means that the Order an Receipt views need to be removed from the Product view folder.  Now I can add OrderController.cs to the Controllers folder:
using System.Linq;
using CoreStore.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace CoreStore.Controllers
{
    public class OrderController: Controller
    {
        private StoreContext context; 

        public IActionResult Index()
        {
            var currentOrderId = HttpContext.Session.GetString("currentOrderId");

            if (currentOrderId == null)
            {
                var orders = context.Orders.Include(o => o.OrderLines).ThenInclude(ol => ol.Product);
                return View(orders.ToList());
            }
            else 
            {
                var currentOrder = context.Orders.Where(o => o.OrderID == int.Parse(currentOrderId)).Include(o => o.OrderLines).ThenInclude(ol => ol.Product).First();
                return View("CurrentOrder", currentOrder);
            }
        }
    }
}
The Index method has two options.  First it checks the Session for the key currentOrderId.  If the key does not exist GetString will return null and will mean that there is no currently open order.  In that case I’ll display a list of past orders.  Otherwise, I’ll the Order for the currentOrderId (which needs to be cast to an int) and display it.  Notice the LINQ query in both cases.  The Include and ThenInclude methods accept a lambda expression to retrieve the related objects.  In both cases I want to display product data for the order.  The product data in stored in the order lines.  First I have to get the OrderLines and then the Product in the OrderLine.  The default view shows the list:
@model IEnumerable<CoreStore.Models.Order>

<h1>Completed Orders</h1>

<table>
    <tr>
        <th>Order ID</th>
        <th>Order Total</th>
        <th></th>
    </tr>
    @foreach(var order in @Model)
    {
        <tr>
            <td>@order.OrderID.ToString()</td>
            <td>@order.Total().ToString("F2")</td>
            <td><a href="/Order/Details/@order.OrderID.ToString()">View</a></td>
        </tr>
    }
</table>

<a href="/Product">Place a new order.</a>
And the CurrentOrder view shows a single Order, the currently open order:
@model CoreStore.Models.Order

<h1>Current Order</h1>

<table>
    <tr>
        <th>Product</th>
        <th>Price</th>
        <th>Quantity</th>
    </tr>
    @foreach(var orderLine in @Model.OrderLines)
    {
        <tr>
            <td>@orderLine.Product.Name</td>
            <td>$@orderLine.Product.Price.ToString("F2")</td>
            <td>@orderLine.Quantity</td>
        </tr>
    }
</table>

<div>
    <a href="/Products">Keep Shopping</a>
    <a href="/Order/Checkout">Checkout</a>
</div>
Now to add items to the Order.  I can reuse the existing Index view for the ProductController.  The only changes I’ll make are in the Order link.  The controller should be Order and not Product.  And the action I’ll change to Purchase.  Now I need to create this Purchase action on the OrderController:
[HttpGet]
public IActionResult Purchase(int id)
{
    var product = context.Products.Where(p => p.ProductID == id).FirstOrDefault();

    if (product == null)
    {
        return View("NotFound");
    }
    else
    {
        return View(product);
    }
}
This is similar to handling purchases in the previous post.  And so is the default view:
@model CoreStore.Models.Product

<p>How many @Model.Name would you like to purchase? They are $@Model.Price.ToString("F2") each.</p>

<form action="/Order/Purchase" method="post">
    <input type="text" name="Quantity"/>
    <input type="hidden" name="ProductID" value="@Model.ProductID"/>
    <input type="submit" value="Purchase"/>
</form>
Before writing the action method for the POST verb, I’ll create the DTO that will be used to wrap the form data and pass it to the action method.  This time I’ll put it in a new folder called DTO:
namespace CoreStore.DTO
{
    public class PurchaseDTO
    {
        public int ProductID { get; set; }
        public int Quantity { get; set; }
    }
}
The POST action method for Purchase will take the PurchaseDTO and use it to add a new OrderLine to the current open order.  If there is no open Order, one will be created:
[HttpPost]
public IActionResult Purchase(PurchaseDTO dto)
{
    var currentOrderId = HttpContext.Session.GetString("currentOrderId");

    if (currentOrderId == null)
    {
        var newOrder = new Order();
        context.Orders.Add(newOrder);
        context.SaveChanges();
        currentOrderId = newOrder.OrderID.ToString();
        HttpContext.Session.SetString("currentOrderId", currentOrderId);
    }

    var currentOrder = context.Orders.Where(o => o.OrderID == int.Parse(currentOrderId)).First();
    var product = context.Products.Where(p => p.ProductID == dto.ProductID).First();
    var orderLine = new OrderLine {
        Product = product, Order = currentOrder, Quantity = dto.Quantity
    };
    context.OrderLines.Add(orderLine);
    currentOrder.OrderLines.Add(orderLine);
    context.SaveChanges();

    return RedirectToAction("Index");
}
It may seem like there is a lot going on here, but that’s just because there are so many different object being used.  There is nothing in this code that hasn’t already been covered.
The only thing left to do is create the Checkout action method and view:
public IActionResult Checkout()
{
    var currentOrderId = HttpContext.Session.GetString("currentOrderId");

    if (currentOrderId == null)
    {
        return RedirectToAction("Index");
    }
    else
    {
        var currentOrder = context.Orders.Where(o => o.OrderID == int.Parse(currentOrderId)).Include(o => o.OrderLines).ThenInclude(OrderLine => OrderLine.Product).First();
        HttpContext.Session.Remove("currentOrderId");
        return View(currentOrder);
    }
}
The only new code here has nothing to do with EF Core.  It’s the line that removes the currentOrderId key from Session.  This will tell the application that no current order exists and that a new one should be made.  And the view is also simple.
@model CoreStore.Models.Order

<h1>Thank you for your order!</h1>

<h4>Your total is $@Model.Total().ToString("F2")</h4>

<div>
    <a href="/Product">Place a new order</a>
    <a href="/Order">View order history</a>
</div>
The application is almost complete! (for this post at least)  All that remains is to implement the Details action on the OrderController.  It will take the ID of the Order to view:
public IActionResult Details(int id)
{
    var order = context.Orders.Where(o => o.OrderID == id).Include(o => o.OrderLines).ThenInclude(ol => ol.Product).FirstOrDefault();

    if (order == null)
    {
        return View("NotFound");
    }
    else
    {
        return View(order);
    }
}
Since the Details view will contain Product information, I need to capture the OrderLine and Product data with the Include and ThenInclude LINQ methods.  The default view is very similar to the CurrentOrder view:
@model CoreStore.Models.Order

<table>
    <tr>
        <th>Product</th>
        <th>Price</th>
        <th>Quantity</th>
    </tr>
    @foreach(var orderLine in @Model.OrderLines)
    {
        <tr>
            <td>@orderLine.Product.Name</td>
            <td>$@orderLine.Product.Price.ToString("F2")</td>
            <td>@orderLine.Quantity</td>
        </tr>
    }
</table>

<div>
    <a href="/">Home</a>
</div>
So now the user can add items to an order and place new orders.  Now you may have noticed there are some things missing from the data model.  And adding them now could be problematic.  But you can’t imagine all of the applications’ needs when it is designed, especially for an application that will get used for years.  So the data model need to be able to grow.  In the next EF Core post I’ll look at how this can work.
The finished code for this post can be found on Github.