DEV Community

Cover image for The comprehensive guide to Entity Framework Core
Maurizio8788
Maurizio8788

Posted on

The comprehensive guide to Entity Framework Core

Hello everyone, in the previous article, we provided an overview of how to access data from our database through ADO.NET. Most of the time, we won't use ADO.NET in our applications; instead, we'll use an ORM (Object Relational Mapper), and in .NET Core, the most commonly used one is Entity Framework Core.

In this article, we won't be using the AdventureWorks2022 database we used previously. Instead, we will examine an example of a small TodoList. This choice will allow us to address topics like migrations and the Code First approach, which we will discuss in detail later.

Ecco il codice Markdown per creare la lista richiesta, con gli elementi principali in grassetto:

Table of Contents:

What is an ORM

An ORM (Object Relational Mapper) is a data access library that enables us to map each table in our databases to a corresponding class. It allows us to map each individual column and its corresponding data type, and it seeks to provide a more fluent way of handling data access through a global configuration. In the case of Entity Framework Core (EF Core), this configuration is represented by the DbContext, which we will delve into further later on.

Database First and Code First Approach

The EF Core team has provided us with two development approaches:

  • Database First: This approach starts with an existing database schema. It allows you to generate entity classes and a context based on the structure of the database. You work with the database schema as your starting point and generate code from it.

  • Code First: In contrast, the Code First approach begins with defining your entity classes and their relationships in code. From there, you can generate a database schema based on your code. This approach is particularly useful when you want to work primarily with code and let EF Core create and manage the database schema for you.

Database First

The Database First approach (or DB First) is used when we have an existing database schema (or decide to create the database schema first) and then create entity classes and the database context based on it manually or by using a process called scaffolding.

Scaffolding is a reverse engineering technique that allows us to create entity classes and a DbContext based on the schema of a database.

In EF Core, you can perform this operation by installing the NuGet package Microsoft.EntityFrameworkCore.Design in addition to the EF Core package of the database provider you are using.

Once these prerequisites are satisfied, you can run the Scaffold-DbContext command from the command line, providing it with the connection string of your database like this:


Scaffold-DbContext β€˜Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=TodoList’ Microsoft.EntityFrameworkCore.SqlServer

Enter fullscreen mode Exit fullscreen mode

Alternatively, by including it in our appsettings.json file, we can retrieve it using this approach.


Scaffold-DbContext 'Name=ConnectionStrings:TodoContext' Microsoft.EntityFrameworkCore.SqlServer

Enter fullscreen mode Exit fullscreen mode

Using these two simple commands, all the entities of our tables with their corresponding relationships will be created, and additionally, the DbContext class will be generated.

Code First

The Code First approach, on the other hand, allows us to create entity tables, their relationships, the mapping between various columns, and optionally, the generation of initial data using what are called "Migrations."

Migrations are a system for updating the database schema and keeping track of ongoing changes to it. In fact, after creating the first migration, you'll see a table named EFMigrationsHistory is created. This system allows us, in case of an error, to revert the changes.

To create a migration, you need to create the DbContext and the entities you wish to create (operations we will delve into later), install the NuGet package Microsoft.EntityFrameworkCore.Tools, and if you're using Visual Studio (recommended), you need to run the following command from the Package Manager Console:


Add-Migrations <nome della migration>

Enter fullscreen mode Exit fullscreen mode

This command will create a 'Migrations' folder within the project, containing the following files:

  • XXXXXXXXXXXXXX_.cs, which contains the instructions for applying the migration.
  • ModelSnapshot.cs, which creates a "snapshot" of the current model. It is used to identify changes made during the implementation of the next migration.

If you wish to run the migration and update the schema of your database, you should execute the following command:


Update-Database

Enter fullscreen mode Exit fullscreen mode

If you want to remove a migration, you can run the following command:


Remove-Migration

Enter fullscreen mode Exit fullscreen mode

Apart from the instructions just explained, there are two tools that allow us to perform these operations much more easily, which I recommend you explore:

Entity Framework Core Command-Line Tools - dotnet ef

Ef Core Power Tools - github repository

The Context Class

