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.

Command Line
dotnet new razor -n MyRazorApp --output MyRazorApp

Navigate into the project directory:

Command Line
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).

Command Line (for SQL Server)
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
If you prefer SQLite, replace 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:

MyRazorApp/Models/Product.cs
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:

MyRazorApp/Data/ApplicationDbContext.cs
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.

MyRazorApp/appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyRazorAppDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    // ... other logging settings
  },
  "AllowedHosts": "*"
}
MyRazorApp/Program.cs
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.

Command Line
dotnet ef migrations add InitialCreate
dotnet ef database update

Seed Initial Data

You can seed your database with initial data. Modify your ApplicationDbContext:

MyRazorApp/Data/ApplicationDbContext.cs (Updated)
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:

Command Line
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.

MyRazorApp/Pages/Products/Index.cshtml
@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>
MyRazorApp/Pages/Products/Index.cshtml.cs
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.

MyRazorApp/Pages/Products/Create.cshtml
@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>
}
MyRazorApp/Pages/Products/Create.cshtml.cs
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.

MyRazorApp/Pages/Products/Edit.cshtml
@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>
}
MyRazorApp/Pages/Products/Edit.cshtml.cs
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.

MyRazorApp/Pages/Products/Delete.cshtml
@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>
MyRazorApp/Pages/Products/Delete.cshtml.cs
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:

MyRazorApp/_Layout.cshtml (Snippet)
<!-- ... 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.