Welcome to your first hands-on chapter! In this chapter, we will build a working EF Core application from scratch. You'll create a console application, define your first entity, set up a DbContext, and save data to a real SQL Server database. By the end of this chapter, you'll have a functioning EF Core application that you can use as a foundation for all future chapters.
2.1 Project Setup
Let's start by creating a new .NET Console Application. Open your terminal, command prompt, or PowerShell.
Step 1: Create a new console application
dotnet new console -n EFCoreTutorial cd EFCoreTutorial
This creates a new folder called EFCoreTutorial with a basic console application inside.
Step 2: Open the project in Visual Studio or VS Code
If you're using Visual Studio, open the .csproj file or the folder. If you're using VS Code, simply open the folder.
Step 3: Explore the initial project structure
Your project should look like this:
EFCoreTutorial/ ├── Program.cs ├── EFCoreTutorial.csproj └── obj/
Program.cs contains the entry point of our application. We'll modify this file later.
2.2 Installing Required NuGet Packages
EF Core functionality comes via NuGet packages. We need three essential packages:
| Package Name | Purpose |
|---|---|
Microsoft.EntityFrameworkCore |
The core EF Core library. Required for every EF Core project. |
Microsoft.EntityFrameworkCore.SqlServer |
The SQL Server database provider. Translates LINQ to SQL Server-specific SQL. |
Microsoft.EntityFrameworkCore.Tools |
Enables EF Core commands in the Package Manager Console (for migrations, scaffolding, etc.). |
Install using .NET CLI:
dotnet add package Microsoft.EntityFrameworkCore dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Tools
Install using Package Manager Console (Visual Studio):
Install-Package Microsoft.EntityFrameworkCore Install-Package Microsoft.EntityFrameworkCore.SqlServer Install-Package Microsoft.EntityFrameworkCore.Tools
Verify installation:
After installation, open your .csproj file. You should see something like:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
</ItemGroup>
</Project>
2.3 Defining Your First Entity Class
In EF Core, an entity is a C# class that maps to a database table. Let's create a Book entity for our book store.
Step 1: Create a Models folder
It's good practice to organize your entities in a Models or Entities folder.
mkdir Models
Step 2: Create the Book class
Create a new file Book.cs inside the Models folder with the following content:
using System.ComponentModel.DataAnnotations;
namespace EFCoreTutorial.Models;
public class Book
{
// Primary Key - By convention, 'Id' or 'BookId' is automatically detected as the PK
public int Id { get; set; }
// [Required] attribute makes this column NOT NULL in the database
[Required]
// [MaxLength] sets the maximum length of the string column
[MaxLength(200)]
public string Title { get; set; }
// The '?' makes this property optional (nullable)
public string? Author { get; set; }
// [DataType] provides additional metadata for scaffolding
[DataType(DataType.Currency)]
public decimal Price { get; set; }
// Nullable DateTime means this column can be NULL in the database
public DateTime? PublishedOn { get; set; }
// We'll add more properties in future chapters
public string? ISBN { get; set; }
}
Understanding the Code:
| Code | Explanation |
|---|---|
public int Id { get; set; } |
By convention, a property named Id or BookId becomes the primary key. It will be an auto-incrementing identity column in SQL Server. |
[Required] |
This data annotation ensures the column is NOT NULL in the database. The property becomes required. |
[MaxLength(200)] |
Sets the maximum length of the string column to 200 characters. In SQL Server, this becomes nvarchar(200). |
string? Author |
The ? indicates this property is nullable. The corresponding database column will allow NULL values. |
[DataType(DataType.Currency)] |
This provides metadata for UI scaffolding. It doesn't affect the database schema directly. |
2.4 Creating the Database Context (DbContext)
The DbContext class is the most important class in EF Core. It represents a session with the database and provides APIs for querying and saving entities.
Step 1: Create a Data folder
mkdir Data
Step 2: Create the AppDbContext class
Create a new file AppDbContext.cs inside the Data folder:
using Microsoft.EntityFrameworkCore;
using EFCoreTutorial.Models;
namespace EFCoreTutorial.Data;
public class AppDbContext : DbContext
{
// DbSet represents a database table
// Querying Books gives you data from the Books table
public DbSet<Book> Books { get; set; }
// The OnConfiguring method is where we configure the database provider
// and connection string
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Define the connection string to SQL Server LocalDB
// This is for development only - in production, this comes from configuration
string connectionString = @"Server=(localdb)\mssqllocaldb;Database=EFCoreBookStore;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True;";
// Tell EF Core to use SQL Server with this connection string
optionsBuilder.UseSqlServer(connectionString);
// OPTIONAL: Log SQL queries to the console
// This is incredibly useful for debugging and learning
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
// OPTIONAL: Enable sensitive data logging (shows parameter values)
// Only use this in development, never in production!
// optionsBuilder.EnableSensitiveDataLogging();
}
}
Understanding the Connection String:
| Part | Meaning |
|---|---|
Server=(localdb)\mssqllocaldb |
Connects to SQL Server Express LocalDB, which installs with Visual Studio. It's perfect for development. |
Database=EFCoreBookStore |
The name of the database. EF Core will create it if it doesn't exist (when we call EnsureCreated or run migrations). |
Trusted_Connection=True |
Uses Windows authentication (your current Windows user). |
MultipleActiveResultSets=true |
Allows multiple queries on the same connection. Useful for scenarios with nested data readers. |
TrustServerCertificate=True |
Bypasses SSL certificate validation for local development. |
2.5 Putting It All Together - Program.cs
Now let's use our AppDbContext to add a book to the database. Replace the contents of Program.cs with:
using EFCoreTutorial.Data;
using EFCoreTutorial.Models;
// 'using' statement ensures the DbContext is properly disposed
// This closes the database connection when we're done
using var db = new AppDbContext();
Console.WriteLine("Ensuring database is created...");
// This creates the database if it doesn't exist
// In the next chapter, we'll replace this with Migrations
db.Database.EnsureCreated();
// Create a new book object
var newBook = new Book
{
Title = "Clean Code: A Handbook of Agile Software Craftsmanship",
Author = "Robert C. Martin",
Price = 45.99m,
PublishedOn = new DateTime(2008, 8, 1),
ISBN = "978-0132350884"
};
// Add the book to the DbSet
// This marks it for insertion but doesn't execute SQL yet
Console.WriteLine("Adding book to change tracker...");
db.Books.Add(newBook);
// SaveChanges() executes the INSERT SQL command
Console.WriteLine("Saving changes to database...");
int recordsWritten = db.SaveChanges();
Console.WriteLine($"Success! {recordsWritten} record(s) written to database.");
// Let's verify by reading the data back
Console.WriteLine("\nReading books from database...");
var books = db.Books.ToList();
foreach (var book in books)
{
Console.WriteLine($"ID: {book.Id}, Title: {book.Title}, Author: {book.Author}, Price: {book.Price:C}, Published: {book.PublishedOn:yyyy-MM-dd}, ISBN: {book.ISBN}");
}
Console.WriteLine("\nPress any key to exit...");
Console.ReadKey();
2.6 Running the Application
Step 1: Run the application
dotnet run
Expected Output:
Ensuring database is created...
Adding book to change tracker...
Saving changes to database...
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (27ms) [Parameters=[@p0='?' (DbType=String), @p1='?' (DbType=String), @p2='?' (DbType=Decimal), @p3='?' (DbType=DateTime2), @p4='?' (DbType=String)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Books] ([Author], [ISBN], [Price], [PublishedOn], [Title])
VALUES (@p0, @p1, @p2, @p3, @p4);
SELECT [Id]
FROM [Books]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
Success! 1 record(s) written to database.
Reading books from database...
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [b].[Id], [b].[Author], [b].[ISBN], [b].[Price], [b].[PublishedOn], [b].[Title]
FROM [Books] AS [b]
ID: 1, Title: Clean Code: A Handbook of Agile Software Craftsmanship, Author: Robert C. Martin, Price: $45.99, Published: 2008-08-01, ISBN: 978-0132350884
Press any key to exit...
What just happened?
- Database Creation:
EnsureCreated()checked if the database existed. Since it didn't, it created both the database and theBookstable based on yourBookentity. - Change Tracking: When you called
db.Books.Add(newBook), EF Core started tracking thenewBookentity and marked it asAdded. - SQL Generation: When you called
SaveChanges(), EF Core generated anINSERTstatement with parameters for each property. - Identity Value: After insertion, EF Core retrieved the auto-generated
Idvalue from the database and updated yournewBookobject. - Querying: The
db.Books.ToList()generated aSELECTstatement and materialized the results back intoBookobjects.
2.7 Verifying the Database
Let's verify that our data is actually in SQL Server.
Using SQL Server Management Studio (SSMS):
- Open SSMS
- Connect to:
(localdb)\mssqllocaldb - Expand Databases - you should see
EFCoreBookStore - Expand Tables - you should see
dbo.Books - Right-click
dbo.Booksand select "Select Top 1000 Rows"
You'll see your book data in the grid.
Using SQL Query:
You can also run this query:
SELECT * FROM Books
Result:
| Id | Title | Author | Price | PublishedOn | ISBN |
|---|---|---|---|---|---|
| 1 | Clean Code: A Handbook of Agile Software Craftsmanship | Robert C. Martin | 45.99 | 2008-08-01 | 978-0132350884 |
2.8 Understanding What EF Core Generated
Let's look at the actual database schema that EF Core created:
Table Structure:
CREATE TABLE [dbo].[Books] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Author] NVARCHAR (MAX) NULL,
[ISBN] NVARCHAR (MAX) NULL,
[Price] DECIMAL (18, 2) NOT NULL,
[PublishedOn] DATETIME2 (7) NULL,
[Title] NVARCHAR (200) NOT NULL,
CONSTRAINT [PK_Books] PRIMARY KEY CLUSTERED ([Id] ASC)
);
Key Observations:
| Our C# Code | Generated SQL | Explanation |
|---|---|---|
public int Id { get; set; } |
[Id] INT IDENTITY(1,1) NOT NULL |
Primary key with auto-increment (identity) |
[Required] on Title |
[Title] NVARCHAR(200) NOT NULL |
Column becomes NOT NULL |
[MaxLength(200)] on Title |
NVARCHAR(200) |
String length is limited to 200 |
string? Author |
[Author] NVARCHAR(MAX) NULL |
Nullable string becomes NULL column |
decimal Price |
[Price] DECIMAL(18,2) NOT NULL |
Default decimal precision (18,2) |
DateTime? PublishedOn |
[PublishedOn] DATETIME2(7) NULL |
DateTime2 is the modern DateTime type |
2.9 Adding More Books (Experiment)
Let's modify Program.cs to add multiple books and see change tracking in action:
using EFCoreTutorial.Data;
using EFCoreTutorial.Models;
using var db = new AppDbContext();
// Ensure database exists
db.Database.EnsureCreated();
// Create multiple books
var booksToAdd = new[]
{
new Book
{
Title = "The Pragmatic Programmer",
Author = "David Thomas",
Price = 49.99m,
PublishedOn = new DateTime(1999, 10, 20),
ISBN = "978-0201616224"
},
new Book
{
Title = "Code Complete",
Author = "Steve McConnell",
Price = 54.99m,
PublishedOn = new DateTime(2004, 6, 9),
ISBN = "978-0735619678"
},
new Book
{
Title = "Design Patterns",
Author = "Erich Gamma",
Price = 59.99m,
PublishedOn = new DateTime(1994, 10, 21),
ISBN = "978-0201633610"
}
};
Console.WriteLine($"Adding {booksToAdd.Length} books...");
db.Books.AddRange(booksToAdd);
int recordsWritten = db.SaveChanges();
Console.WriteLine($"Added {recordsWritten} books.");
// Read and display all books
Console.WriteLine("\nAll books in database:");
foreach (var book in db.Books.OrderBy(b => b.Title))
{
Console.WriteLine($"- {book.Title} by {book.Author} (${book.Price})");
}
Key Points:
AddRange()adds multiple entities at onceOrderBy()sorts the results- All SQL commands are batched into a single database round-trip
2.10 Common Issues and Solutions
Issue 1: Connection string errors
Error: Cannot connect to (localdb)\mssqllocaldb
Solution: Ensure LocalDB is installed and running:
sqllocaldb info sqllocaldb start mssqllocaldb
Issue 2: Package version mismatches
Error: System.TypeLoadException or missing methods
Solution: Ensure all EF Core packages are the same version:
dotnet add package Microsoft.EntityFrameworkCore --version 8.0.0 dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.0 dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.0
Issue 3: Database already exists
Scenario: You want to start fresh
Solution: Delete and recreate the database:
// Add this before EnsureCreated() db.Database.EnsureDeleted(); // Deletes the database if it exists db.Database.EnsureCreated(); // Creates a fresh database
Issue 4: Nullable reference types warnings
Warning: Non-nullable property 'Title' must contain a non-null value when exiting constructor
Solution: Either make it nullable (string?) or initialize it:
public string Title { get; set; } = string.Empty;
2.11 Best Practices Introduced
- Use folders to organize code: Models folder for entities, Data folder for DbContext.
-
Always dispose DbContext: Use
usingstatement orusing varto ensure connections are closed. -
Log SQL during development:
optionsBuilder.LogTo(Console.WriteLine)helps you understand what EF Core is doing. -
Use data annotations for simple validation:
[Required],[MaxLength]provide both validation and database schema hints. - Start with EnsureCreated for learning: But remember we'll replace it with Migrations in the next chapter.
2.12 Chapter Summary
| Concept | What We Learned |
|---|---|
| Project Setup | Create a console app and install EF Core NuGet packages (Core, SqlServer, Tools) |
| Entity Class | Plain C# class with properties. Id becomes primary key. Data annotations add database constraints. |
| DbContext | Main class that coordinates database operations. Contains DbSet properties for tables. |
| Connection String | Tells EF Core which database server and database to use. |
| EnsureCreated | Quick way to create database (development only, not for production). |
| Add and SaveChanges | Add tracks entities, SaveChanges executes SQL and returns number of affected rows. |
| Querying | Using LINQ on DbSet to retrieve data (ToList, OrderBy). |
What's Next?
In Chapter 3: Database Migrations, we will:
- Understand why
EnsureCreated()is not suitable for production - Learn what migrations are and why they're essential
- Install the
dotnet-efglobal tool - Create our first migration
- Apply migrations to update the database schema
- Add a new property to our Book entity and create a second migration
- Generate SQL scripts for production deployments
Migrations are what make EF Core truly professional - they give you version control for your database schema!