Introduction
Happy New Year 2026 to all! This year, I want to share more basic must-learn knowledge on .NET. This tutorial is my first sharing on dev.to this year. Happy to see you all again.
Entity Framework (EF) is Microsoft's object-relational mapper (ORM) for .NET. It simplifies data access by allowing you to work with databases using .NET objects, eliminating the need for most data-access code. In this tutorial, we'll build a simple CRUD (Create, Read, Update, Delete) Blazor Server app using EF Core with PostgreSQL. We'll cover setting up the project, defining models, seeding data, and creating a basic UI for managing customers.
By the end, you'll have a functional app demonstrating EF's power for database operations.
Prerequisites
Before we begin, ensure you have the following:
- Visual Studio or any C# IDE (e.g., VS Code).
- .NET 10 SDK installed (download from Microsoft's site).
- PostgreSQL installed and running (download from EnterpriseDB). Set up a database (e.g., "BlazorDemo") and note the connection details.
Step 1: Setting Up the Blazor Project
Create a new Blazor Server app:
dotnet new blazorserver --force
This scaffolds a basic Blazor Server project.
Step 2: Adding Entity Framework Packages
Add the necessary NuGet packages for EF Core and PostgreSQL:
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
-
Microsoft.EntityFrameworkCore: Core EF functionality.
-
Microsoft.EntityFrameworkCore.Tools: For migrations and database commands.
-
Npgsql.EntityFrameworkCore.PostgreSQL: PostgreSQL provider.
Step 3: Creating Models
Define your data models. Create a Models folder and add classes like Customer.cs:
using System.ComponentModel.DataAnnotations;
namespace BlazorWithEntityFramework.Models
{
public class Customer
{
[Key]
public long CustomerId { get; set; }
[Required]
[StringLength(1000)]
public string CustomerNumber { get; set; } = string.Empty;
[Required]
[StringLength(100)]
public string LastName { get; set; } = string.Empty;
[Required]
[StringLength(100)]
public string FirstName { get; set; } = string.Empty;
[Required]
public DateTime Dob { get; set; }
public bool IsDeleted { get; set; } = false;
[StringLength(100)]
public string CreateBy { get; set; } = "SYSTEM";
public DateTime CreateDate { get; set; } = DateTime.UtcNow;
[StringLength(100)]
public string ModifyBy { get; set; } = "SYSTEM";
public DateTime ModifyDate { get; set; } = DateTime.UtcNow;
}
}
Product.cs:
using System.ComponentModel.DataAnnotations;
namespace BlazorWithEntityFramework.Models
{
public class Product
{
[Key]
public long ProductId { get; set; }
[Required]
[StringLength(1000)]
public string ProductName { get; set; } = string.Empty;
[Required]
[StringLength(1000)]
public string ProductCode { get; set; } = string.Empty;
[Required]
[Range(0, int.MaxValue)]
public int AvailableQuantity { get; set; }
public bool IsDeleted { get; set; } = false;
[StringLength(100)]
public string CreateBy { get; set; } = "SYSTEM";
public DateTime CreateDate { get; set; } = DateTime.UtcNow;
[StringLength(100)]
public string ModifyBy { get; set; } = "SYSTEM";
public DateTime ModifyDate { get; set; } = DateTime.UtcNow;
public ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
}
}
Order.cs:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BlazorWithEntityFramework.Models
{
public class Order
{
[Key]
public long OrderId { get; set; }
public long? CustomerId { get; set; }
[Required]
[StringLength(1000)]
public string OrderNumber { get; set; } = string.Empty;
[Required]
public DateTime OrderDate { get; set; }
public bool IsDeleted { get; set; } = false;
[StringLength(100)]
public string CreateBy { get; set; } = "SYSTEM";
public DateTime CreateDate { get; set; } = DateTime.UtcNow;
[StringLength(100)]
public string ModifyBy { get; set; } = "SYSTEM";
public DateTime ModifyDate { get; set; } = DateTime.UtcNow;
[ForeignKey("CustomerId")]
public Customer? Customer { get; set; }
public ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
}
}
OrderItem.cs:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace BlazorWithEntityFramework.Models
{
public class OrderItem
{
[Key]
public long OrderItemId { get; set; }
public long? OrderId { get; set; }
public long? ProductId { get; set; }
[Required]
[Range(1, int.MaxValue)]
public int Quantity { get; set; }
public bool IsDeleted { get; set; } = false;
[StringLength(100)]
public string CreateBy { get; set; } = "SYSTEM";
public DateTime CreateDate { get; set; } = DateTime.UtcNow;
[StringLength(100)]
public string ModifyBy { get; set; } = "SYSTEM";
public DateTime ModifyDate { get; set; } = DateTime.UtcNow;
[ForeignKey("OrderId")]
public Order? Order { get; set; }
[ForeignKey("ProductId")]
public Product? Product { get; set; }
}
}
Step 4: Setting Up the DbContext
Create a Data folder and add AppDbContext.cs:
using Microsoft.EntityFrameworkCore;
using BlazorWithEntityFramework.Models;
namespace BlazorWithEntityFramework.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Customer> Customers { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Seed data
modelBuilder.Entity<Customer>().HasData(
new Customer
{
CustomerId = 1,
CustomerNumber = "CUST0001",
LastName = "Au Yeung",
FirstName = "David",
Dob = new DateTime(1980, 12, 31, 0, 0, 0, DateTimeKind.Utc),
IsDeleted = false,
CreateBy = "SYSTEM",
CreateDate = DateTime.UtcNow,
ModifyBy = "SYSTEM",
ModifyDate = DateTime.UtcNow
},
new Customer
{
CustomerId = 2,
CustomerNumber = "CUST0002",
LastName = "Chan",
FirstName = "Peter",
Dob = new DateTime(1982, 1, 15, 0, 0, 0, DateTimeKind.Utc),
IsDeleted = false,
CreateBy = "SYSTEM",
CreateDate = DateTime.UtcNow,
ModifyBy = "SYSTEM",
ModifyDate = DateTime.UtcNow
}
);
modelBuilder.Entity<Product>().HasData(
new Product
{
ProductId = 1,
ProductName = "Android Phone",
ProductCode = "A0001",
AvailableQuantity = 100,
IsDeleted = false,
CreateBy = "SYSTEM",
CreateDate = DateTime.UtcNow,
ModifyBy = "SYSTEM",
ModifyDate = DateTime.UtcNow
},
new Product
{
ProductId = 2,
ProductName = "iPhone",
ProductCode = "I0001",
AvailableQuantity = 100,
IsDeleted = false,
CreateBy = "SYSTEM",
CreateDate = DateTime.UtcNow,
ModifyBy = "SYSTEM",
ModifyDate = DateTime.UtcNow
}
);
modelBuilder.Entity<Order>().HasData(
new Order
{
OrderId = 1,
CustomerId = 1,
OrderNumber = "ORD0001",
OrderDate = DateTime.UtcNow,
IsDeleted = false,
CreateBy = "SYSTEM",
CreateDate = DateTime.UtcNow,
ModifyBy = "SYSTEM",
ModifyDate = DateTime.UtcNow
}
);
modelBuilder.Entity<OrderItem>().HasData(
new OrderItem
{
OrderItemId = 1,
OrderId = 1,
ProductId = 2,
Quantity = 10,
IsDeleted = false,
CreateBy = "SYSTEM",
CreateDate = DateTime.UtcNow,
ModifyBy = "SYSTEM",
ModifyDate = DateTime.UtcNow
}
);
}
}
}
Step 5: Configuring the Connection String
In appsettings.json, add:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=BlazorDemo;Username=yourusername;Password=yourpassword"
}
}
Update Program.cs to register the DbContext:
builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
And ensure DB creation:
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.EnsureCreated();
}
Step 6: Creating Migrations and Updating the Database
Generate and apply migrations:
dotnet ef migrations add InitialCreate
dotnet ef database update
EnsureCreated() in Program.cs ensures the database is created and seeded on startup. This works alongside migrations for a demo. Note that in production, you might prefer migrations exclusively for better control.
Step 7: Building the CRUD UI
Create a page like Pages/Customers.razor for managing customers:
@page "/customers"
@inject AppDbContext DbContext
<h3>Customers</h3>
@if (customers == null)
{
<p>Loading...</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Customer Number</th>
<th>Last Name</th>
<th>First Name</th>
<th>DOB</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var customer in customers.Where(c => !c.IsDeleted))
{
<tr>
<td>@customer.CustomerId</td>
<td>@customer.CustomerNumber</td>
<td>@customer.LastName</td>
<td>@customer.FirstName</td>
<td>@customer.Dob.ToShortDateString()</td>
<td>
<button class="btn btn-primary" @onclick="() => EditCustomer(customer)">Edit</button>
<button class="btn btn-danger" @onclick="() => DeleteCustomer(customer)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
<button class="btn btn-success" @onclick="AddCustomer">Add New Customer</button>
@if (isEditing)
{
<div class="modal" style="display:block;">
<div class="modal-content">
<h4>@(editingCustomer.CustomerId == 0 ? "Add" : "Edit") Customer</h4>
<form>
<div class="form-group">
<label>Customer Number</label>
<input type="text" class="form-control" @bind="editingCustomer.CustomerNumber" />
</div>
<div class="form-group">
<label>Last Name</label>
<input type="text" class="form-control" @bind="editingCustomer.LastName" />
</div>
<div class="form-group">
<label>First Name</label>
<input type="text" class="form-control" @bind="editingCustomer.FirstName" />
</div>
<div class="form-group">
<label>DOB</label>
<input type="date" class="form-control" @bind="editingCustomer.Dob" />
</div>
<button type="button" class="btn btn-primary" @onclick="SaveCustomer">Save</button>
<button type="button" class="btn btn-secondary" @onclick="CancelEdit">Cancel</button>
</form>
</div>
</div>
}
}
@code {
private List<Customer> customers = new();
private Customer editingCustomer = new();
private bool isEditing = false;
protected override async Task OnInitializedAsync()
{
await LoadCustomers();
}
private async Task LoadCustomers()
{
customers = await DbContext.Customers.AsNoTracking().ToListAsync();
}
private void AddCustomer()
{
editingCustomer = new Customer { Dob = DateTime.UtcNow };
isEditing = true;
}
private void EditCustomer(Customer customer)
{
editingCustomer = new Customer
{
CustomerId = customer.CustomerId,
CustomerNumber = customer.CustomerNumber,
LastName = customer.LastName,
FirstName = customer.FirstName,
Dob = customer.Dob,
IsDeleted = customer.IsDeleted,
CreateBy = customer.CreateBy,
CreateDate = customer.CreateDate,
ModifyBy = "USER",
ModifyDate = DateTime.UtcNow
};
isEditing = true;
}
private async Task SaveCustomer()
{
if (editingCustomer.CustomerId == 0)
{
DbContext.Customers.Add(editingCustomer);
}
else
{
var existing = await DbContext.Customers.FindAsync(editingCustomer.CustomerId);
if (existing != null)
{
DbContext.Entry(existing).CurrentValues.SetValues(editingCustomer);
}
}
await DbContext.SaveChangesAsync();
isEditing = false;
await LoadCustomers();
}
private void CancelEdit()
{
isEditing = false;
}
private async Task DeleteCustomer(Customer customer)
{
var existing = await DbContext.Customers.FindAsync(customer.CustomerId);
if (existing != null)
{
existing.IsDeleted = true;
existing.ModifyBy = "USER";
existing.ModifyDate = DateTime.UtcNow;
DbContext.Customers.Update(existing);
}
await DbContext.SaveChangesAsync();
await LoadCustomers();
}
}
Add navigation in NavMenu.razor.
Step 8: Running the App
Run the app:
Navigate to /customers to test CRUD operations.

Conclusion
You've built a CRUD Blazor app with EF and PostgreSQL! EF simplifies database interactions, and Blazor provides a seamless UI. Experiment with more entities or features like validation.
Feel free to ask questions or expand on this. Happy coding!
Love C#!