Skip to main content

Entity Framework Core

Overview​

Entity Framework Core (EF Core) is a modern Object-Relational Mapper (ORM) for .NET. It enables developers to:

  • Work with databases using C# objects
  • Avoid writing most SQL manually
  • Perform CRUD operations
  • Manage database schema migrations
  • Use LINQ for querying
  • Support multiple database providers

Why EF Core?​

FeatureBenefit
LINQ SupportStrongly typed queries
MigrationsVersion-controlled database schema
Change TrackingAutomatic updates
Cross-platformWorks on Windows/Linux/macOS
Provider ModelSQL Server, PostgreSQL, SQLite, MySQL
PerformanceOptimized query execution
Async SupportScalable applications

EF Core Architecture​


Installing EF Core​

SQL Server Provider​

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

PostgreSQL Provider (Npgsql)​

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

Tools Package​

dotnet add package Microsoft.EntityFrameworkCore.Tools

DbContext​

DbContext is the central class in EF Core. Responsibilities:

  • Database connection
  • Query execution
  • Change tracking
  • Saving changes
  • Transaction handling
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}

public DbSet<User> Users { get; set; }
}

Entity Classes​

Entities represent database tables.

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}

Registering EF Core in ASP.NET Core​

SQL Server​

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("Default")));

PostgreSQL (Npgsql)​

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("Default")));

Connection Strings​

SQL Server​

{
"ConnectionStrings": {
"Default": "Server=.;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True"
}
}

PostgreSQL​

{
"ConnectionStrings": {
"Default": "Host=localhost;Database=ShopDb;Username=postgres;Password=secret"
}
}

Migrations​

Migrations manage schema changes.

dotnet ef migrations add InitialCreate
dotnet ef database update

Migration workflow:


CRUD Operations​

Create​

var user = new User
{
Name = "John",
Email = "john@example.com"
};

_context.Users.Add(user);
await _context.SaveChangesAsync();

Read​

var users = await _context.Users.ToListAsync();

Update​

var user = await _context.Users.FindAsync(1);
user.Name = "Updated";
await _context.SaveChangesAsync();

Delete​

var user = await _context.Users.FindAsync(1);
_context.Users.Remove(user);
await _context.SaveChangesAsync();

LINQ Queries​

Filtering​

var users = await _context.Users
.Where(x => x.Name.Contains("John"))
.ToListAsync();

Sorting​

var users = await _context.Users
.OrderBy(x => x.Name)
.ToListAsync();

Projection​

var users = await _context.Users
.Select(x => new { x.Id, x.Name })
.ToListAsync();

Relationships​

RelationshipExample
One-to-OneUser → Profile
One-to-ManyBlog → Posts
Many-to-ManyStudents ↔ Courses

One-to-Many Example​

public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; set; }
}

public class Post
{
public int Id { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}

Fluent API​

Used for advanced configuration.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasIndex(x => x.Email)
.IsUnique();
}

Data Annotations​

public class User
{
[Key]
public int Id { get; set; }

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

Change Tracking​

Tracking Query​

var users = await _context.Users.ToListAsync();

No Tracking Query (faster reads)​

var users = await _context.Users
.AsNoTracking()
.ToListAsync();

Eager Loading​

var blogs = await _context.Blogs
.Include(x => x.Posts)
.ToListAsync();

Lazy Loading​

public virtual ICollection<Post> Posts { get; set; }

Explicit Loading​

await _context.Entry(blog)
.Collection(x => x.Posts)
.LoadAsync();

Transactions​

using var transaction = await _context.Database.BeginTransactionAsync();

try
{
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
}

Concurrency Handling​

public class Product
{
public int Id { get; set; }

[Timestamp]
public byte[] RowVersion { get; set; }
}

Raw SQL Queries​

var users = await _context.Users
.FromSqlRaw("SELECT * FROM Users")
.ToListAsync();

Performance Optimization​

OptimizationBenefit
AsNoTrackingFaster reads
ProjectionSmaller payloads
PaginationReduced memory usage
Compiled QueriesFaster execution
Batch UpdatesFewer DB calls

Pagination​

var users = await _context.Users
.Skip(0)
.Take(10)
.ToListAsync();

Avoid N+1 Queries​

// Bad
foreach (var blog in blogs)
{
Console.WriteLine(blog.Posts.Count);
}

// Good
var blogs = await _context.Blogs
.Include(x => x.Posts)
.ToListAsync();

Indexing​

modelBuilder.Entity<User>()
.HasIndex(x => x.Email);

Security Best Practices​

PracticePurpose
Parameterized QueriesPrevent SQL Injection
ValidationProtect data integrity
Least PrivilegeLimit DB permissions
Secrets ManagerSecure connection strings

EF Core automatically parameterizes LINQ queries — always prefer LINQ over raw interpolated SQL:

// Safe
.Where(x => x.Name == name)

// UNSAFE — SQL injection risk
FromSqlRaw($"SELECT * FROM Users WHERE Name = '{name}'")

Repository Pattern​

public class UserRepository : IUserRepository
{
private readonly AppDbContext _context;

public UserRepository(AppDbContext context)
{
_context = context;
}

public async Task<List<User>> GetAllAsync()
{
return await _context.Users.ToListAsync();
}
}

Unit of Work Pattern​


Testing EF Core​

In-Memory Database (unit tests)​

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));

SQLite Testing (preferred for realistic integration tests)​

options.UseSqlite("DataSource=:memory:");

Advanced Topics​

TopicDescription
InterceptorsQuery interception
Compiled QueriesFaster repeated queries
Value ConvertersCustom mappings
Temporal TablesHistorical tracking
Global Query FiltersMulti-tenancy/soft delete
Shadow PropertiesHidden columns

Global Query Filters (Soft Delete)​

modelBuilder.Entity<User>()
.HasQueryFilter(x => !x.IsDeleted);

Logging SQL Queries​

builder.Services.AddDbContext<AppDbContext>(options =>
options
.UseNpgsql(connectionString)
.LogTo(Console.WriteLine));

Npgsql-Specific Features​

JSON Columns​

public class Order
{
public int Id { get; set; }
public JsonDocument Metadata { get; set; }
}

Array Columns​

public class Tag
{
public int Id { get; set; }
public string[] Labels { get; set; }
}
.Where(x => EF.Functions.ToTsVector("english", x.Content)
.Matches("dotnet"))

Database Providers​

ProviderPackage
SQL ServerMicrosoft.EntityFrameworkCore.SqlServer
PostgreSQLNpgsql.EntityFrameworkCore.PostgreSQL
SQLiteMicrosoft.EntityFrameworkCore.Sqlite
MySQLPomelo.EntityFrameworkCore.MySql
OracleOracle.EntityFrameworkCore

EF Core Cheat Sheet​

TaskCode
Add Entity_context.Add(entity)
Save ChangesSaveChangesAsync()
Query DataToListAsync()
Include RelationsInclude(x => x.Posts)
Disable TrackingAsNoTracking()
Add Migrationdotnet ef migrations add

Learning Roadmap​


Interview Questions​

Beginner​

  1. What is EF Core?
  2. What is DbContext?
  3. What is a DbSet?
  4. Difference between tracking and no tracking?
  5. What are migrations?

Intermediate​

  1. Explain eager vs lazy loading.
  2. What causes N+1 problems?
  3. How does change tracking work?
  4. Explain Fluent API.
  5. What is concurrency handling?

Advanced​

  1. How would you optimize EF Core performance?
  2. Explain query execution pipeline.
  3. What are compiled queries?
  4. How would you implement soft delete?
  5. Explain transaction handling in EF Core.