Skip to main content

.NET Code First

Introduction

Code First is a development approach in Entity Framework (EF) that allows developers to define their data models using C# classes first, then have Entity Framework generate the database schema based on those classes. This approach puts the code at the center of your development process rather than the database design.

As part of the .NET data access ecosystem, Code First gives developers more control over their domain models and creates a more natural development workflow, especially for those who prefer to think in terms of objects rather than database tables.

Understanding the Code First Workflow

The Code First approach follows these general steps:

  1. Create domain classes (POCOs - Plain Old CLR Objects)
  2. Configure the model using Fluent API or Data Annotations
  3. Create a DbContext class to represent your database
  4. Use Migrations to create and update the database schema
  5. Perform CRUD operations through the DbContext

Let's explore each step in detail.

Setting Up Your Environment

To get started with Code First in a .NET application, you'll need to install the required NuGet packages:

csharp
// Via Package Manager Console
Install-Package Microsoft.EntityFrameworkCore.SqlServer
Install-Package Microsoft.EntityFrameworkCore.Tools

// Via .NET CLI
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

Creating Domain Classes

Domain classes are simple C# classes that represent entities in your domain. These will become tables in your database.

csharp
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public DateTime PublicationDate { get; set; }
public decimal Price { get; set; }
public int PublisherId { get; set; }

// Navigation property
public Publisher Publisher { get; set; }
}

public class Publisher
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }

// Navigation property
public ICollection<Book> Books { get; set; }
}

Creating the DbContext

The DbContext is the main class that coordinates Entity Framework functionality for a specific data model. It represents a session with the database, allowing you to query and save data.

csharp
public class BookstoreContext : DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Publisher> Publishers { get; set; }

public BookstoreContext(DbContextOptions<BookstoreContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure your model using Fluent API
modelBuilder.Entity<Book>()
.Property(b => b.Title)
.IsRequired()
.HasMaxLength(200);

modelBuilder.Entity<Book>()
.Property(b => b.Price)
.HasPrecision(18, 2);

// Configure relationships
modelBuilder.Entity<Book>()
.HasOne(b => b.Publisher)
.WithMany(p => p.Books)
.HasForeignKey(b => b.PublisherId);
}
}

Configuring DbContext in the Application

In ASP.NET Core, you would register your DbContext in the Program.cs file:

csharp
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddDbContext<BookstoreContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Other service registrations...

And your connection string would be defined in appsettings.json:

json
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=Bookstore;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Using Data Annotations for Model Configuration

Instead of (or in addition to) the Fluent API, you can use Data Annotations attributes to configure your model:

csharp
public class Book
{
public int Id { get; set; }

[Required]
[MaxLength(200)]
public string Title { get; set; }

[Required]
public string Author { get; set; }

[DataType(DataType.Date)]
public DateTime PublicationDate { get; set; }

[Required]
[Precision(18, 2)]
public decimal Price { get; set; }

public int PublisherId { get; set; }

public Publisher Publisher { get; set; }
}

Working with Migrations

Migrations allow you to create and update your database schema based on changes to your model.

Creating Your First Migration

Once your model is defined, you can create your first migration:

bash
# Using Package Manager Console
Add-Migration InitialCreate

# Using .NET CLI
dotnet ef migrations add InitialCreate

This command creates a new migration file that contains the code necessary to create the database schema.

Applying Migrations

To apply the migration and create or update the database:

bash
# Using Package Manager Console
Update-Database

# Using .NET CLI
dotnet ef database update

Updating Your Model with New Migrations

When you change your model (add, modify, or remove entities or properties), create a new migration:

bash
# Using Package Manager Console
Add-Migration AddCategoryToBooks

# Using .NET CLI
dotnet ef migrations add AddCategoryToBooks

Then update the database as before.

Performing CRUD Operations

Once your database is created, you can perform CRUD operations through your DbContext:

Creating Records

csharp
using (var context = new BookstoreContext(options))
{
// Create a publisher
var publisher = new Publisher
{
Name = "Packt Publishing",
Address = "Birmingham, UK"
};

context.Publishers.Add(publisher);
context.SaveChanges(); // Save to get the generated ID

// Create books
var book1 = new Book
{
Title = "Entity Framework Core in Action",
Author = "Jon P Smith",
PublicationDate = new DateTime(2018, 8, 31),
Price = 44.99m,
PublisherId = publisher.Id
};

var book2 = new Book
{
Title = ".NET Core in Action",
Author = "Dustin Metzgar",
PublicationDate = new DateTime(2018, 3, 31),
Price = 39.99m,
PublisherId = publisher.Id
};

context.Books.AddRange(book1, book2);
context.SaveChanges();
}

Reading Records