The DbContext class is the primary class that allows us to query our data, manage the database connection, and ensure that the mapping is correct.

To create a DbContext class in EF Core, it is sufficient to make one of our classes inherit from the DbContext class, like this:


public class TodosContext : DbContext 
{

}

Enter fullscreen mode Exit fullscreen mode

Of course, you can give your class any name you prefer. However, it's a common convention to name it after the database and add the "Context" suffix. This way, if you have multiple DbContext classes, you can easily distinguish them.

Another small step to follow is to use the default constructor of the DbContext class:


public class TodosContext : DbContext 
{
   public TodosContext(DbContextOptions<TodosContext> options) : base(options) {}
}

Enter fullscreen mode Exit fullscreen mode

Now that we've created our DbContext class, we can finally create our entities. In this case, let's simulate a simple TodoList application by creating our two entities: Todo and TodoItem:


// file Todo.cs
public class Todo {
   public Todo() {
      TodoItems = new HashSet<TodoItem>();
   }
   public int Id { get;set;}
   public string Name { get;set; }
   public ICollection<TodoItem> TodoItems { get;set; }
}

// file TodoItem.cs
public class TodoItem {
   public int Id { get;set;}
   public string Description { get;set; }
   public bool IsCompleted { get;set; }
}

Enter fullscreen mode Exit fullscreen mode

Once we have created our classes, we can add them to our DbContext as properties of type DbSet:


public class TodosContext : DbContext 
{
   public TodosContext(DbContextOptions<TodosContext> options) : base(options) {}

   public DbSet<Todo> Todos { get;set; }
   public DbSet<TodoItem> TodoItems { get;set; }
}

Enter fullscreen mode Exit fullscreen mode

The DbSet class in EF Core represents a specific table or view in the database within the database context. It enables CRUD operations, LINQ queries, and provides a change tracking mechanism to simplify data management within the database entities.

Column Mapping

The DbContext has many methods that you can override, and one of the most important ones I'd like to mention is the OnModelCreating method. It allows you to perform fluent mapping of your entities using the modelBuilder parameter:


public class TodosContext : DbContext 
{
   public TodosContext(DbContextOptions<TodosContext> options) : base(options) {}

   ...entities classes created

   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
    modelBuilder.Entity<Todo>( builder => {
        builder.HasKey(x => x.Id);

            builder.Property(t => t.Id)
                   .ValueGeneratedOnAdd();

        builder.Property(x => x.Name)
                   .HasColumnName("todo_name");

            builder.HasMany(x => x.TodoItems)
                   .WithOne(t => t.Todo)
                   .HasForeignKey(t => t.TodoId)
                   .OnDelete(DeleteBehavior.Cascade);
      });
   }
}

Enter fullscreen mode Exit fullscreen mode

This is what we call Fluent Mapping of entity classes. Through the builder, we inform our DbContext about properties, the model, names (if different from the property name in the entity class), whether a property is required, and most importantly, we can map relationships between various entities.

A widely used mapping strategy in EF Core involves Data Annotations, which are attributes used to customize the mapping of entity classes to database tables. Data Annotations allow you to define column attributes and relationships in detail. An example illustrates this practice:


