DEV Community

Cover image for Step-by-Step Guide: Build a CRUD Blazor App with Entity Framework and PostgreSQL
David Au Yeung
David Au Yeung

Posted on

Step-by-Step Guide: Build a CRUD Blazor App with Entity Framework and PostgreSQL

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
                }
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Configuring the Connection String

In appsettings.json, add:

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Database=BlazorDemo;Username=yourusername;Password=yourpassword"
  }
}
Enter fullscreen mode Exit fullscreen mode

Update Program.cs to register the DbContext:

builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
Enter fullscreen mode Exit fullscreen mode

And ensure DB creation:

using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    db.Database.EnsureCreated();
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Creating Migrations and Updating the Database

Generate and apply migrations:

dotnet ef migrations add InitialCreate
dotnet ef database update
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Add navigation in NavMenu.razor.

Step 8: Running the App

Run the app:

dotnet run
Enter fullscreen mode Exit fullscreen mode

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#!

Top comments (0)