csharp
using (var context = new BookstoreContext(options))
{
// Get all books
var allBooks = context.Books.ToList();

// Get a specific book
var book = context.Books.Find(1);

// Query with LINQ
var expensiveBooks = context.Books
.Where(b => b.Price > 40)
.OrderBy(b => b.Title)
.ToList();

// Include related data
var booksWithPublishers = context.Books
.Include(b => b.Publisher)
.ToList();
}

Updating Records

csharp
using (var context = new BookstoreContext(options))
{
var book = context.Books.Find(1);
if (book != null)
{
book.Price = 49.99m;
context.SaveChanges();
}
}

Deleting Records

csharp
using (var context = new BookstoreContext(options))
{
var book = context.Books.Find(1);
if (book != null)
{
context.Books.Remove(book);
context.SaveChanges();
}
}

Real-World Application: Building a Simple Bookstore API

Let's create a simple ASP.NET Core Web API controller for our bookstore:

csharp
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly BookstoreContext _context;

public BooksController(BookstoreContext context)
{
_context = context;
}

// GET: api/Books
[HttpGet]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
return await _context.Books
.Include(b => b.Publisher)
.ToListAsync();
}

// GET: api/Books/5
[HttpGet("{id}")]
public async Task<ActionResult<Book>> GetBook(int id)
{
var book = await _context.Books
.Include(b => b.Publisher)
.FirstOrDefaultAsync(b => b.Id == id);

if (book == null)
{
return NotFound();
}

return book;
}

// POST: api/Books
[HttpPost]
public async Task<ActionResult<Book>> PostBook(Book book)
{
_context.Books.Add(book);
await _context.SaveChangesAsync();

return CreatedAtAction(nameof(GetBook), new { id = book.Id }, book);
}

// PUT: api/Books/5
[HttpPut("{id}")]
public async Task<IActionResult> PutBook(int id, Book book)
{
if (id != book.Id)
{
return BadRequest();
}

_context.Entry(book).State = EntityState.Modified;

try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BookExists(id))
{
return NotFound();
}
else
{
throw;
}
}

return NoContent();
}

// DELETE: api/Books/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteBook(int id)
{
var book = await _context.Books.FindAsync(id);
if (book == null)
{
return NotFound();
}

_context.Books.Remove(book);
await _context.SaveChangesAsync();

return NoContent();
}

private bool BookExists(int id)
{
return _context.Books.Any(e => e.Id == id);
}
}

Advanced Code First Features

Seeding Data

You can seed your database with initial data by overriding the OnModelCreating method:

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Other configurations...

// Seed data
modelBuilder.Entity<Publisher>().HasData(
new Publisher { Id = 1, Name = "Packt Publishing", Address = "Birmingham, UK" },
new Publisher { Id = 2, Name = "O'Reilly Media", Address = "Sebastopol, CA, USA" }
);

modelBuilder.Entity<Book>().HasData(
new Book {
Id = 1,
Title = "Entity Framework Core in Action",
Author = "Jon P Smith",
PublicationDate = new DateTime(2018, 8, 31),
Price = 44.99m,
PublisherId = 1
},
new Book {
Id = 2,
Title = "Learning Entity Framework Core",
Author = "Jon P Smith",
PublicationDate = new DateTime(2018, 3, 21),
Price = 39.99m,
PublisherId = 1
}
);
}

Working with Value Conversions

Value Conversions allow you to transform data between model and database representations:

csharp
// Define an enum in your model
public enum BookCategory
{
Fiction,
NonFiction,
Biography,
Technical,
Children
}

public class Book
{
// Other properties...
public BookCategory Category { get; set; }
}

// Configure the value conversion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Other configurations...

modelBuilder.Entity<Book>()
.Property(b => b.Category)
.HasConversion<string>();
}

Configuring Indexes

You can create indexes to improve query performance:

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Other configurations...

modelBuilder.Entity<Book>()
.HasIndex(b => b.Title);

modelBuilder.Entity<Book>()
.HasIndex(b => new { b.Author, b.Title })
.IsUnique();
}

Summary

Entity Framework Core's Code First approach offers a powerful way to develop applications with a focus on your domain model rather than database details. By defining C# classes and configuring them with attributes or the Fluent API, you can generate and maintain a database schema that accurately reflects your application's needs.

The Code First workflow includes:

  • Creating domain classes
  • Configuring the model with Fluent API or Data Annotations
  • Creating a DbContext to interact with the database
  • Using Migrations to create and update the database schema
  • Performing CRUD operations through the DbContext

By mastering these concepts, you'll be able to build robust, data-driven .NET applications with clean, maintainable code.

Additional Resources

Exercises

  1. Create a new .NET application with a Code First model for a simple blog system with posts, comments, and categories.
  2. Add validation attributes to your model classes to enforce business rules.
  3. Implement a repository pattern on top of Entity Framework to abstract the data access logic.
  4. Create a Web API that uses your Code First model to provide CRUD operations for blog posts.
  5. Add pagination and filtering to your API to handle large datasets efficiently.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)