Hassan Agmir Hassan Agmir

What it does function OnModelCreating

Hassan Agmir

Create the DbContext

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseSqlServer("Server=localhost;Database=MyAppDb;Trusted_Connection=True;TrustServerCertificate=True;");
        // PostgreSQL example:
        // options.UseNpgsql("Host=localhost;Database=myapp;Username=postgres;Password=secret");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>()
            .HasOne(p => p.Category)
            .WithMany(c => c.Products)
            .HasForeignKey(p => p.CategoryId);
    }
}

In real apps, don't hardcode the connection string — pull it from appsettings.json via dependency injection instead (see step 6).

What it does function OnModelCreating

OnModelCreating is where you configure how your C# classes map to database tables — beyond what EF Core can figure out automatically. This specific code tells EF Core:

"A Product has one Category (via the Category property), and a Category has many Products (via the Products list). Use Product.CategoryId as the foreign key column."

Do you actually need it here? Often no.

EF Core has convention-based configuration — it can often infer relationships automatically just from your property names and types. Given your Product and Category classes:

public class Product
{
    public int CategoryId { get; set; }
    public Category? Category { get; set; }
}

public class Category
{
    public List<Product> Products { get; set; } = new();
}

EF Core will automatically detect this is a one-to-many relationship and use CategoryId as the foreign key — because the naming follows convention ({NavigationProperty}Id). So for this exact setup, you could skip OnModelCreating entirely and it would still work.

When OnModelCreating actually becomes necessary

Situation

Example

Foreign key name doesn't follow convention

public int CatRef { get; set; } instead of CategoryId

Multiple relationships between the same two entities

A Product has both a Category and a SecondaryCategory — EF can't guess which FK goes where

Composite keys

Primary key made of 2+ columns

Setting constraints

Max string length, required fields, unique indexes

Renaming tables/columns

Table Products should map to DB table tbl_products

Delete behavior

.OnDelete(DeleteBehavior.Cascade) vs Restrict

Seeding data

modelBuilder.Entity<Category>().HasData(...)

Example where it's actually needed

public class Product
{
    public int Id { get; set; }
    public int MainCategoryId { get; set; }
    public Category MainCategory { get; set; } = null!;

    public int? BackupCategoryId { get; set; }
    public Category? BackupCategory { get; set; }
}

Here EF Core cannot guess which FK maps to which navigation property — you'd have to be explicit:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .HasOne(p => p.MainCategory)
        .WithMany()
        .HasForeignKey(p => p.MainCategoryId);

    modelBuilder.Entity<Product>()
        .HasOne(p => p.BackupCategory)
        .WithMany()
        .HasForeignKey(p => p.BackupCategoryId)
        .OnDelete(DeleteBehavior.Restrict); // don't cascade-delete on backup
}

Bottom line for your current project

For your simple Product/Category setup, it's not strictly necessary — but it's good practice to keep it anyway because:

  1. It's explicit and self-documenting (anyone reading the code immediately sees the relationship)

  2. It protects you if you rename a property later and accidentally break the convention

  3. It's the natural place to add OnDelete behavior, which you'll likely want to think about (what happens to Products when a Category is deleted?)

The DbSet properties — yes, these stay in AppDbContext

There's no way around this part; every entity needs a DbSet<T> property in the context. But this part is fine to keep centralized since it's just a list, not logic:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();
    public DbSet<Order> Orders => Set<Order>();
    // ... one line per entity, this stays short and readable
}

The relationship config — move to IEntityTypeConfiguration<T> (one file per entity)

Instead of stacking everything inside OnModelCreating, EF Core lets you split each entity's configuration into its own class. This is the standard clean pattern used in real projects.

Configurations/ProductConfiguration.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using DemoApi.Models;

namespace DemoApi.Data.Configurations;

public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
    public void Configure(EntityTypeBuilder<Product> builder)
    {
        builder.HasOne(p => p.Category)
            .WithMany(c => c.Products)
            .HasForeignKey(p => p.CategoryId);

        builder.Property(p => p.Name)
            .IsRequired()
            .HasMaxLength(200);
    }
}

Configurations/CategoryConfiguration.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using DemoApi.Models;

namespace DemoApi.Data.Configurations;

public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
    public void Configure(EntityTypeBuilder<Category> builder)
    {
        builder.Property(c => c.Name)
            .IsRequired()
            .HasMaxLength(100);
    }
}

Then AppDbContext.OnModelCreating becomes tiny — one line loads them all

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}

ApplyConfigurationsFromAssembly automatically finds every class implementing IEntityTypeConfiguration<T> in your project and applies it. You never touch this line again, even as you add 20 more entities.

Updated project structure

DemoApi/
├── Models/
│   ├── Product.cs
│   └── Category.cs
│
├── Data/
│   ├── AppDbContext.cs
│   └── Configurations/
│       ├── ProductConfiguration.cs
│       └── CategoryConfiguration.cs

Why this is better

Approach

Problem as project grows

Everything in OnModelCreating

One giant method, hard to review, merge conflicts in git when multiple people touch the same file

IEntityTypeConfiguration<T> per entity

Each entity's rules live next to that entity's concerns; adding entity #21 doesn't touch old files at all

So going forward, when you add a new model

  1. Create Models/Order.cs

  2. Add public DbSet<Order> Orders => Set<Order>(); to AppDbContext.cs (this one line is unavoidable)

  3. Create Data/Configurations/OrderConfiguration.cs for its relationships/constraints

  4. OnModelCreating needs zero changes — ApplyConfigurationsFromAssembly picks it up automatically

  5. Run dotnet ef migrations add AddOrder

Subscribe to my Newsletters

Stay updated with the latest programming tips, tricks, and IT insights! Join my community to receive exclusive content on coding best practices.

© Copyright 2025 by Hassan Agmir . Built with ❤ by Me