After the Dapper introduction, it is time to do the same for Entity Framework. I won't repeat the same stuff about ORMs, so check out my previous post if you want to remind yourself about them.

Entity Framework was first introduced by Microsoft in 2008 as part of .NET Framework 3.5. It was designed to be an Object-Relational Mapper (ORM) that abstracts the underlying database and allows developers to interact with data using .NET objects. This was a significant shift from traditional ADO.NET approaches, aiming to reduce the amount of boilerplate code required for data access.

Our Black Friday sale is now LIVE on Dometrain. Until the 2nd of December you can use code BLACKFRIDAY24 to get 40% off any course, already discounted bundle or your first year of Dometrain Pro, giving you access to every course on Dometrain.

blackfriday.jpg

The Transition to EF Core

With the release of .NET Core, Microsoft introduced Entity Framework Core (EF Core) as a complete rewrite of Entity Framework. While the initial versions lacked some features compared to EF6, subsequent releases not only have closed the gap significantly but also improved and expanded it a lot.

Evolution of EF Core Versions

  • EF Core 2.x: Brought back many missing features and improved stability.
  • EF Core 3.x: Focused on LINQ translation and breaking changes to improve correctness.
  • EF Core 5 and 6: Continued performance enhancements and introduced new features like many-to-many relationships without joining entities.
  • EF Core 7: Added features like JSON column support, bulk update/delete and performance optimizations.
  • EF Core 8: Added Raw SQL queries for unmapped types.
  • EF Core 9: Support for more LINQ scenarios and, again, better performance.

Historically, Entity Framework has been criticized for its performance overhead compared to micro-ORMs like Dapper or hand-written ADO.NET code. This overhead is often due to features like change tracking, complex LINQ query translation, and abstraction layers that provide convenience at the cost of raw speed.

Examples

Enough history, time to learn how this thing works!

To start, we need the MSSQL server installed, and then we need to create a console application.
Now, you need to install these packages:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Now, let's create a model to mess around with our DB:

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

    public required string Name { get; set; }

    public decimal Price { get; set; }
}

Also, a DbContext:

public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=.;Database=ProductDB;Trusted_Connection=True;TrustServerCertificate=True");
    }
}

Now, we're going to explore how we can create our database from our code. In the Code-First approach, you focus on the domain of your application and create the classes rather than design your database first. I have a diagram that showcases the whole logic of Code-first:

EFCodeFirst.png

In our case, we have reached the step where we need to create our migrations and create the database. To do that, run the following commands:

dotnet ef migrations add InitialCreate
dotnet ef database update

Congratulations, we now have a database in our hands that we can work with.

Since this is a getting-started post, first, we are going to explore some basic functionality of EF Core. Let's do a CRUD operation on a product:

using var db = new AppDbContext();

// Create
Console.WriteLine("Inserting a new product");
db.Add(new Product { Name = "Apple", Price = 0.50m });
await db.SaveChangesAsync();

// Read
Console.WriteLine("Querying for a product");
var product = await db.Products
    .FirstOrDefaultAsync(p => p.Name == "Apple");

Console.WriteLine($"Found product: {product?.Name} - ${product?.Price}");

// Update
Console.WriteLine("Updating the product");
product.Price = 0.75m;
await db.SaveChangesAsync();

// Delete
Console.WriteLine("Deleting the product");
db.Remove(product);
await db.SaveChangesAsync();

This example demonstrates basic CRUD (Create, Read, Update, Delete) operations using Entity Framework Core with asynchronous methods. First, we insert the apple, then we read it and update it, and last, we delete it.

Now that we have an idea, we need to explain how the magic of EF works under the hood.

Change Tracker in EF Core

The change tracker in Entity Framework Core is a crucial component that keeps track of all modifications made to instances of your entity classes. When you retrieve entities from the database, EF Core attaches them to a DbContext, starting to monitor their state. As you make changes—such as adding new entities, updating existing ones, or deleting them—the change tracker records these operations. It maintains information about the original and current values of the properties, the state of each entity (Added, Modified, Deleted, Unchanged), and the relationships between entities. When you call the SaveChangesAsync() method, the change tracker uses this information to generate and execute the appropriate SQL commands to update the database accordingly. This automated tracking simplifies data manipulation by abstracting the underlying database interactions, allowing us to focus on business logic without manually handling SQL statements.

To showcase how the change tracker works in Entity Framework Core using the Product example from earlier, we'll perform a series of operations and inspect the state of entities as tracked by EF Core:

using var db = new AppDbContext();

// Create a new product instance
var newProduct = new Product { Name = "Banana", Price = 0.30m };

// Add the product to the context
db.Products.Add(newProduct);

// Inspect the change tracker entries before saving
Console.WriteLine("Before SaveChanges:");
foreach (var entry in db.ChangeTracker.Entries())
{
    Console.WriteLine($"Entity: {entry.Entity.GetType().Name}, State: {entry.State}");
}

// Save changes to the database
await db.SaveChangesAsync();

// Inspect the change tracker entries after saving
Console.WriteLine("\nAfter SaveChanges:");
foreach (var entry in db.ChangeTracker.Entries())
{
    Console.WriteLine($"Entity: {entry.Entity.GetType().Name}, State: {entry.State}");
}

// Modify the product
newProduct.Price = 0.35m;

// Inspect the change tracker after modification
Console.WriteLine("\nAfter Modification:");
foreach (var entry in db.ChangeTracker.Entries())
{
    Console.WriteLine($"Entity: {entry.Entity.GetType().Name}, State: {entry.State}");
}

// Save the updated product
await db.SaveChangesAsync();

// Delete the product
db.Products.Remove(newProduct);

// Inspect the change tracker after deletion
Console.WriteLine("\nAfter Deletion:");
foreach (var entry in db.ChangeTracker.Entries())
{
    Console.WriteLine($"Entity: {entry.Entity.GetType().Name}, State: {entry.State}");
}

// Save the deletion
await db.SaveChangesAsync();

Let's break down this example:

  • Adding a New Product: We create a new Product instance and add it to the context using db.Products.Add(newProduct). The change tracker now marks this entity as Added.

  • Inspecting Before SaveChanges(): Before calling SaveChanges(), we iterate over db.ChangeTracker.Entries() to see the state of each tracked entity. The output will show that the Product entity is in the Added state.

  • After SaveChanges(): Once we save changes, the state of the entity changes to Unchanged because it's now synchronized with the database.

  • Modifying the Product: We change the Price of the product. The change tracker detects this modification and marks the entity's state as Modified.

  • Deleting the Product: When we remove the product using db.Products.Remove(newProduct), the change tracker marks it as Deleted.

The states of the tracker are the following:

  1. Added: The entity is new and will be inserted into the database.
  2. Unchanged: The entity hasn't been modified since it was loaded.
  3. Modified: Some or all properties of the entity have been changed.
  4. Deleted: The entity has been marked for deletion.
  5. Detached: The entity is not being tracked by the context.

By accessing db.ChangeTracker.Entries(), we can inspect all entities being tracked and their states. This is useful for debugging and understanding how EF Core is preparing to interact with the database.

Conclusion

In this post, we've explored the basics of setting up EF Core, performing CRUD operations, and understanding how the change tracker manages entity states behind the scenes. Now I want to hear from you. Do you want another post for EF Core to cover more advanced topics? Are you already using it? I hope you enjoyed it, and as always, keep coding.

Next steps

If you're interested in taking your knowledge of Entity Framework Core in .NET a step further, you can always check our "From Zero to Hero: Entity Framework Core in .NET" course on Dometrain.

cover.jpg