What it does function OnModelCreating
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
Producthas oneCategory(via theCategoryproperty), and aCategoryhas manyProducts(via theProductslist). UseProduct.CategoryIdas 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 |
|
Multiple relationships between the same two entities | A |
Composite keys | Primary key made of 2+ columns |
Setting constraints | Max string length, required fields, unique indexes |
Renaming tables/columns | Table |
Delete behavior |
|
Seeding data |
|
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:
It's explicit and self-documenting (anyone reading the code immediately sees the relationship)
It protects you if you rename a property later and accidentally break the convention
It's the natural place to add
OnDeletebehavior, 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.csWhy this is better
Approach | Problem as project grows |
|---|---|
Everything in | One giant method, hard to review, merge conflicts in git when multiple people touch the same file |
| 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
Create
Models/Order.csAdd
public DbSet<Order> Orders => Set<Order>();toAppDbContext.cs(this one line is unavoidable)Create
Data/Configurations/OrderConfiguration.csfor its relationships/constraintsOnModelCreatingneeds zero changes —ApplyConfigurationsFromAssemblypicks it up automaticallyRun
dotnet ef migrations add AddOrder