Entities and Configuration

How a class becomes a table: keys, nullability and column types by convention, and the fluent API for when convention is wrong.

SyntaxmodelBuilder.Entity<Product>().Property(p => p.Name).HasMaxLength(200).IsRequired();

An entity is a class EF Core maps to a table. EF Core works out most of the mapping from the class itself. These guesses are called conventions.

What convention decides

The table name comes from the DbSet property name, so DbSet<Product> Products maps to a table called Products. A property called Id or ProductId becomes the primary key, the column whose value identifies a row. An int key is generated by the database on insert.

Nullability follows the C# type. Because nullable reference types are enabled, string Name becomes a NOT NULL column and string? Sku becomes a nullable one. A string with no length limit becomes an unbounded text column.

Saying it yourself

Two ways exist, and they do the same work. Data annotations are attributes on the class, such as [MaxLength(200)]. The fluent API is code in OnModelCreating, such as Property(p => p.Name).HasMaxLength(200). Annotations are shorter; the fluent API can express everything, including indexes and composite keys, and keeps database detail out of the class. Most teams use annotations for validation and the fluent API for schema.

What trips people up

The model decides the schema, so a schema the model does not describe is a schema EF Core will try to change on the next migration. Decide on lengths, precision and indexes before the first migration, not after the table has rows in it.

Example

Example · csharp
// dotnet add package Microsoft.EntityFrameworkCore.Sqlite
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

// Reading context.Model builds the mapping. It touches no database.
using var context = new ShopContext();
IEntityType product = context.Model.FindEntityType(typeof(Product))!;

Console.WriteLine($"Table: {product.GetTableName()}");
foreach (IProperty property in product.GetProperties().OrderBy(p => p.Name))
{
    Console.WriteLine($"{property.Name} -> {(property.IsNullable ? "optional" : "required")}");
}

public class Product
{
    public int Id { get; set; }
    public required string Name { get; set; }   // not nullable, so NOT NULL
    public string? Sku { get; set; }            // nullable, so NULL allowed
    public int PriceCents { get; set; }         // money as whole cents, never double
}

public class ShopContext : DbContext
{
    public DbSet<Product> Products => Set<Product>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlite("Data Source=shop.db");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>(entity =>
        {
            entity.ToTable("products");                      // override the conventional name
            entity.Property(p => p.Name).HasMaxLength(200);
            entity.HasIndex(p => p.Sku).IsUnique();
        });
    }
}

// Output:
// Table: products
// Id -> required
// Name -> required
// PriceCents -> required
// Sku -> optional

When to use it

  • Give every string column a sensible maximum length before the first migration is generated.
  • Map C# property names such as PriceCents onto existing snake_case columns in a database you did not create.
  • Add a unique index so the database itself rejects two products with the same SKU.

More examples

Annotations and the fluent API side by side

Read both forms, because other people's code uses both. Use annotations when the rule is about the class, and the fluent API when the rule is about the database, which keeps System.ComponentModel attributes out of types you might also send over HTTP.

Example · csharp
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

// Shorthand: attributes on the class.
[Table("products")]
public class ProductAnnotated
{
    [Key]
    public int Id { get; set; }

    [Required]
    [MaxLength(200)]
    public string Name { get; set; } = "";

    [Column("price_cents")]
    public int PriceCents { get; set; }
}

// Longhand: the same mapping expressed in OnModelCreating.
// Both produce the same table. Neither is more powerful for these four lines,
// but only the fluent form can express indexes, composite keys and relationships.
public class ShopContext : DbContext
{
    public DbSet<ProductFluent> Products => Set<ProductFluent>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ProductFluent>(entity =>
        {
            entity.ToTable("products");
            entity.HasKey(p => p.Id);
            entity.Property(p => p.Name).IsRequired().HasMaxLength(200);
            entity.Property(p => p.PriceCents).HasColumnName("price_cents");
        });
    }
}

One configuration class per entity

This is the layout to reach for once a model has more than a handful of entities. Each file holds one table's rules, and ApplyConfigurationsFromAssembly means adding a new entity does not mean editing a shared method.

Example · csharp
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

// OnModelCreating becomes unreadable at twenty entities. Move each entity's
// mapping into its own class that implements IEntityTypeConfiguration<T>.
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
    public void Configure(EntityTypeBuilder<Product> builder)
    {
        builder.ToTable("products");
        builder.Property(p => p.Name).IsRequired().HasMaxLength(200);
        builder.Property(p => p.Sku).HasMaxLength(32);
        builder.HasIndex(p => p.Sku).IsUnique();
    }
}

public class ShopContext(DbContextOptions<ShopContext> options) : DbContext(options)
{
    public DbSet<Product> Products => Set<Product>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Finds every IEntityTypeConfiguration<T> in this assembly and applies it.
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ShopContext).Assembly);
    }
}

A composite key and an exact column type

Join tables and lookup tables often have no single identity column. HasPrecision matters because a decimal mapped by default can lose the cents, and the loss shows up only once real prices are stored.

Example · csharp
using Microsoft.EntityFrameworkCore;

public class OrderLine
{
    public int OrderId { get; set; }
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

public class SalesContext(DbContextOptions<SalesContext> options) : DbContext(options)
{
    public DbSet<OrderLine> OrderLines => Set<OrderLine>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<OrderLine>(entity =>
        {
            // A key made of two columns together. Convention cannot guess this,
            // so it must be stated; without it EF Core reports no key at all.
            entity.HasKey(line => new { line.OrderId, line.ProductId });

            // decimal has no single right database type, so say which one.
            entity.Property(line => line.UnitPrice).HasPrecision(18, 2);
        });
    }
}

Discussion

  • Be the first to comment on this lesson.