EF CORE - PRODUCT CRUD WITH RAZOR PAGES

ENTITY FRAMEWORK CORE (EF CORE) — PART 2: PRODUCT CRUD WITH RAZOR PAGES

PART 1 created the Product model, ProductDbContext, migration and SQLite database. This tutorial adds complete Create, Read, Update and Delete pages to the dotnetef application.

1. Confirm the PART 1 starting point

Open a terminal in the project directory:

cd ~/dotnetprojects/dotnetef

Confirm the database and migration:

dotnet ef migrations list --context ProductDbContext
sqlite3 Data/products.db ".tables"
dotnet build

The database should contain Products and __EFMigrationsHistory.

2. Understand CRUD

OperationPurposeEF Core method
CreateInsert a new productAddAsync()
ReadRetrieve productsToListAsync(), FirstOrDefaultAsync()
UpdateChange an existing productSaveChangesAsync()
DeleteRemove a productRemove()

3. Install the Razor Pages scaffolding tool

Install the .NET 8 code generator:

dotnet tool install --global dotnet-aspnet-codegenerator \
  --version "8.*"

If it is already installed:

dotnet tool update --global dotnet-aspnet-codegenerator \
  --version "8.*"

Add both project packages required by the Razor Pages scaffolder. Keep the EF Core package on the same 8.0 patch version used elsewhere in this project:

dotnet add package \
  Microsoft.VisualStudio.Web.CodeGeneration.Design \
  --version 8.0.23

dotnet add package \
  Microsoft.EntityFrameworkCore.Tools \
  --version 8.0.30

dotnet add package \
  Microsoft.EntityFrameworkCore.SqlServer \
  --version 8.0.30

Confirm that the packages are present:

dotnet list package

The list should now include these packages:

  • Microsoft.EntityFrameworkCore.Design 8.0.30
  • Microsoft.EntityFrameworkCore.Sqlite.Core 8.0.30
  • Microsoft.EntityFrameworkCore.SqlServer 8.0.30
  • Microsoft.EntityFrameworkCore.Tools 8.0.30
  • Microsoft.VisualStudio.Web.CodeGeneration.Design 8.0.23
  • SQLitePCLRaw.bundle_sqlite3 2.1.11

Then restore and build:

dotnet restore
dotnet build

Do not continue to Step 4 if the build fails. The generator builds the project before it creates Pages/Products.

Why install the SQL Server package for a SQLite project? This version of the Razor Pages code generator checks for Microsoft.EntityFrameworkCore.SqlServer while preparing its design-time scaffolding services. Installing the package satisfies that scaffolding requirement; it does not convert the application or its database to SQL Server.

The application continues to use SQLite because Program.cs configures the context with UseSqlite():

builder.Services.AddDbContext<ProductDbContext>(options =>
    options.UseSqlite(
        builder.Configuration.GetConnectionString(
            "ProductDatabase"
        )
    )
);

Keep UseSqlite(). Do not replace it with UseSqlServer().

4. Generate the Product CRUD pages

dotnet aspnet-codegenerator razorpage \
  -m Product \
  -dc ProductDbContext \
  -udl \
  -outDir Pages/Products \
  --referenceScriptLibraries

The options mean:

OptionMeaning
-m ProductUse the Product model
-dc ProductDbContextUse the existing database context
-udlUse the project's shared layout
-outDir Pages/ProductsWrite pages to the Products directory
--referenceScriptLibrariesAdd client-side validation scripts

Run the generator only once for this exercise. If the files already exist, do not overwrite edited pages unless that is intentional.

5. Inspect the generated files

Pages/Products/
├── Create.cshtml
├── Create.cshtml.cs
├── Delete.cshtml
├── Delete.cshtml.cs
├── Details.cshtml
├── Details.cshtml.cs
├── Edit.cshtml
├── Edit.cshtml.cs
├── Index.cshtml
└── Index.cshtml.cs
PageRouteResponsibility
Index/ProductsList all products
Create/Products/CreateInsert a product
Details/Products/Details?id=1Display one product
Edit/Products/Edit?id=1Update one product
Delete/Products/Delete?id=1Confirm and remove one product

6. Add Products to the navigation bar

Open Pages/Shared/_Layout.cshtml. Add this list item after Privacy:

<li class="nav-item">
    <a class="nav-link text-dark"
       asp-page="/Products/Index">
        Products
    </a>
</li>

The navigation should now contain Home, Privacy and Products.

7. Understand the Read operation

The generated Pages/Products/Index.cshtml.cs follows this pattern:

using dotnetef.Data;
using dotnetef.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;

namespace dotnetef.Pages.Products;

public class IndexModel : PageModel
{
    private readonly ProductDbContext _context;

    public IndexModel(ProductDbContext context)
    {
        _context = context;
    }

    public IList<Product> Product { get; set; }
        = new List<Product>();

    public async Task OnGetAsync()
    {
        Product = await _context.Products
            .AsNoTracking()
            .ToListAsync();
    }
}

ProductDbContext is injected through the constructor. ToListAsync() executes the query asynchronously. AsNoTracking() avoids change tracking because the list is read-only.

