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
| Operation | Purpose | EF Core method |
|---|---|---|
| Create | Insert a new product | AddAsync() |
| Read | Retrieve products | ToListAsync(), FirstOrDefaultAsync() |
| Update | Change an existing product | SaveChangesAsync() |
| Delete | Remove a product | Remove() |
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.30Microsoft.EntityFrameworkCore.Sqlite.Core 8.0.30Microsoft.EntityFrameworkCore.SqlServer 8.0.30Microsoft.EntityFrameworkCore.Tools 8.0.30Microsoft.VisualStudio.Web.CodeGeneration.Design 8.0.23SQLitePCLRaw.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:
| Option | Meaning |
|---|---|
-m Product | Use the Product model |
-dc ProductDbContext | Use the existing database context |
-udl | Use the project's shared layout |
-outDir Pages/Products | Write pages to the Products directory |
--referenceScriptLibraries | Add 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
| Page | Route | Responsibility |
|---|---|---|
| Index | /Products | List all products |
| Create | /Products/Create | Insert a product |
| Details | /Products/Details?id=1 | Display one product |
| Edit | /Products/Edit?id=1 | Update one product |
| Delete | /Products/Delete?id=1 | Confirm 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
- Click Create New.
- Enter a name, description, price and stock quantity.
- Click Create.
- 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
- Open Details and confirm every stored value.
- Open Edit, change the price or stock, and save.
- Confirm that
CreatedAtdid not change. - Open Delete.
- 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
| Request | Handler | Database effect |
|---|---|---|
GET /Products | OnGetAsync() | Read all |
GET /Products/Create | OnGet() | None |
POST /Products/Create | OnPostAsync() | Insert |
GET /Products/Edit?id=1 | OnGetAsync() | Read one |
POST /Products/Edit | OnPostAsync() | Update |
POST /Products/Delete | OnPostAsync() | Delete |
19. Common problems
| Problem | Remedy |
|---|---|
| Code generator is unavailable | Install the global .NET 8 scaffolding tool |
Generator says to install Microsoft.EntityFrameworkCore.Tools | Add Microsoft.EntityFrameworkCore.Tools version 8.0.30, restore and build, then run the generator again |
Generator says to install Microsoft.EntityFrameworkCore.SqlServer | Add Microsoft.EntityFrameworkCore.SqlServer version 8.0.30, restore and build, then rerun the generator. Keep UseSqlite() in Program.cs |
Pages/Products was not created | Read the first generator error, correct it, confirm dotnet build succeeds, then rerun Step 4 from the project root |
| Model or context cannot be found | Build first and confirm their namespaces |
no such table: Products | Run dotnet ef database update |
| Validation messages do not appear | Confirm the validation partial and script references |
| Product link gives 404 | Use asp-page="/Products/Index" |
| Changes are not saved | Confirm 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
- Microsoft Learn: Add a model to Razor Pages
- Microsoft Learn: Razor Pages CRUD with EF Core
- Microsoft Learn: ASP.NET Core code generator
Next: ENTITY FRAMEWORK CORE (EF CORE) — PART 3 adds ASP.NET Core Identity with a SQLite user database.