[Table("TodosItems")]
class TodoItems {
   [Key]
   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public int Id { get;set; }
   [MaxLength(150)]
   [Required]
   [Column("item_description")]
   public string Description { get;set; }
   [Column("item_si_completed")]
   public bool IsCompleted { get; set; }
   public int TodoId { get; set; }
   [ForeignKey(nameof(Todo))]
   public Todo? Todo { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

In the example above, we use Data Annotations to specify the table name ("TodoItems"), declare the primary key (Id), define the maximum length and requirement of the Description attribute, and specify the column name IsCompleted in the database. This provides detailed control over the mapping between the entity class and the database table.

Queries with Entity Framework

Now that everything is correctly configured, we can finally think about how to execute our first query with EF Core.

First of all, we need to register the DbContext class with the .NET Core dependency injection system and define the database provider we are using. In this case, we are using Sqlite. You can install the NuGet package Microsoft.EntityFrameworkCore.Sqlite for this purpose:


// file Program.cs

builder.Services.AddDbContext<TodosContext>(options => {
  options.UseSqlite(builder.Configuration.GetConnectionString("TodoContext"));
});

Enter fullscreen mode Exit fullscreen mode

Let's add these instructions to the OnModelCreating method in our database context to have some sample data available immediately as an example:


modelBuilder.Entity<Todo>(builder =>
{
    // …other properties

    builder.HasData(
        new Todo
        {
            Id = 1,
            Name = "Project management",
        });
});


modelBuilder.Entity<TodoItem>(builder =>
{
    builder.HasData(
      new TodoItem { Id = 1, Description = "Create a Database Context", IsCompleted = false, TodoId = 1 },
      new TodoItem { Id = 2, Description = "Create Todo entity", IsCompleted = false, TodoId = 1 },
      new TodoItem { Id = 3, Description = "Create TodoItem entity", IsCompleted = false, TodoId = 1 }
    );
});

Enter fullscreen mode Exit fullscreen mode

Retrieve Entities

Finally, let's focus on making our first query:


[Route("api/[controller]")]
[ApiController]
public class TodosController : ControllerBase
{
    private readonly TodosContext _context;

    public TodosController(TodosContext context)
    {
        _context = context;
    }

    [HttpGet]
    [Route(nameof(GetAllTodos))]
    public async Task<ActionResult<List<Todo>>> GetAllTodos()
    {
        try
        {
            List<Todo> todos = await 
               _context.Todos.AsNoTracking().ToListAsync();
            return Ok(todos);
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

As you can see, we've injected the context class into the controller's constructor and then used it in our endpoint to query the database with LINQ and retrieve the data created earlier:

Get All Query

Operators like ToList, ToListAsync, ToArray, ToDictionary, and similar methods are extension methods in EF Core used to actually execute LINQ queries and materialize the resulting data from the database. These methods are essential because they allow deferring the execution of queries until the actual results are requested. Additionally, they convert the results into convenient data structures such as lists, dictionaries, or arrays that can be easily used in the application. This approach provides better control over when queries are executed and yields optimized results for further processing.

Another important EF Core extension method that requires explanation is AsNoTracking(). This method is used to inform the framework not to track entities retrieved from the database. This choice significantly improves performance when fetching data from a query.

When using AsNoTracking(), the database context will not maintain an internal state of the retrieved entities. This means there will be no tracking of changes made to these entities, making the data retrieval process faster and more efficient.

However, it's important to note that when using AsNoTracking(), you won't have the ability to make direct changes to the retrieved entities and save them to the database without additional steps. Therefore, it's crucial to use this method carefully, reserving it for situations where you only need to retrieve data and not modify entities.

If, on the other hand, we want to retrieve the Todos along with their respective TodoItems, we should:


[HttpGet]
[Route(nameof(GetAllTodoWithItems))]
public async Task<ActionResult<List<Todo>>> GetAllTodoWithItems()
{
    try
    {
        List<Todo> todos = await _context.Todos.Include(x => x.TodoItems )
                                               .AsNoTracking()
                                               .ToListAsync();
        return Ok(todos);
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

Enter fullscreen mode Exit fullscreen mode

That Include statement will perform a left join on the TodoItems table and retrieve, of course if they exist, all the TodoItems for each Todo item.

Todos with items

Add a New Entity

Inserting a new entity into our database is very simple:


[HttpPost]
[Route(nameof(CreateTodo))]
public async Task<ActionResult<Todo>> CreateTodo([FromBody] Todo todo)
{
   try
   {
      _context.Todos.Add(todo);
      await _context.SaveChangesAsync();

      return CreatedAtRoute(nameof(GetTodo), new { todoId = todo.Id }, todo);
    }
    catch (Exception ex)
    {
       throw ex;
    }
}

Enter fullscreen mode Exit fullscreen mode

It's important to note that the simple Add operation won't immediately insert our new entity into the database. Instead, it adds the entity to the ChangeTracker of the database context, setting it to an Add state. The actual insertion into the database will occur only when we call the SaveChangesAsync() method. This operation is crucial for confirming and making the changes permanent in the database. So, remember that Add is just the first step, and SaveChangesAsync() is what performs the final database insertion action.

Update an Entity

To update an entity in our database, we can retrieve our TodoItem by its ID and then make changes to the object. It's important to ensure that you save the changes to the database using SaveChanges or SaveChangesAsync after making the modifications to the object:


[HttpPut("{todoItemId:int}")]
public async Task<ActionResult> UpdateTodoItem(
    int todoItemId, [FromBody] TodoItem todoItemUpdated) {
  ArgumentNullException.ThrowIfNull(todoItemId, nameof(todoItemId));
  try {
    TodoItem? todoItemFromDatabase =
        await _context.TodoItems.FirstOrDefaultAsync(x => x.Id == todoItemId);
    if (todoItemFromDatabase == null)
      return NotFound();

    todoItemFromDatabase.IsCompleted = todoItemUpdated.IsCompleted;
    todoItemFromDatabase.Description = todoItemUpdated.Description;

    _context.TodoItems.Update(todoItemFromDatabase);
    await _context.SaveChangesAsync();

    return NoContent();
  } catch (Exception ex) {
    throw ex;
  }
}

Enter fullscreen mode Exit fullscreen mode

In this controller, we retrieve the existing TodoItem object from the database based on the provided ID. This is done using await _context.TodoItems.FirstOrDefaultAsync(x => x.Id == todoItemId);. Subsequently, the properties of the todoItemFromDatabase object are updated with the new data provided. Finally, the todoItemFromDatabase object is marked as modified using _context.TodoItems.Update(todoItemFromDatabase);, and the changes are applied to the database with await _context.SaveChangesAsync();.

Delete an Entity

To delete a Todo item from the database, we can use the .Remove() operator. EF Core will automatically handle the deletion of associated TodoItem properties belonging to this entity based on the relationship configuration. This simplifies the management of cascading deletions when configured correctly:


[HttpDelete("{todoId}")]
public async Task<ActionResult> Delete(int todoId) {
  try {
    Todo? todo = await _context.Todos.FirstOrDefaultAsync(y => y.Id == todoId);
    if (todo is null)
      return NotFound();

    _context.Todos.Remove(todo);
    await _context.SaveChangesAsync();

    return NoContent();
  } catch (Exception ex) {
    throw;
  }
}

Enter fullscreen mode Exit fullscreen mode

If we now make a call to the endpoint GetAllTodoWithItems we will see that the entity and its respective items have been deleted from our database.

Once you have understood how to set up the basic configuration and performed the main CRUD operations, you can confidently say that you have covered the fundamental knowledge needed to start working with EF Core. You will find all the code used in this article and the previous one available in this GitHub repository:

EF Core TodoList Application

This repository contains a simple TodoList application built using Entity Framework Core (EF Core). In this application, we explore the fundamental concepts of setting up and using EF Core to interact with a database.

Getting Started

Prerequisites

Before you start, make sure you have the following installed:

  • .NET Core SDK
  • Visual Studio or your preferred code editor
  • SQLite for database development

Installation

  1. Clone this repository to your local machine.
  2. Open the solution in Visual Studio or your code editor of choice.
  3. Build the solution to restore dependencies.

Usage

  1. Set up your database connection in the appsettings.json file.
  2. Create the database schema using EF Core migrations
    dotnet ef database update
    Enter fullscreen mode Exit fullscreen mode
  3. Run the application to start managing your TodoList.

Key Concepts Covered

  • Database First and Code First approaches
  • Fluent Mapping of Entity Classes
  • Performing CRUD operations with EF Core
  • Using Data Annotations for configuration
  • Handling Relationships in EF…

Link to the repo

I hope you've enjoyed this introduction to data processing in .NET, and I hope it proves to be helpful in your studies. If you liked the article, please give it a thumbs up, and I hope you're willing to leave a comment to share knowledge and exchange opinions.

Happy Coding!

Top comments (2)

Collapse
 
rasheedmozaffar profile image
Rasheed K Mozaffar

Awesome guide πŸ™Œ
Thanks for sharing πŸ™

Collapse
 
maurizio8788 profile image
Maurizio8788

I am happy with your judgement!! ☺️