Bakery Tutorial

This is part of a tutorial series that shows how to build a data-drive web application using Visual Studio Code, ASP.NET Core Razor Pages, Entity Framework Core and SQLite and .NET 7.

The source code for the completed application is available on GitHub

Working with data in a Razor Pages application.

The Bakery Shop will become a data-driven web application, leveraging a relational database for efficient data storage. While alternative methods exist for data management, this pattern is widely adopted by the majority of web applications. With this foundation in place, several crucial decisions need to be made. How will the web application interact with the database, and what approach should be taken for data manipulation within the application?

Adopting a Model-based approach

When working with data in a .NET application, you have a choice. You can either shuttle data throughout the application in recordset or dataset structures, or you can represent the things (or entities) that your application is concerned with as classes, and use instances of those as containers for individual rows of data from the database.

The latter approach is recommended, as it facilitates working with data in a strongly-typed manner. This approach offers dual benefits: compile-time checking to minmise runtime errors; and potentially better quality code. It is a lot easier to reason about and maintain code where a piece of data is represented as Person.FirstName instead of ds.Tables[0].Rows[0]["FirstName"], which is the way that you would access an item of data in an ADO.NET DataSet object.

Collectively, the code representation of your application's entities is known as the Model. In the case of the Bakery application, the primary focus lies on the products offered by The Bakery Shop, resulting in a singular class within the model: Product.

Creating the model

Add a new folder to the root of the application named Models. Then add a C# class file to the Models folder using the following command:

dotnet new class -n Product -o Models

The generated file should have the following content:

namespace Bakery;
public class Product
{

}

Alter the namespace to Bakery.Models and add the following properties to the Product class:

namespace Bakery.Models;

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public string ImageName { get; set; }
}

Modern web applications commonly utilise Object-Relational Mappers (ORMs), which facilitate the mapping of database tables and columns to model classes and properties. ORMs enable seamless bidirectional data transfer between the application and the database. In addition to mapping, advanced ORMs provide additional functionalities such as automatic generation and execution of SQL commands, transaction management, and migrations.

The recommended ORM to use with ASP.NET Core applications is Entity Framework Core (EF Core). In the next step you will add it to the application.

Adding Entity Framework Core

EF Core depends on components called providers to enable it to work with specific databases. The EF Core team maintains official providers for SQL Server and SQLite as well as other database systems. In this exercise, you will use SQLite, which is a cross-platform file-based database.

When starting a new project, EF Core is not included in the project template by default. However, you can easily add it to your project by installing the corresponding NuGet package. To install EF Core with the SQLite provider into your application, execute the following command in the terminal to install the latest release version of the package:

dotnet add package Microsoft.EntityFrameworkCore.Sqlite

Adding and registering a Context

The core component of EF Core that you will utilise to interact with the database is a class derived from DbContext, commonly referred to as the "context". The context represents a session with the database and offers a comprehensive API that enables the following capabilities:

  1. Database Connections: The context facilitates establishing and managing connections to the database.

  2. Data Operations: You can perform various data operations, including querying and persisting data, using the context.

  3. Change Tracking: The context tracks changes made to the entities within its scope, allowing you to efficiently handle updates.

  4. Model Building: With the context, you can define and configure the model, specifying the structure and relationships of the entities.

  5. Data Mapping: The context handles the mapping between the model classes and the corresponding database tables and columns.

  6. Object Caching: To enhance performance, the context incorporates an object cache that stores frequently accessed entities.

  7. Transaction Management: The context supports managing transactions, ensuring data consistency and atomicity.

To add a context, create a folder named Data in the root of the project. Then add a C# class file to it named BakeryContext.cs. Amend the content as follows:

using Bakery.Models;
using Microsoft.EntityFrameworkCore;

namespace Bakery.Data;

public class BakeryContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite(@"Data source=Bakery.db");
    }
}

The context has one property - a DbSet named Products. The DbSet class maps to a table in the database. By default, mapping is controlled through name-based conventions. The name of the DbSet is expected to match the name of the corresponding table, and its column names will match property names on the Product class. EF Core also provides a powerful API for configuring the mapping where conventions need to be overridden, or configuration needs to be specified explicitly.

The OnConfiguring method provides a place for you to configure your context. In this case, you have configured the context to use the SQLite provider, and you have specified the connection string to be used.

When working with a context, you can just instantiate an instance wherever you need it, e.g.

using var context = new BakeryContext();
// do something with data

The DbContext implements IDisposable, so you are responsible for ensuring disposal of the context when you have finished with it. The usual pattern for managing this is to instantiate the context with a using statement as shown here (although technically this example shows a using declaration).

However, the recommended approach to working with a context in an ASP.NET Core application is to register it with the dependency injection (DI) system and make it available as a "service". The DI system takes responsibility for managing the lifetime of the context. Recall from the New Application section that service registration takes place in the Program.cs file.

The Dependency Inversion Principal

Dependency injection is the most common way to achieve the Dependency Inversion Principal (the "D" in SOLID), which states that high level modules (e.g. a consuming class) should not depend on low level modules (e.g. a DataContext). Both should depend on abstractions.

What this means in practice is that you can swap the implementation of the dependency without touching code in the consuming class. You control the actual implementation at the point of service registration. This is most useful when testing when you can swap the implementation of the service for a mock or test double so that your tests are not conducted against a live database, potentially slowing them down.

To register the context, add a using directive at the top of Program.cs to bring the Bakery.Data namespace into scope. Then call the AddDbContext extension method to register the BakeryContext with the service container:

using Bakery.Data;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<BakeryContext>();

Now the context can be made available anywhere within the application by injecting it via the class constructor. The AddDbContext method registers the context with a Scoped lifetime, meaning that a new one will be made available for each request. The services container will take care of ensuring that it is disposed of correctly.

Summary

You now have a model (small, but still a model), and an EF Core context. All you need now is a database. In the next section, you will see how EF Core migrations can be used to create one.

Last updated: 23/11/2023 09:10:19

© 2018 - 2024 - Mike Brind.
All rights reserved.
Contact me at Outlook.com