Introduction
This tutorial guides you through building a web application using ASP.NET Core Razor Pages and Entity Framework Core. We'll cover setting up your project, defining data models, interacting with a database, and creating a full CRUD (Create, Read, Update, Delete) experience for your data.
Razor Pages provide a page-focused model for building Razor-based web UIs with ASP.NET Core. Entity Framework Core (EF Core) is a modern object-relational mapper (ORM) for .NET that enables .NET developers to work with a database using domain-specific objects. It eliminates the need for most of the data-access code that developers traditionally need to write.
Prerequisites
- .NET SDK installed (version 6.0 or later recommended).
- A code editor (like Visual Studio Code, Visual Studio).
- Basic understanding of C# and ASP.NET Core.
Project Setup
First, create a new ASP.NET Core Web Application project using the Razor Pages template.
dotnet new razor -n MyRazorApp --output MyRazorApp
Navigate into the project directory:
cd MyRazorApp
Entity Framework Core Setup
Install NuGet Packages
You'll need to install the EF Core tools and the EF Core provider for your chosen database (e.g., SQL Server, SQLite).
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
Microsoft.EntityFrameworkCore.SqlServer
with Microsoft.EntityFrameworkCore.Sqlite
.
Define Model Class
Create a C# class to represent your data entity. For example, a Product
model.
Create a Models
folder in your project root and add the following file:
namespace MyRazorApp.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
}
}
Create DbContext
Create a class that inherits from DbContext
. This class represents your database session and allows you to query and save data.
Create a Data
folder and add the following file:
using Microsoft.EntityFrameworkCore;
using MyRazorApp.Models;
namespace MyRazorApp.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; }
}
}
Configure your database connection in appsettings.json
and register the DbContext in Program.cs
.
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyRazorAppDb;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
// ... other logging settings
},
"AllowedHosts": "*"
}
using Microsoft.EntityFrameworkCore;
using MyRazorApp.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
// Configure DbContext
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
Migrations
Create and apply database migrations to set up your database schema.
dotnet ef migrations add InitialCreate
dotnet ef database update
Seed Initial Data
You can seed your database with initial data. Modify your ApplicationDbContext
:
using Microsoft.EntityFrameworkCore;
using MyRazorApp.Models;
namespace MyRazorApp.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasData(
new Product { Id = 1, Name = "Laptop", Price = 1200.00m, Description = "Powerful processing and display." },
new Product { Id = 2, Name = "Keyboard", Price = 75.50m, Description = "Mechanical keyboard with RGB." },
new Product { Id = 3, Name = "Mouse", Price = 25.00m, Description = "Ergonomic wireless mouse." }
);
}
}
}
After adding seed data, re-apply migrations:
dotnet ef migrations add SeedProducts
dotnet ef database update
Razor Pages
Now, let's create Razor Pages for CRUD operations on our Product
model.
List Page
Create a folder named Products
under Pages
. Inside, create Index.cshtml
and Index.cshtml.cs
.
@page
@model MyRazorApp.Pages.Products.IndexModel
@{
ViewData["Title"] = "Products";
}
<h1>Product List</h1>
<p>
<a asp-page="Create" class="btn btn-primary">Create New Product</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Products[0].Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Products[0].Price)
</th>
<th>
@Html.DisplayNameFor(model => model.Products[0].Description)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Products) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.Id">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MyRazorApp.Models;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MyRazorApp.Pages.Products
{
public class IndexModel : PageModel
{
private readonly MyRazorApp.Data.ApplicationDbContext _context;
public IndexModel(MyRazorApp.Data.ApplicationDbContext context)
{
_context = context;
}
public IList<Product> Products { get;set; }
public async Task OnGetAsync()
{
Products = await _context.Products.ToListAsync();
}
}
}
Create Page
Create Create.cshtml
and Create.cshtml.cs
in the Pages/Products
folder.
@page
@model MyRazorApp.Pages.Products.CreateModel
@{
ViewData["Title"] = "Create Product";
}
<h1>Create Product</h1>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Product.Name" class="control-label"></label>
<input asp-for="Product.Name" class="form-control" />
<span asp-validation-for="Product.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Product.Price" class="control-label"></label>
<input asp-for="Product.Price" class="form-control" />
<span asp-validation-for="Product.Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Product.Description" class="control-label"></label>
<textarea asp-for="Product.Description" class="form-control"></textarea>
<span asp-validation-for="Product.Description" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
}
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using MyRazorApp.Models;
using MyRazorApp.Data;
using System.Threading.Tasks;
namespace MyRazorApp.Pages.Products
{
public class CreateModel : PageModel
{
private readonly ApplicationDbContext _context;
public CreateModel(ApplicationDbContext context)
{
_context = context;
}
[BindProperty]
public Product Product { get; set; }
public IActionResult OnGet()
{
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Products.Add(Product);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
Edit Page
Create Edit.cshtml
and Edit.cshtml.cs
in the Pages/Products
folder.
@page
@model MyRazorApp.Pages.Products.EditModel
@{
ViewData["Title"] = "Edit Product";
}
<h1>Edit Product</h1>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Product.Id" />
<div class="form-group">
<label asp-for="Product.Name" class="control-label"></label>
<input asp-for="Product.Name" class="form-control" />
<span asp-validation-for="Product.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Product.Price" class="control-label"></label>
<input asp-for="Product.Price" class="form-control" />
<span asp-validation-for="Product.Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Product.Description" class="control-label"></label>
<textarea asp-for="Product.Description" class="form-control"></textarea>
<span asp-validation-for="Product.Description" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
@section Scripts {
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
}
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MyRazorApp.Models;
using MyRazorApp.Data;
using System.Threading.Tasks;
namespace MyRazorApp.Pages.Products
{
public class EditModel : PageModel
{
private readonly ApplicationDbContext _context;
public EditModel(ApplicationDbContext context)
{
_context = context;
}
[BindProperty]
public Product Product { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Product = await _context.Products.FirstOrDefaultAsync(m => m.Id == id);
if (Product == null)
{
return NotFound();
}
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
var productToUpdate = await _context.Products.FirstOrDefaultAsync(m => m.Id == Product.Id);
if (productToUpdate == null)
{
return NotFound();
}
productToUpdate.Name = Product.Name;
productToUpdate.Price = Product.Price;
productToUpdate.Description = Product.Description;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductExists(Product.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool ProductExists(int id)
{
return _context.Products.Any(e => e.Id == id);
}
}
}
Delete Page
Create Delete.cshtml
and Delete.cshtml.cs
in the Pages/Products
folder.
@page
@model MyRazorApp.Pages.Products.DeleteModel
@{
ViewData["Title"] = "Delete Product";
}
<h1>Delete Product</h1>
<h3>Are you sure you want to delete this?</h3>
<div>
<h4>Product</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Product.Name)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Product.Name)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Product.Price)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Product.Price)
</dd>
<dt class="col-sm-2">
@Html.DisplayNameFor(model => model.Product.Description)
</dt>
<dd class="col-sm-10">
@Html.DisplayFor(model => model.Product.Description)
</dd>
</dl>
<form method="post">
<input type="hidden" asp-for="Product.Id" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-page="Index">Back to List</a>
</form>
</div>
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MyRazorApp.Models;
using MyRazorApp.Data;
using System.Threading.Tasks;
namespace MyRazorApp.Pages.Products
{
public class DeleteModel : PageModel
{
private readonly ApplicationDbContext _context;
public DeleteModel(ApplicationDbContext context)
{
_context = context;
}
[BindProperty]
public Product Product { get; set; }
public async Task<IActionResult> OnGetAsync(int id)
{
Product = await _context.Products.FirstOrDefaultAsync(m => m.Id == id);
if (Product == null)
{
return NotFound();
}
return Page();
}
public async Task<IActionResult> OnPostAsync(int id)
{
var product = await _context.Products.FindAsync(id);
if (product != null)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
return RedirectToPage("./Index");
}
}
}
Data Binding
Razor Pages uses a powerful data binding mechanism. Properties decorated with [BindProperty]
in the PageModel are automatically bound to form data submitted via POST requests.
The @Html.DisplayNameFor
and @Html.DisplayFor
helpers in the Razor views are useful for displaying attribute names and values from your models.
Data Validation
ASP.NET Core supports data annotations for validation. By decorating your model properties with validation attributes (e.g., [Required]
, [StringLength]
, [Range]
), you can enforce data integrity.
To enable client-side validation, ensure you have the necessary JavaScript libraries included in your layout or page:
<!-- ... other head content ... -->
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
<!-- ... other body content ... -->
<!-- Add these for unobtrusive validation -->
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
And in your page's Razor code (as shown in the Create and Edit examples):
@section Scripts {
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
}
The asp-validation-summary="ModelOnly"
and span asp-validation-for="..."
tags render validation messages.
Conclusion
You have successfully built a basic ASP.NET Core web application using Razor Pages and Entity Framework Core, implementing CRUD operations for your data. This foundation can be expanded with more complex features, enhanced UI, and advanced database interactions.
Explore further to learn about:
- Advanced EF Core querying (LINQ).
- Complex model relationships (one-to-many, many-to-many).
- Authentication and authorization.
- API development with ASP.NET Core.