8. Understand the Create operation

The important portion of Create.cshtml.cs is:

[BindProperty]
public Product Product { get; set; } = default!;

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    Product.CreatedAt = DateTime.UtcNow;

    await _context.Products.AddAsync(Product);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

[BindProperty] receives submitted form values. Validation attributes from Product.cs populate ModelState. The product is added only when the form is valid.

9. Prevent users from editing CreatedAt during creation

Remove the generated CreatedAt form group from Pages/Products/Create.cshtml. The server assigns it using:

Product.CreatedAt = DateTime.UtcNow;

This is safer than accepting a creation timestamp from the browser.

10. Understand the Details operation

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id is null)
    {
        return NotFound();
    }

    Product = await _context.Products
        .AsNoTracking()
        .FirstOrDefaultAsync(product =>
            product.Id == id
        );

    if (Product is null)
    {
        return NotFound();
    }

    return Page();
}

The identifier arrives through the query string. The handler returns HTTP 404 when the identifier is missing or no matching product exists.

11. Understand the Update operation

The Edit page loads the existing record during GET and saves submitted changes during POST.

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Attach(Product).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!await ProductExistsAsync(Product.Id))
        {
            return NotFound();
        }

        throw;
    }

    return RedirectToPage("./Index");
}

The concurrency check determines whether another request deleted the row before the edit was saved.

12. Protect CreatedAt during editing

The simplest tutorial-safe approach is to load the existing row and update only editable properties. Replace the generated Edit POST handler with:

public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var productToUpdate = await _context.Products
        .FirstOrDefaultAsync(product =>
            product.Id == Product.Id
        );

    if (productToUpdate is null)
    {
        return NotFound();
    }

    productToUpdate.Name = Product.Name;
    productToUpdate.Description = Product.Description;
    productToUpdate.Price = Product.Price;
    productToUpdate.StockQuantity = Product.StockQuantity;

    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}

Remove the editable CreatedAt input from Edit.cshtml. The original timestamp remains unchanged.

13. Understand the Delete operation

public async Task<IActionResult> OnPostAsync(int? id)
{
    if (id is null)
    {
        return NotFound();
    }

    var product = await _context.Products.FindAsync(id);

    if (product is not null)
    {
        _context.Products.Remove(product);
        await _context.SaveChangesAsync();
    }

    return RedirectToPage("./Index");
}

The Delete GET handler displays a confirmation page. The POST handler performs the irreversible database operation.

14. Build and run

dotnet build
dotnet run

Open:

http://localhost:<port>/Products

15. Test Create

  1. Click Create New.
  2. Enter a name, description, price and stock quantity.
  3. Click Create.
  4. Confirm that the new row appears in the list.

Test validation by submitting an empty name, price zero or negative stock.

16. Test Read, Update and Delete

  1. Open Details and confirm every stored value.
  2. Open Edit, change the price or stock, and save.
  3. Confirm that CreatedAt did not change.
  4. Open Delete.
  5. Confirm deletion and verify that the row disappears.

17. Verify records directly in SQLite

sqlite3 -header -column Data/products.db \
  "SELECT Id, Name, Price, StockQuantity, CreatedAt FROM Products;"

This confirms that the browser operations changed the physical SQLite database.

18. Understand the request pattern

RequestHandlerDatabase effect
GET /ProductsOnGetAsync()Read all
GET /Products/CreateOnGet()None
POST /Products/CreateOnPostAsync()Insert
GET /Products/Edit?id=1OnGetAsync()Read one
POST /Products/EditOnPostAsync()Update
POST /Products/DeleteOnPostAsync()Delete

19. Common problems

ProblemRemedy
Code generator is unavailableInstall the global .NET 8 scaffolding tool
Generator says to install Microsoft.EntityFrameworkCore.ToolsAdd Microsoft.EntityFrameworkCore.Tools version 8.0.30, restore and build, then run the generator again
Generator says to install Microsoft.EntityFrameworkCore.SqlServerAdd Microsoft.EntityFrameworkCore.SqlServer version 8.0.30, restore and build, then rerun the generator. Keep UseSqlite() in Program.cs
Pages/Products was not createdRead the first generator error, correct it, confirm dotnet build succeeds, then rerun Step 4 from the project root
Model or context cannot be foundBuild first and confirm their namespaces
no such table: ProductsRun dotnet ef database update
Validation messages do not appearConfirm the validation partial and script references
Product link gives 404Use asp-page="/Products/Index"
Changes are not savedConfirm that SaveChangesAsync() is awaited

What has been achieved?

  • Five Product Razor Pages have been generated.
  • Products can be created, listed, viewed, edited and deleted.
  • EF Core operations run asynchronously.
  • Model validation protects database input.
  • The server controls the creation timestamp.
  • CRUD changes can be verified directly in SQLite.

PART 2 complete: The dotnetef application now provides complete Product CRUD operations backed by Data/products.db.

Official references

Next: ENTITY FRAMEWORK CORE (EF CORE) — PART 3 adds ASP.NET Core Identity with a SQLite user database.