Chapter 4: Defining the Model (Fluent API vs. Data Annotations) - IndianTechnoEra
Latest update Android YouTube

Chapter 4: Defining the Model (Fluent API vs. Data Annotations)

Welcome to Chapter 4! In the previous chapters, we used Data Annotations ([Required], [MaxLength]) to configure our Book entity. However, EF Core offers two ways to configure your model: Data Annotations and Fluent API. In this chapter, we'll explore both approaches in depth, understand when to use each, and learn how to precisely control how our C# classes map to database tables.

4.1 The Three Ways to Configure EF Core Models

EF Core provides three levels of configuration, each with increasing power and complexity:

Configuration Method Description Example
Conventions Built-in rules that EF Core applies automatically. You don't write any code. Id becomes primary key. string becomes nvarchar(max).
Data Annotations Attributes placed on your entity classes and properties. [Required], [MaxLength(200)], [Table("Books")]
Fluent API Code configuration inside OnModelCreating method. modelBuilder.Entity<Book>().Property(b => b.Title).IsRequired()

Configuration Precedence:

 Fluent API > Data Annotations > Conventions 

If you configure the same thing with multiple methods, Fluent API wins. This allows you to start with conventions, override with Data Annotations when needed, and use Fluent API for complex configurations.


4.2 Setting Up Our Learning Project

Let's create a more comprehensive model to explore all configuration options. We'll expand our Book Store with related entities.

Create new entity classes:

