Entity Framework Core Data Definition

This document provides a comprehensive guide to defining your data models using Entity Framework Core (EF Core). EF Core allows you to map your .NET objects to relational database tables, simplifying data access and management.

Core Concepts

Entities

Entities are .NET classes that represent tables in your database. Each property of an entity class typically maps to a column in the corresponding table.

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Entity Types

EF Core discovers entity types by scanning your DbContext for DbSet<T> properties. You can also explicitly configure entity types using the ModelBuilder API.

DbSet

The DbSet<TEntity> property in your DbContext represents a collection of all entities of a given type in the context, or that can be queried from the database.

public class ApplicationDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    // ... other configurations
}

Mapping to Database Tables

Convention-Based Mapping

EF Core uses conventions to map your entity types to database tables and your properties to columns. By default:

Data Annotations

You can use data annotations from the System.ComponentModel.DataAnnotations namespace to configure your entity models directly within your classes.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class Product
{
    [Key]
    public int ProductId { get; set; }

    [Required]
    [MaxLength(100)]
    public string Name { get; set; }

    [Column(TypeName = "decimal(18,2)")]
    public decimal Price { get; set; }

    public int CategoryId { get; set; }
    [ForeignKey("CategoryId")]
    public Category Category { get; set; }
}

Fluent API

For more complex configurations or to avoid cluttering your entity classes, you can use the Fluent API in your DbContext's OnModelCreating method.

public class ApplicationDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>()
            .HasKey(p => p.ProductId);

        modelBuilder.Entity<Product>()
            .Property(p => p.Name)
            .IsRequired()
            .HasMaxLength(100);

        modelBuilder.Entity<Product>()
            .Property(p => p.Price)
            .HasColumnType("decimal(18,2)");

        modelBuilder.Entity<Product>()
            .HasOne(p => p.Category)
            .WithMany(c => c.Products)
            .HasForeignKey(p => p.CategoryId);
    }
}

Entity Relationships

One-to-Many

A common relationship where one entity can be associated with multiple other entities.

// Category class
public class Category
{
    public int CategoryId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

One-to-One

Each entity in one set corresponds to exactly one entity in another set.

// User class
public class User
{
    public int UserId { get; set; }
    public string Username { get; set; }
    public virtual UserProfile Profile { get; set; }
}

// UserProfile class
public class UserProfile
{
    public int UserId { get; set; } // Foreign key also serves as primary key
    public string FullName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public virtual User User { get; set; }
}

Configuration in OnModelCreating:

modelBuilder.Entity<User>()
    .HasOne(u => u.Profile)
    .WithOne(p => p.User)
    .HasForeignKey<UserProfile>(p => p.UserId);

Many-to-Many

Entities in one set can be associated with multiple entities in another set, and vice versa. This is typically implemented using a joining table.

// Post class
public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public virtual ICollection<Tag> Tags { get; set; }
}

// Tag class
public class Tag
{
    public int TagId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Post> Posts { get; set; }
}

EF Core can automatically create the joining table (e.g., `PostTag`) with the correct foreign keys when using conventions or explicit mapping in OnModelCreating.

Table and Column Naming

Table Names

Default: DbSet<Product> Products maps to a table named "Products".

Fluent API override:

modelBuilder.Entity<Product>().ToTable("ProductCatalog");

Column Names

Default: Property Name maps to a column named "Name".

Data Annotation override:

[Column("ProductName")]
public string Name { get; set; }

Fluent API override:

modelBuilder.Entity<Product>().Property(p => p.Name).HasColumnName("ProductName");

Key Configuration

Primary Keys

Configured automatically for properties named Id or <EntityName>Id.

Fluent API configuration:

modelBuilder.Entity<Product>().HasKey(p => p.ProductId);

Foreign Keys

Configured automatically based on naming conventions and navigation properties.

Fluent API configuration (already shown in relationships examples).

Value Generation

Identity (Auto-increment)

By default, integer primary keys are configured as identity columns (auto-incrementing).

Fluent API configuration:

modelBuilder.Entity<Product>()
    .Property(p => p.ProductId)
    .ValueGeneratedOnAdd();

Computed Values

For columns whose values are generated by the database (e.g., using triggers or default constraints).

modelBuilder.Entity<Product>()
    .Property(p => p.RowVersion)
    .IsRowVersion(); // Example for concurrency tokens

modelBuilder.Entity<Product>()
    .Property(p => p.CreatedAt)
    .ValueGeneratedOnAddOrUpdate(); // For audit timestamps

Data Types and Constraints

Column Types

You can explicitly specify the database data type for a column.

// Using Data Annotations
[Column(TypeName = "varchar(50)")]
public string Code { get; set; }

// Using Fluent API
modelBuilder.Entity<Product>().Property(p => p.Code).HasColumnType("varchar(50)");

Required Properties

Ensures that a column cannot contain null values.

// Using Data Annotations
[Required]
public string Name { get; set; }

// Using Fluent API
modelBuilder.Entity<Product>().Property(p => p.Name).IsRequired();

Max Length

Sets the maximum length for string or byte array columns.

// Using Data Annotations
[MaxLength(255)]
public string Description { get; set; }

// Using Fluent API
modelBuilder.Entity<Product>().Property(p => p.Description).HasMaxLength(255);

Uniqueness

Ensures that all values in a column are unique.

modelBuilder.Entity<Product>().HasIndex(p => p.Sku).IsUnique();
Note: EF Core provides a rich set of options for mapping your domain model to your database schema. Choose the approach (conventions, data annotations, or Fluent API) that best suits your project's needs and complexity.
Tip: For complex domain models, consider a combination of Fluent API for global configurations and data annotations for specific property-level details.