Models/Author.cs:

 namespace EFCoreTutorial.Models; public class Author { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string? Biography { get; set; } public DateTime? BirthDate { get; set; } // Navigation property - An author can have many books public ICollection<Book> Books { get; set; } = new List<Book>(); } 

Models/Category.cs:

 namespace EFCoreTutorial.Models; public class Category { public int Id { get; set; } public string Name { get; set; } public string? Description { get; set; } // Navigation property - Many-to-many relationship public ICollection<Book> Books { get; set; } = new List<Book>(); } 

Models/Review.cs:

 namespace EFCoreTutorial.Models; public class Review { public int Id { get; set; } public int Rating { get; set; } // 1-5 stars public string? Comment { get; set; } public string ReviewerName { get; set; } public DateTime ReviewDate { get; set; } // Foreign key public int BookId { get; set; } // Navigation property public Book Book { get; set; } } 

Update Models/Book.cs:

 using System.ComponentModel.DataAnnotations; namespace EFCoreTutorial.Models; public class Book { public int Id { get; set; } [Required] [MaxLength(200)] public string Title { get; set; } // This will be replaced by Author relationship // public string? Author { get; set; } [DataType(DataType.Currency)] public decimal Price { get; set; } public DateTime? PublishedOn { get; set; } public string? ISBN { get; set; } [MaxLength(50)] public string? Genre { get; set; } // Foreign key for Author public int? AuthorId { get; set; } // Navigation properties public Author? Author { get; set; } public ICollection<Review> Reviews { get; set; } = new List<Review>(); public ICollection<Category> Categories { get; set; } = new List<Category>(); } 

Update Data/AppDbContext.cs:

 using Microsoft.EntityFrameworkCore; using EFCoreTutorial.Models; namespace EFCoreTutorial.Data; public class AppDbContext : DbContext { public DbSet<Book> Books { get; set; } public DbSet<Author> Authors { get; set; } public DbSet<Category> Categories { get; set; } public DbSet<Review> Reviews { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { string connectionString = @"Server=(localdb)\mssqllocaldb;Database=EFCoreBookStore;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True;"; optionsBuilder.UseSqlServer(connectionString); optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information); optionsBuilder.EnableSensitiveDataLogging(); // For learning purposes } protected override void OnModelCreating(ModelBuilder modelBuilder) { // We'll add Fluent API configurations here base.OnModelCreating(modelBuilder); } } 

Now let's create a migration to set up these tables:

 dotnet ef migrations add AddAuthorCategoryReview dotnet ef database update 

4.3 Conventions: The Default Behavior

Before we start configuring, let's understand what EF Core does by default. These conventions save you from writing repetitive code.

Common EF Core Conventions:

Convention Default Behavior Example
Primary Key Property named Id or <TypeName>Id becomes the primary key. Book.Id → Primary Key
Author.Id → Primary Key
Table Name Table name matches DbSet<T> property name or plural of entity name. DbSet<Book> BooksBooks table
Column Names Column names match property names. Title property → Title column
Data Types stringnvarchar(max)
intint
decimaldecimal(18,2)
string Titlenvarchar(max)
Nullability Reference types are nullable if marked with ?, otherwise required. string? Biography → NULL column
string FirstName → NOT NULL
Foreign Keys Property named <NavigationPropertyName>Id or <PrincipalEntityName>Id becomes FK. Book.AuthorId → Foreign Key to Authors table
Relationships Navigation properties automatically create relationships. Book.Author + Author.Books → One-to-Many

Run the migration we just created and examine the generated tables in SSMS. You'll see all these conventions in action.


4.4 Data Annotations: Attributes on Your Entities

Data Annotations are attributes from System.ComponentModel.DataAnnotations and System.ComponentModel.DataAnnotations.Schema namespaces. They're simple and keep configuration close to the properties they affect.

Common Data Annotations:

Attribute Purpose Example
[Key] Marks a property as the primary key (when convention doesn't apply) [Key] public int BookCode { get; set; }
[Required] Makes the property NOT NULL in database [Required] public string Title { get; set; }
[MaxLength(n)] Sets maximum length for string properties [MaxLength(200)] public string Title { get; set; }
[StringLength(n)] Sets both max length and min length (for validation) [StringLength(200, MinimumLength = 3)]
[Column] Configures column name, type, and order [Column("BookTitle", TypeName = "nvarchar(200)")]
[Table] Specifies the table name and schema [Table("BooksInventory", Schema = "store")]
[ForeignKey] Specifies which property is the foreign key [ForeignKey("AuthorId")] public Author Author { get; set; }
[InverseProperty] Specifies which navigation property to use when multiple relationships exist [InverseProperty("WrittenBooks")]
[Index] Creates an index on the column (EF Core 5+) [Index(nameof(ISBN), IsUnique = true)]
[NotMapped] Excludes the property from database mapping [NotMapped] public string FullName => FirstName + " " + LastName;
[DatabaseGenerated] Configures value generation (Identity, Computed, None) [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Timestamp] Configures property as a concurrency token (row version) [Timestamp] public byte[] RowVersion { get; set; }

Example: Enhanced Book entity with Data Annotations

Let's enhance our Book entity with more data annotations:

 using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EFCoreTutorial.Models; [Table("Books", Schema = "dbo")] [Index(nameof(ISBN), IsUnique = true, Name = "IX_Books_ISBN")] [Index(nameof(Title), nameof(AuthorId), Name = "IX_Books_Title_Author")] public class Book { [Key] public int Id { get; set; } [Required(ErrorMessage = "Title is required")] [StringLength(200, MinimumLength = 1, ErrorMessage = "Title must be between 1 and 200 characters")] [Column("BookTitle", TypeName = "nvarchar(200)")] public string Title { get; set; } [Column(TypeName = "decimal(10, 2)")] [Range(0, 9999.99, ErrorMessage = "Price must be between 0 and 9999.99")] public decimal Price { get; set; } [Column("PublicationDate")] [DataType(DataType.Date)] public DateTime? PublishedOn { get; set; } [StringLength(13, MinimumLength = 10)] [RegularExpression(@"^(?:\d{10}|\d{13})$", ErrorMessage = "ISBN must be 10 or 13 digits")] public string? ISBN { get; set; } [StringLength(50)] public string? Genre { get; set; } // Foreign key public int? AuthorId { get; set; } // Navigation properties [ForeignKey("AuthorId")] public Author? Author { get; set; } public ICollection<Review> Reviews { get; set; } = new List<Review>(); [NotMapped] public string DisplayTitle => $"{Title} (${Price})"; [NotMapped] public bool IsCheap => Price < 20; } 

Pros and Cons of Data Annotations:

✅ Advantages ❌ Disadvantages
Simple and easy to understand Clutters entity classes with attributes
Configuration is close to the property Limited functionality (can't do complex configurations)
Works well for simple validations Mixes persistence concerns with domain model
IntelliSense support Less reusable (same attributes everywhere)

4.5 Fluent API: Configuration in Code

Fluent API configures your model using code inside the OnModelCreating method. It's more powerful and keeps your entity classes clean.

Basic Fluent API Syntax:

 protected override void OnModelCreating(ModelBuilder modelBuilder) { // Configure the Book entity modelBuilder.Entity<Book>(entity => { // Table configuration entity.ToTable("Books", "dbo"); // Primary key entity.HasKey(b => b.Id); // Property configurations entity.Property(b => b.Title) .IsRequired() .HasMaxLength(200) .HasColumnName("BookTitle") .HasColumnType("nvarchar(200)"); entity.Property(b => b.Price) .HasColumnType("decimal(10, 2)") .HasDefaultValue(0); entity.Property(b => b.PublishedOn) .HasColumnName("PublicationDate") .HasColumnType("date"); // Indexes entity.HasIndex(b => b.ISBN) .IsUnique() .HasDatabaseName("IX_Books_ISBN"); entity.HasIndex(b => new { b.Title, b.AuthorId }) .HasDatabaseName("IX_Books_Title_Author"); // Ignore properties entity.Ignore(b => b.DisplayTitle); entity.Ignore(b => b.IsCheap); }); base.OnModelCreating(modelBuilder); } 

Common Fluent API Methods:

Category Method Purpose
Table ToTable() Sets table name and schema
HasDefaultSchema() Sets default schema for all tables
HasAnnotation() Adds custom annotations
Property HasKey() Sets primary key
IsRequired() Makes property NOT NULL
HasMaxLength() Sets maximum length
HasColumnType() Sets specific SQL data type
HasColumnName() Sets column name
HasDefaultValue() Sets default value
HasDefaultValueSql() Sets default value using SQL
ValueGeneratedOnAdd() Value generated on add (identity)
IsConcurrencyToken() Marks as concurrency token
Index HasIndex() Creates an index
IsUnique() Makes index unique
HasFilter() Adds filter condition
HasDatabaseName() Sets index name
Relationships HasOne() Configures one-to-one or one-to-many from principal
HasMany() Configures one-to-many or many-to-many from principal
WithOne() Configures the other side of one-to-one
WithMany() Configures the other side of one-to-many
HasForeignKey() Specifies foreign key property
OnDelete() Configures delete behavior (Cascade, Restrict, etc.)
Query HasQueryFilter() Adds global query filter (e.g., soft delete)
Owned OwnsOne() Configures owned entity (value object)
OwnsMany() Configures collection of owned entities

4.6 Fluent API in Depth: Complete Example

Let's configure all our entities using Fluent API in a clean, organized way.

Updated AppDbContext.cs with Fluent API:

 using Microsoft.EntityFrameworkCore; using EFCoreTutorial.Models; namespace EFCoreTutorial.Data; public class AppDbContext : DbContext { public DbSet<Book> Books { get; set; } public DbSet<Author> Authors { get; set; } public DbSet<Category> Categories { get; set; } public DbSet<Review> Reviews { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { string connectionString = @"Server=(localdb)\mssqllocaldb;Database=EFCoreBookStore;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True;"; optionsBuilder.UseSqlServer(connectionString); optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information); optionsBuilder.EnableSensitiveDataLogging(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Apply all configurations ConfigureBook(modelBuilder); ConfigureAuthor(modelBuilder); ConfigureCategory(modelBuilder); ConfigureReview(modelBuilder); // Or use this approach with separate configuration classes (see section 4.9) // modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); base.OnModelCreating(modelBuilder); } private void ConfigureBook(ModelBuilder modelBuilder) { modelBuilder.Entity<Book>(b => { // Table b.ToTable("Books", "dbo"); // Primary Key b.HasKey(b => b.Id); // Properties b.Property(b => b.Title) .IsRequired() .HasMaxLength(200) .HasColumnName("BookTitle") .HasColumnType("nvarchar(200)"); b.Property(b => b.Price) .HasColumnType("decimal(10, 2)") .HasDefaultValue(0); b.Property(b => b.PublishedOn) .HasColumnName("PublicationDate") .HasColumnType("date"); b.Property(b => b.ISBN) .HasMaxLength(13) .IsRequired(false); b.Property(b => b.Genre) .HasMaxLength(50); // Indexes b.HasIndex(b => b.ISBN) .IsUnique() .HasDatabaseName("IX_Books_ISBN") .HasFilter("[ISBN] IS NOT NULL"); b.HasIndex(b => new { b.Title, b.AuthorId }) .HasDatabaseName("IX_Books_Title_Author"); // Relationships b.HasOne(b => b.Author) .WithMany(a => a.Books) .HasForeignKey(b => b.AuthorId) .OnDelete(DeleteBehavior.SetNull) // If author deleted, set AuthorId to null .HasConstraintName("FK_Books_Author"); // Ignored properties b.Ignore(b => b.DisplayTitle); b.Ignore(b => b.IsCheap); }); } private void ConfigureAuthor(ModelBuilder modelBuilder) { modelBuilder.Entity<Author>(a => { a.ToTable("Authors", "dbo"); a.HasKey(a => a.Id); a.Property(a => a.FirstName) .IsRequired() .HasMaxLength(50); a.Property(a => a.LastName) .IsRequired() .HasMaxLength(50); a.Property(a => a.Biography) .HasMaxLength(2000); a.Property(a => a.BirthDate) .HasColumnType("date"); // Computed column for full name a.Property(a => a.FullName) .HasComputedColumnSql("[FirstName] + ' ' + [LastName]"); // Index on name a.HasIndex(a => new { a.LastName, a.FirstName }) .HasDatabaseName("IX_Authors_Name"); }); } private void ConfigureCategory(ModelBuilder modelBuilder) { modelBuilder.Entity<Category>(c => { c.ToTable("Categories", "dbo"); c.HasKey(c => c.Id); c.Property(c => c.Name) .IsRequired() .HasMaxLength(100); c.Property(c => c.Description) .HasMaxLength(500); // Many-to-many relationship with Book c.HasMany(c => c.Books) .WithMany(b => b.Categories) .UsingEntity<Dictionary<string, object>>( "BookCategories", // Join table name j => j.HasOne<Book>().WithMany().HasForeignKey("BookId"), j => j.HasOne<Category>().WithMany().HasForeignKey("CategoryId"), j => { j.HasKey("BookId", "CategoryId"); j.ToTable("BookCategories", "dbo"); }); }); } private void ConfigureReview(ModelBuilder modelBuilder) { modelBuilder.Entity<Review>(r => { r.ToTable("Reviews", "dbo"); r.HasKey(r => r.Id); r.Property(r => r.Rating) .IsRequired(); r.Property(r => r.Comment) .HasMaxLength(1000); r.Property(r => r.ReviewerName) .IsRequired() .HasMaxLength(100); r.Property(r => r.ReviewDate) .HasDefaultValueSql("GETDATE()"); // Relationship with Book r.HasOne(r => r.Book) .WithMany(b => b.Reviews) .HasForeignKey(r => r.BookId) .OnDelete(DeleteBehavior.Cascade); // If book deleted, delete its reviews }); } } 

4.7 Adding a Computed Column Example

Notice we added a FullName computed column in the Author configuration. Let's add that property to the Author class:

 namespace EFCoreTutorial.Models; public class Author { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string? Biography { get; set; } public DateTime? BirthDate { get; set; } // Computed column - will be calculated by database public string FullName { get; set; } public ICollection<Book> Books { get; set; } = new List<Book>(); } 

Now create a migration to apply these configurations:

 dotnet ef migrations add FluentAPIConfigurations dotnet ef database update 

4.8 Data Annotations vs. Fluent API: Comparison

Aspect Data Annotations Fluent API
Readability Easy to see configuration on the property Configuration centralized in one place
Separation of Concerns Mixes persistence with domain model Keeps domain models clean (POCOs)
Power Limited to what attributes provide Full access to all EF Core features
Complex Relationships Difficult or impossible for complex scenarios Complete control over relationships
Reusability Must repeat attributes on each entity Can create reusable configuration classes
Validation Works with ASP.NET Core validation Database only, no UI validation
IntelliSense Excellent Good, but requires knowing method names
Learning Curve Lower Higher, more methods to learn

When to Use Each:

  • Use Data Annotations for:
    • Simple validations that also apply to UI ([Required], [StringLength])
    • Quick and simple configurations
    • When you want configuration visible on the property
  • Use Fluent API for:
    • Complex relationship configurations
    • Indexes and constraints
    • When following Clean Architecture/Domain-Driven Design
    • Configurations that don't belong in domain models
    • Advanced features (computed columns, query filters)
  • Best Practice: Use a mix. Keep simple validations as annotations, use Fluent API for database-specific configurations.

4.9 Organizing Fluent API with IEntityTypeConfiguration<T>

As your model grows, putting all configurations in OnModelCreating becomes messy. EF Core allows you to create separate configuration classes.

Step 1: Create configuration classes

Data/Configurations/BookConfiguration.cs:

 using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using EFCoreTutorial.Models; namespace EFCoreTutorial.Data.Configurations; public class BookConfiguration : IEntityTypeConfiguration<Book> { public void Configure(EntityTypeBuilder<Book> builder) { builder.ToTable("Books", "dbo"); builder.HasKey(b => b.Id); builder.Property(b => b.Title) .IsRequired() .HasMaxLength(200) .HasColumnName("BookTitle"); builder.Property(b => b.Price) .HasColumnType("decimal(10, 2)") .HasDefaultValue(0); builder.Property(b => b.PublishedOn) .HasColumnName("PublicationDate") .HasColumnType("date"); builder.HasIndex(b => b.ISBN) .IsUnique() .HasDatabaseName("IX_Books_ISBN"); builder.HasOne(b => b.Author) .WithMany(a => a.Books) .HasForeignKey(b => b.AuthorId) .OnDelete(DeleteBehavior.SetNull); } } 

Data/Configurations/AuthorConfiguration.cs:

 using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using EFCoreTutorial.Models; namespace EFCoreTutorial.Data.Configurations; public class AuthorConfiguration : IEntityTypeConfiguration<Author> { public void Configure(EntityTypeBuilder<Author> builder) { builder.ToTable("Authors", "dbo"); builder.HasKey(a => a.Id); builder.Property(a => a.FirstName) .IsRequired() .HasMaxLength(50); builder.Property(a => a.LastName) .IsRequired() .HasMaxLength(50); builder.Property(a => a.FullName) .HasComputedColumnSql("[FirstName] + ' ' + [LastName]"); } } 

Step 2: Apply configurations in DbContext

 protected override void OnModelCreating(ModelBuilder modelBuilder) { // Apply all configurations from this assembly modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); // Or apply individually // modelBuilder.ApplyConfiguration(new BookConfiguration()); // modelBuilder.ApplyConfiguration(new AuthorConfiguration()); base.OnModelCreating(modelBuilder); } 

The ApplyConfigurationsFromAssembly method automatically finds and applies all classes implementing IEntityTypeConfiguration<T>.


4.10 Global Configurations and Conventions

EF Core 6+ allows you to configure conventions globally.

Setting Max String Length Globally:

 protected override void OnModelCreating(ModelBuilder modelBuilder) { // Make all string properties nvarchar(100) by default foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { if (property.ClrType == typeof(string)) { property.SetMaxLength(100); } } } base.OnModelCreating(modelBuilder); } 

Configuring All Decimal Properties:

 protected override void OnModelCreating(ModelBuilder modelBuilder) { // Set all decimal properties to decimal(18,4) foreach (var property in modelBuilder.Model.GetEntityTypes() .SelectMany(t => t.GetProperties()) .Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?))) { property.SetColumnType("decimal(18,4)"); } base.OnModelCreating(modelBuilder); } 

Removing Conventions:

 protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { // Remove the pluralizing table name convention configurationBuilder.Conventions.Remove(typeof(TableNameFromDbSetConvention)); // Or add custom conventions // configurationBuilder.Conventions.Add(...); } 

4.11 Practical Examples: Common Configurations

1. Unique Constraint on Email

 // Fluent API modelBuilder.Entity<User>() .HasIndex(u => u.Email) .IsUnique(); // Data Annotation [Index(nameof(Email), IsUnique = true)] public class User { public int Id { get; set; } public string Email { get; set; } } 

2. Default Values

 // Fluent API modelBuilder.Entity<Order>() .Property(o => o.OrderDate) .HasDefaultValueSql("GETDATE()"); modelBuilder.Entity<Order>() .Property(o => o.Status) .HasDefaultValue("Pending"); 

3. Excluding Properties

 // Fluent API modelBuilder.Entity<Book>() .Ignore(b => b.DisplayTitle); // Data Annotation [NotMapped] public string DisplayTitle => $"{Title}"; 

4. Composite Primary Key

 // Fluent API modelBuilder.Entity<BookCategory>() .HasKey(bc => new { bc.BookId, bc.CategoryId }); // Data Annotation - Not possible, must use Fluent API 

5. Shadow Properties

 // Properties not in your entity class but exist in database modelBuilder.Entity<Book>() .Property<DateTime>("LastUpdated"); // Query using shadow property var books = context.Books .Where(b => EF.Property<DateTime>(b, "LastUpdated") > DateTime.Now.AddDays(-7)) .ToList(); 

6. Query Filters (Soft Delete)

 // Add a global filter to exclude soft-deleted records modelBuilder.Entity<Book>() .HasQueryFilter(b => !b.IsDeleted); // Or for multi-tenancy modelBuilder.Entity<Book>() .HasQueryFilter(b => b.TenantId == _currentTenantId); 

4.12 Creating and Applying Migrations

Now that we have all our configurations, let's create a migration and see the SQL generated.

Step 1: Create a migration

 dotnet ef migrations add FullModelConfiguration 

Step 2: Examine the migration code

Open the generated migration file. You'll see all our configurations translated into migration operations:

 protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Authors", schema: "dbo", columns: table => new { Id = table.Column<int>(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), FirstName = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false), LastName = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false), Biography = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true), BirthDate = table.Column<DateTime>(type: "date", nullable: true), FullName = table.Column<string>(type: "nvarchar(max)", nullable: true, computedColumnSql: "[FirstName] + ' ' + [LastName]") }, constraints: table => { table.PrimaryKey("PK_Authors", x => x.Id); }); // More tables... } 

Step 3: Apply the migration

 dotnet ef database update 

Step 4: Verify in SSMS

Open SQL Server Management Studio and examine:

  • Table structures (columns, data types, nullability)
  • Indexes (right-click table → Indexes)
  • Foreign Keys (right-click table → Keys)
  • Default Constraints (expand table → Constraints)

4.13 Testing Our Configuration

Let's write a simple program to test our configured model:

 using EFCoreTutorial.Data; using EFCoreTutorial.Models; using var db = new AppDbContext(); // Create an author var author = new Author { FirstName = "Robert", LastName = "Martin", Biography = "Software engineer and author, known for promoting software design principles.", BirthDate = new DateTime(1952, 12, 5) }; // Create books var book1 = new Book { Title = "Clean Code", Price = 45.99m, PublishedOn = new DateTime(2008, 8, 1), ISBN = "9780132350884", Genre = "Programming", Author = author }; var book2 = new Book { Title = "Clean Architecture", Price = 39.99m, PublishedOn = new DateTime(2017, 9, 10), ISBN = "9780134494166", Genre = "Programming", Author = author }; // Add categories var category1 = new Category { Name = "Software Development", Description = "Books about coding" }; var category2 = new Category { Name = "Best Sellers", Description = "Popular books" }; book1.Categories.Add(category1); book1.Categories.Add(category2); book2.Categories.Add(category1); // Add reviews book1.Reviews.Add(new Review { Rating = 5, Comment = "Essential reading for every developer!", ReviewerName = "John Doe", ReviewDate = DateTime.Now }); // Save everything db.Authors.Add(author); db.SaveChanges(); Console.WriteLine("Data saved successfully!"); // Query with all relationships var booksWithDetails = db.Books .Include(b => b.Author) .Include(b => b.Categories) .Include(b => b.Reviews) .ToList(); foreach (var book in booksWithDetails) { Console.WriteLine($"\nBook: {book.Title} by {book.Author?.FullName}"); Console.WriteLine($"Price: {book.Price:C}, Genre: {book.Genre}"); Console.WriteLine($"Categories: {string.Join(", ", book.Categories.Select(c => c.Name))}"); Console.WriteLine($"Reviews: {book.Reviews.Count}"); } 

4.14 Best Practices Summary

  1. Keep entities clean (POCOs) - Use Fluent API for database-specific configurations, especially in large applications.
  2. Use Data Annotations for validation - They work with both EF Core and ASP.NET Core validation.
  3. Separate configurations into classes - Use IEntityTypeConfiguration<T> for better organization.
  4. Be explicit about relationships - Configure foreign keys and delete behaviors explicitly.
  5. Add indexes for performance - Index foreign keys and frequently queried columns.
  6. Use meaningful constraint names - Makes debugging easier.
  7. Consider database defaults - Use HasDefaultValueSql for database-generated defaults.
  8. Document complex configurations - Add comments explaining why certain configurations exist.

Chapter Summary

Concept Key Takeaway
Conventions EF Core's built-in rules that work without any configuration. Good for simple scenarios.
Data Annotations Attributes on entity classes. Simple and visible, but clutter entities and have limited power.
Fluent API Code configuration in OnModelCreating. More powerful, keeps entities clean, but more verbose.
IEntityTypeConfiguration<T> Separate configuration classes for better organization and reusability.
Indexes Improve query performance. Can be configured with [Index] or Fluent API.
Relationships Configure with Has/With methods in Fluent API for precise control.
Value Generation Control how values are generated (Identity, Computed, Default).
Shadow Properties Properties in the database not mapped to entity classes.
Query Filters Global filters applied to all queries (soft delete, multi-tenancy).

What's Next?

Congratulations! You now have a deep understanding of model configuration in EF Core. You can precisely control how your C# classes map to database tables.

In Chapter 5: Relationships in EF Core, we will dive deep into:

  • One-to-Many relationships (Authors → Books)
  • Many-to-Many relationships (Books ↔ Categories)
  • One-to-One relationships (Book ↔ BookDetail)
  • Self-referencing relationships (Category → ParentCategory)
  • Configuring cascade delete behaviors
  • Handling optional and required relationships
  • Many-to-Many with join entities (advanced scenarios)

We'll build on what we've learned in this chapter and explore all relationship types with practical examples.


إرسال تعليق

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.