A Comprehensive Guide to Entity Framework Core in .NET 8

Overview

The Entity Framework Core (EF Core) has emerged as a cornerstone tool for data access in .NET development, offering developers a robust and versatile solution. With the advancements introduced in .NET 8 and C# 10, developers now have access to a richer set of features and enhancements, which further enhances the capabilities of EF Core.

We will explore Entity Framework Core in-depth in this comprehensive guide, ranging from fundamental concepts to advanced techniques. We dive into the intricacies of EF Core, leveraging the latest capabilities of C# 10 and .NET 8 to equip developers with the knowledge and skills needed to use EF Core effectively.

Throughout this guide, we aim to equip developers with a deep understanding of Entity Framework Core, enabling them to build efficient, scalable, and maintainable data-driven applications in the .NET ecosystem. This guide will help you unlock the full potential of Entity Framework Core in our .NET projects, regardless of whether you're a newbie or an experienced user.

Getting Started with Entity Framework Core

We developers who wish to build data-driven applications in .NET must get started with Entity Framework Core (EF Core). A lightweight, extensible, cross-platform ORM (Object-Relational Mapper) framework that simplifies data access and manipulation, EF Core offers numerous benefits to developers.

It abstracts away the complexities of database interaction, allowing developers to work with entities and relationships using familiar C# syntax, one of EF Core's primary benefits. By providing a high-level abstraction over the underlying database, EF Core streamlines development and reduces the amount of boilerplate code required for data access.

Once EF Core is installed in a .NET 8 project, developers can use the EF Core API to define database contexts, entities, and relationships. Once installed, developers can use the EF Core API to configure relationships. In EF Core, the DbContext class represents a database session and provides access to DbSet properties that enable interactions with entities.

// Example: Creating a DemoDbContext class the file is kept in DbContext Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data

using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Data;
public class DemoDbContext: DbContext
{
    public DemoDbContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<User> Users { get; set; }
    // Other DbSet properties for additional entities

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("OurConnectionString");
    }

}

The MyDbContext class inherits from DbContext and defines a DbSet property for the User entity. The OnConfiguring method configures EF Core to use SQL Server as the database provider and specifies the connection string.

With EF Core, developers can perform CRUD operations (Create, Read, Update, Delete) once the database context is set up. In CRUD operations, entity objects are manipulated and methods such as Add, Find, Update, and Remove are called on the DbSet properties of the DbContext.

As a result, getting started with Entity Framework Core requires setting it up in a .NET project, defining database contexts, entities, and relationships, and using EF Core API to perform CRUD operations. Developers can build data-driven applications more efficiently and effectively in .NET using EF Core.

Querying Data with LINQ

In Entity Framework Core (EF Core), using LINQ (Language Integrated Query) allows you to retrieve data and manipulate it using C# syntax from a database. To interact with the database efficiently and effectively, developers can write expressive and readable LINQ queries to filter, sort, and project data.

We Developers must understand the basics of LINQ and how it integrates with EF Core before they can query data using LINQ in EF Core. C#'s LINQ language extensions allow developers to query data from a variety of data sources with a unified syntax. With EF Core, developers can work with entity data intuitively and familiarly by translating LINQ queries to SQL queries that are executed against underlying databases.

Working with LINQ in EF Core requires understanding the concept of deferred execution. In EF Core, LINQ queries are executed lazily, meaning they are not executed immediately when defined, but rather when the query results are accessed or enumerated. By deferring execution, developers can build complex queries and apply additional operations, such as filtering or sorting, before they execute them against the database.

IQueryable represents a query that can be further modified or composed before execution, another key feature of LINQ in EF Core. With IQueryable, developers can dynamically build queries based on runtime conditions or user input, allowing them to filter, sort, and project data dynamically.

// Example: Creating a UserRepository class the file is kept in Repository Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Interfaces;
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Data.Repository;
public class UserRepository : IUserRepository
{
    private readonly DemoDbContext _dbContext;
    private bool _disposed = false;

    public UserRepository(DemoDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    /// <summary>
    /// This is an Example Querying data with LINQ
    /// </summary>
    /// <returns>List</returns>
    public Task<List<User>> GetAllActiveUsersAsync() =>
      _dbContext.Users
          .Where(u => u.IsActive)
          .OrderBy(u => u.LastName)
          .ThenBy(u => u.FirstName)
          .ToListAsync();


    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            _dbContext.Dispose();
        }

        _disposed = true;
    }


    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

In this code example above, the LINQ query filters users based on their age, orders them by last name, and selects only the first and last names of the users. LINQ operators can be chained together to build complex queries that are executed efficiently against the database using the IQueryable interface.

In short, querying data with LINQ in EF Core is a flexible and powerful way to interact with the database. Developers can write expressive and efficient queries that meet the needs of their applications by understanding LINQ syntax, deferred execution, and the IQueryable interface.

Working with Migrations

For managing database schema changes and ensuring consistency and integrity, working with migrations in Entity Framework Core (EF Core) is crucial. Developers can use migrations to evolve the database schema over time while preserving existing data and ensuring a smooth transition between versions.

 The basics of migrations and how they work are required for developers to get started with EF Core migrations. By using migrations, developers can define and apply changes to the database schema using C# code rather than altering the database manually. The advantage of this approach is that database schema changes can be controlled, database updates can be repeated, and deployment pipelines can be automated.

With Entity Framework Core, the process of generating and applying migrations is straightforward. Developers can generate migrations based on changes to the entity model and apply them to the database using the CLI (Command-Line Interface). Developers can use the following command to create a new migration named "InitialCreate":

dotnet ef migrations add InitialCreate

Once the migration is generated, developers can apply it to the database using the following command:

dotnet ef database update

The built-in tooling provided by EF Core can also be used to manage migrations in Visual Studio 2022 IDE. By right-clicking on the project containing the DbContext and choosing "Add > New Scaffolded Item," developers can choose "EF Core > Migrations" and follow the prompts to create a new migration. Similarly, developers can apply migrations by right-clicking on the project and selecting "Update Database."

Additionally, EF Core migrations support data migrations and seeding initial data in addition to managing schema changes. Developers can use data migrations to populate or transform data during the migration process, which ensures that the database remains consistent after schema changes. In migration code, developers can execute SQL commands or manipulate data by using the migrationBuilder object. In the OnModelCreating method of the DbContext, seeding initial data involves inserting predefined data into the database when the model is created or updated.

As a result, managing database schema changes, applying updates, and ensuring data consistency is essential when working with migrations in EF Core. Developers can effectively manage database schema changes and maintain the integrity of their applications' data by understanding the basics of migrations and using the tools provided by Visual Studio 2022 IDE and EF Core.

Advanced Concepts and Techniques

A developer can optimize performance, implement complex functionality, and optimize the efficiency of their applications by understanding advanced concepts and techniques in Entity Framework Core (EF Core). Developers can build high-performance, scalable applications by mastering these techniques and maximizing EF Core's potential.

EF Core offers three loading strategies: eager loading, lazy loading, and explicit loading. Developers can use eager loading to load related entities along with the main entity in a single query, reducing the number of database roundtrips. Data is only loaded when needed by lazy loading, which defers loading related entities until they are accessed. Developers can specify when related entities are loaded explicitly, allowing them to retrieve data more efficiently.

// Example: Creating a UserRepository class the file is kept in Models Folder in Project EntityFrameworkCoreGuideNET8.Domain
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
public record Order
{
    [Key]
    public Guid Id { get; init; }

    public Guid UserId { get; init; }

    [ForeignKey("UserId")]
    public required User User { get; init; }

    [Required]
    public string ProductName { get; init; }=string.Empty;

    [Required,Range(0, double.MaxValue, ErrorMessage = "Price must be a positive value.")]
    public decimal Price { get; init; }

    [Required]
    public DateTime OrderDate { get; init; }
        
}
// Example: Creating a UserRepository class the file is kept in Repository Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Interfaces;
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Data.Repository;
public class UserRepository : IUserRepository
{
    private readonly DemoDbContext _dbContext;
    private bool _disposed = false;

    public UserRepository(DemoDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

    /// <summary>
    /// This is an Example Querying data with LINQ
    /// </summary>
    /// <returns>List</returns>
    public Task<List<User>> GetAllActiveUsersAsync() =>
      _dbContext.Users
          .Where(u => u.IsActive)
          .OrderBy(u => u.LastName)
          .ThenBy(u => u.FirstName)
          .ToListAsync();


    /// <summary>
    /// This example is  Eager loading in EF Core
    /// </summary>
    /// <returns></returns>
    public Task<List<User>> LoadUsersWithOrdersAsync() =>
     _dbContext.Users
        .Include(u=>u.Orders)
         .Where(u => u.IsActive)
         .OrderBy(u => u.LastName)
         .ThenBy(u => u.FirstName)
         .ToListAsync();

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            _dbContext.Dispose();
        }

        _disposed = true;
    }


    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

To maintain data consistency and integrity in EF Core applications, transactions are crucial. By combining multiple database operations into a single, atomic unit of work, developers can ensure that either all operations succeed, or none are applied. In addition to preventing data corruption, this ensures that the database remains consistent.

EF Core offers developers flexibility and performance optimization opportunities when working with stored procedures and raw SQL queries. By encapsulating complex logic and business rules in stored procedures, developers improve performance and security while improving performance. For greater control and performance optimization, developers can execute SQL commands directly against the database using raw SQL queries, bypassing EF Core's query translation mechanism.

The efficiency of EF Core applications can be significantly improved by optimizing performance with caching and batching. Developers can cache frequently accessed data in memory, reducing the number of database queries they need to make and improving application responsiveness. By combining multiple database operations into a single batch, batching minimizes roundtrips and improves overall performance by minimizing latency.

We Developers can build high-performance, scalable applications by mastering advanced concepts and techniques in EF Core, such as loading strategies, transactions, stored procedures, raw SQL queries, caching, and batching. With the help of these techniques, developers can optimize performance, implement complex functionality, and deliver exceptional user experiences for their EF Core applications.

Integrating Entity Framework Core with ASP.NET Core

Web Developers can build scalable and robust web applications using ASP.NET Core and Entity Framework Core (EF Core). Developers can ensure the security, performance, and maintainability of their APIs by implementing best practices and design patterns based on the powerful capabilities of ASP.NET Core and EF Core.

Implementing the repository pattern and unit of work is an essential component of building RESTful APIs. The repository pattern separates the application's business logic from the data access logic. Developers can achieve better separation of concerns and improved testability of their code by encapsulating data access operations in repositories and coordinating transactions in units of work.

Building APIs with EF Core requires handling concurrency and optimistic locking. Multiple users can attempt to modify the same data at the same time, causing concurrency issues. Developers can detect and resolve conflicts gracefully by implementing optimistic locking mechanisms, such as row versioning or timestamp columns, ensuring data integrity and consistency in multi-user environments.

For sensitive information to be protected and compliance with security requirements, authorization and authentication are paramount. ASP.NET Core provides robust authentication and authorization mechanisms, including JWT (JSON Web Tokens), OAuth, and OpenID Connect. A developer can control access to API endpoints based on user roles, permissions, and other criteria by configuring authentication and authorization policies.

// Example: is using Program.cs  file is kept in  Project EntityFrameworkCoreGuideNET8.Business.UI, which is ASP.net Core MVC Project
using EntityFrameworkCoreGuideNET8.Business.UI.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();

// Example: Configuring authentication and authorization in ASP.NET Core
builder.Services.AddJwtAuthentication(builder.Configuration);

builder.Services.AddAdminAuthorization();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();
// Example: Creating a JwtAuthentication.cs class the file is kept in Extensions Folder in Project EntityFrameworkCoreGuideNET8.Business.UI

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace EntityFrameworkCoreGuideNET8.Business.UI.Extensions;
public static  class JwtAuthentication
{
    public static void AddJwtAuthentication(this IServiceCollection services, IConfiguration configuration)
    {
        // Example: Configuring authentication and authorization in ASP.NET Core
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = configuration["Jwt:Issuer"],
                    ValidAudience = configuration["Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]))
                };
            });


    }
}
// Example: Creating a Authorization.cs class the file is kept in Extensions Folder in Project EntityFrameworkCoreGuideNET8.Business.UI
namespace EntityFrameworkCoreGuideNET8.Business.UI.Extensions;
public static class Authorization
{
    public static void AddAdminAuthorization(this IServiceCollection services)
    {
        services.AddAuthorization(options =>
        {
            options.AddPolicy("AdminOnly", policy =>
            {
                policy.RequireRole("Admin");
            });
        });
    }
}

With ASP.NET Core and EF Core, it is necessary to implement best practices such as the repository pattern and unit of work, handle concurrency and optimistic locking, and secure data access through authentication and authorization. These best practices allow developers to build APIs that are scalable, secure, and maintainable.

C# 10 Features for Entity Framework Core

Utilizing the new features introduced in C# 10 can greatly enhance our experience with Entity Framework Core (EF Core) development. By leveraging these features, developers can streamline their code, improve readability, and enhance productivity.

The use of record types in C# 10 is one of the most notable features. Record types provide a concise and expressive way of defining immutable data types, making them ideal for EF Core applications that require entity representation. As a result of using record types for entity classes, developers can reduce boilerplate code and improve code clarity.

// Example: Using record types for entity classes
public record User
{
    public int Id { get; init; }
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

Additionally, C# 10 introduces a number of pattern-matching enhancements that can be beneficial for EF Core development. Developers can simplify complex code and improve maintainability by using pattern-matching enhancements. By using pattern-matching enhancements, developers can write more expressive and concise code for handling conditional logic.

In C# 10, nullable reference types were introduced. Nullable reference types allow developers to annotate their code to indicate where null values are allowed or disallowed. Developers can reduce the likelihood of runtime errors in EF Core applications by using nullable reference types, which make their code more robust and prevent null reference exceptions.

Web Developers can improve their experience with Entity Framework Core development, resulting in more efficient, robust, and maintainable codebases, by utilizing the latest features introduced in C# 10, such as record types, pattern-matching enhancements, and nullable reference types. With these features and EF Core, developers can easily build high-quality, scalable applications.

Entity Framework Core Performance Optimization

Entity Framework Core (EF Core) applications must understand performance optimization techniques to ensure efficient database operations. By optimizing database operations, developers can significantly enhance the performance and responsiveness of their applications, resulting in a better user experience and increased scalability.

To optimize query execution, database indexes help provide a quick way to locate specific rows in a database table, which speeds up query execution. By creating indexes on frequently queried columns, developers can reduce the time it takes to retrieve data from the database, resulting in faster query performance.

The performance of queries can also be significantly improved by optimizing them. By carefully crafting queries, developers can minimize the amount of data retrieved from the database and maximize the use of indexes. Several techniques can be used to improve query performance, including avoiding unnecessary joins, selecting only the necessary columns, and filtering data with WHERE clauses.

By caching frequently accessed data in memory, developers can reduce the number of database queries and improve application responsiveness, which can also be essential to performance optimization. The use of in-memory caching frameworks like Redis or the implementation of distributed caching solutions can help minimize database load and improve application performance.

To effectively optimize performance, developers must also monitor and analyse performance metrics to identify bottlenecks and areas for improvement. Developers can pinpoint performance issues and take corrective action with tools like Entity Framework Profiler or SQL Server Management Studio that provide insights into query execution times, database load, and resource utilization.

// Example: Creating a DemoDbContext class the file is kept in DbContext Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data

using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Data;
public class DemoDbContext: DbContext
{
    
    public DemoDbContext(DbContextOptions options) : base(options)
    {
    
    }

    public DbSet<User> Users { get; set; }
    // Other DbSet properties for additional entities

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("OurConnectionString");
    }

    // Example: Creating a database index in EF Core
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>()
            .HasIndex(u => u.Email)
            .IsUnique();
    }

}

We developer can improve the performance of their EF Core applications by using performance optimization techniques like database indexes, optimizing queries, implementing caching strategies, and monitoring performance metrics. By combining these techniques with careful analysis and tuning, significant performance improvements and a better user experience can be achieved.

Testing Entity Framework Core Applications

Tests are crucial to the reliability and maintainability of Entity Framework Core (EF Core) applications. By thoroughly testing our codebase, you can reduce the risk of bugs and errors in production environments by identifying and addressing potential issues early in the development lifecycle.

Different types of testing methodologies, such as unit testing, integration testing, and mocking frameworks, can be used to test EF Core applications. Testing individual components and units of code in isolation is referred to as unit testing. In contrast, integration testing involves ensuring that different components or modules work smoothly together.

As a way to test database interactions in EF Core applications, developers often use mocking frameworks to simulate database behavior without actually hitting the database. You can isolate our tests without relying on external dependencies by creating mock objects that mimic the behavior of real database objects using mocking frameworks.

Setting up in-memory databases or using mocking libraries are also useful techniques for testing EF Core applications. With in-memory databases, you are able to run tests against a temporary database that exists only in memory, providing a lightweight and fast alternative to traditional database systems. By creating mock objects for database operations, mocking libraries eliminate the need to interact with a physical database during testing.

The reliability and maintainability of our EF Core applications can be greatly improved by implementing testing strategies such as unit testing, integration testing, mocking frameworks, and in-memory databases or mocking libraries. Our software will perform at its best if you invest time and effort into testing.

//DbContextMocker.cs class the file is kept in   Project EntityFrameworkCoreGuideNET8.Tests
using EntityFrameworkCoreGuideNET8.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCoreGuideNET8.Tests;
public static class DbContextMocker
{
    public static DemoDbContext GetDemoDbContext(string dbName)
    {
        var options = new DbContextOptionsBuilder<DemoDbContext>()
            .UseInMemoryDatabase(databaseName: dbName)
            .Options;

        var dbContext = new DemoDbContext(options);

        // Seed your in-memory database with test data if needed

        return dbContext;
    }
}
//UserRepositoryTests.cs class the file is kept in   Project EntityFrameworkCoreGuideNET8.Tests
using EntityFrameworkCoreGuideNET8.Infrastructure.Data.Repository;

namespace EntityFrameworkCoreGuideNET8.Tests;

public class UserRepositoryTests
{
    // Example: Writing unit tests for EF Core repositories

    [Fact]
    public async Task GetUsers_ReturnsAllUsers()
    {
        // Arrange
        var dbContext = DbContextMocker.GetDemoDbContext(nameof(GetUsers_ReturnsAllUsers));
        var repository = new UserRepository(dbContext);

        // Act
        var users = await repository.GetAllActiveUsersAsync();

        // Assert
        Assert.Equal(3, users.Count);
    }

}

Entity Framework Core Best Practices
 

Maintain Code Quality and Consistency

Maintaining code quality and consistency is essential when working with Entity Framework Core (EF Core) to ensure robust and maintainable applications. By adhering to recommended best practices and conventions, developers can streamline development, improve code readability, and minimize potential issues. Let's explore some of these best practices.

// Example: Creating a DemoDbContext class the file is kept in DbContext Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data

using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Data;
public class DemoDbContext: DbContext
{
    
    public DemoDbContext(DbContextOptions options) : base(options)
    {
    
    }

    public DbSet<User> Users { get; set; }
    // Other DbSet properties for additional entities

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("OurConnectionString");
    }

    // Example: Creating a database index in EF Core
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>()
            .HasIndex(u => u.Email)
            .IsUnique();

        // Fluent API configurations
        modelBuilder.Entity<User>()
            .Property(e => e.Id)
            .IsRequired();

    }

}

using EntityFrameworkCoreGuideNET8.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

Console.WriteLine("Hello, from Ziggy Rafiq!");



using (var context = new DemoDbContext(new DbContextOptionsBuilder<DemoDbContext>()
    .UseSqlServer("our_connection_string_here").Options))
{
    // Use context to interact with the database
}

Use meaningful naming conventions

Choose descriptive names for entities, properties, and methods to enhance code readability and understanding.

// Example: Creating a User record the file is kept in Models Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data
using System.ComponentModel.DataAnnotations;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
public record User
{
    [Key]
    public Guid Id { get; init; }

    public List<Order> Orders { get; init; } = new List<Order>();

    [Required]
    public string FirstName { get; init; } = string.Empty;

    [Required]
    public string LastName { get; init; } = string.Empty;

    [Required]
    public string Email { get; init; } = string.Empty;

    [Required]
    public string EmailConfirmed { get; init; } = string.Empty;

    [Required]
    public string Phone { get; init; } = string.Empty;

    [Required]
    public string PhoneConfirmed { get; init; } = string.Empty;

    public bool IsActive { get; init; } = false;

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

Keep DbContext lean and focused

Avoid cluttering the DbContext class with unnecessary configurations or dependencies. Keep it focused on defining entities and their relationships.

// Example: Creating a DemoDbContext class the file is kept in DbContext Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data

using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Data;
public class DemoDbContext: DbContext
{
    
    public DemoDbContext(DbContextOptions options) : base(options)
    {
    
    }

    public DbSet<User> Users { get; set; }
    // Other DbSet properties for additional entities

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("OurConnectionString");
    }
 

}

Use Data Annotations or Fluent API

Choose either Data Annotations or Fluent API consistently for configuring entities and relationships. Prefer Fluent API for more complex configurations.

// Example: Creating a User record the file is kept in Models Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data
using System.ComponentModel.DataAnnotations;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
public record User
{
    [Key]
    public Guid Id { get; init; }

    public List<Order> Orders { get; init; } = new List<Order>();

    [Required]
    public string FirstName { get; init; } = string.Empty;

    [Required]
    public string LastName { get; init; } = string.Empty;

    [Required]
    public string Email { get; init; } = string.Empty;

    [Required]
    public string EmailConfirmed { get; init; } = string.Empty;

    [Required]
    public string Phone { get; init; } = string.Empty;

    [Required]
    public string PhoneConfirmed { get; init; } = string.Empty;

    public bool IsActive { get; init; } = false;

    [Timestamp]
    public byte[]? RowVersion { get; set; }
}
// Example: Creating a UserRepository class the file is kept in Models Folder in Project EntityFrameworkCoreGuideNET8.Domain
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
public record Order
{
    [Key]
    public Guid Id { get; init; }

    public Guid UserId { get; init; }

    [ForeignKey("UserId")]
    public required User User { get; init; }

    [Required]
    public string ProductName { get; init; }=string.Empty;

    [Required,Range(0, double.MaxValue, ErrorMessage = "Price must be a positive value.")]
    public decimal Price { get; init; }

    [Required]
    public DateTime OrderDate { get; init; }
        
}

Handle concurrency with timestamps

Use timestamps (e.g., RowVersion) to handle concurrency conflicts when multiple users update the same entity simultaneously.

// Example: Creating a User record the file is kept in Models Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data
using System.ComponentModel.DataAnnotations;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
public record User
{
    [Key]
    public Guid Id { get; init; }

    public List<Order> Orders { get; init; } = new List<Order>();

    [Required]
    public string FirstName { get; init; } = string.Empty;

    [Required]
    public string LastName { get; init; } = string.Empty;

    [Required]
    public string Email { get; init; } = string.Empty;

    [Required]
    public string EmailConfirmed { get; init; } = string.Empty;

    [Required]
    public string Phone { get; init; } = string.Empty;

    [Required]
    public string PhoneConfirmed { get; init; } = string.Empty;

    public bool IsActive { get; init; } = false;

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

Optimize database interactions

Minimize database roundtrips by using eager loading, explicit loading, or lazy loading appropriately. Avoid N+1 query issues by eagerly loading related entities when needed.

// Eager loading example
var users = dbContext.Users.Include(u => u.Orders).ToList();

Handle exceptions gracefully

Implement error handling mechanisms to handle exceptions thrown by EF Core operations gracefully. Use try-catch blocks to catch specific exceptions and provide meaningful error messages to users.

// Example: Creating a UserRepository class the file is kept in Repository Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.DTOs;
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Interfaces;
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Data.Repository;
public class UserRepository : IUserRepository
{
    private readonly DemoDbContext _dbContext;
    private bool _disposed = false;

    public UserRepository(DemoDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

  

 

    /// <summary>
    /// Add or Updates the User
    /// </summary>
    /// <param name="user">User Model</param>
    /// <returns></returns>
    public async Task SaveAsync(User user)
    {
        try
        {
            _dbContext.Entry(user).State = user.Id == Guid.Empty ? EntityState.Added : EntityState.Modified;

            await _dbContext.SaveChangesAsync();
        }
        catch (DbUpdateException ex)
        {
            Console.WriteLine("An error occurred while saving changes to the database: " + ex.Message);
        }
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            _dbContext.Dispose();
        }

        _disposed = true;
    }


    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

By following these best practices and conventions, developers can maintain code quality, consistency, and reliability when working with Entity Framework Core. These practices contribute to building scalable, maintainable, and efficient applications in the .NET ecosystem.

Use DTOs (Data Transfer Objects)

When working with Entity Framework Core (EF Core) in ASP.NET Core applications, utilizing Data Transfer Objects (DTOs) for projection offers numerous benefits, including improved performance, reduced data transfer, and enhanced security. Additionally, avoiding direct usage of DbSet in controllers helps maintain the separation of concerns and promotes cleaner, more maintainable code. Let's delve into best practices for implementing DTOs and avoiding direct DbSet usage in controllers:

Implement DTOs for projection

Create DTO classes tailored to specific use cases to transfer data between our application layers. DTOs allow you to shape the data returned from EF Core queries precisely as needed by the client without exposing our domain entities directly.

// DTOs.cs file  in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data
namespace EntityFrameworkCoreGuideNET8.Infrastructure.Domain.DTOs;
public record UserDto(Guid Id, string FirstName, string LastName, string Email, string Phone, bool IsActive);


public record OrderDto(Guid Id, Guid UserId, string ProductName, decimal Price, DateTime OrderDate);

Use projection to populate DTOs

When querying data from EF Core, use projection to map the query results directly to DTOs. This approach reduces the amount of data transferred over the network and improves performance by fetching only the required fields.

// Example: Creating a UserRepository class the file is kept in Repository Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.DTOs;
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Interfaces;
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Data.Repository;
public class UserRepository : IUserRepository
{
    private readonly DemoDbContext _dbContext;
    private bool _disposed = false;

    public UserRepository(DemoDbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
    }

    /// <summary>
    /// This example demonstrates the usage of UserDto for accessing the data.
    /// </summary>
    /// <returns>List of Users</returns>
    public Task<List<UserDto>> GetUserDtosAsync() => _dbContext.Users
      .Select(u => new UserDto(
          u.Id,
          u.FirstName,
          u.LastName,
          u.Email,
          u.Phone,
          u.IsActive
      ))
      .ToListAsync();


    

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            _dbContext.Dispose();
        }

        _disposed = true;
    }


    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Avoid using DbSet directly in controllers

Instead of returning DbSet directly from controllers, project the data into DTOs to ensure that only necessary data is exposed to clients. This approach helps prevent over-fetching of data and minimizes potential security risks associated with exposing domain entities.

[ApiController]
[Route("api/users")]
public class UserController : ControllerBase
{
    private readonly MyDbContext _dbContext;

    public UserController(MyDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    [HttpGet]
    public ActionResult<IEnumerable<UserDto>> GetUsers()
    {
        var userDtos = _dbContext.Users
            .Select(u => new UserDto
            {
                UserId = u.UserId,
                UserName = u.UserName,
                // Map other properties as needed...
            })
            .ToList();

        return Ok(userDtos);
    }
}

By utilizing DTOs for projection and avoiding direct usage of DbSet in controllers, developers can design more efficient, secure, and maintainable ASP.NET Core applications. This approach facilitates better separation of concerns, enhances performance, and improves overall code quality.

Ensure concurrency conflicts are handled gracefully

Ensuring concurrency conflicts are handled gracefully, implementing logging and error handling, and regularly updating EF Core and its dependencies are crucial practices for maintaining the reliability, security, and performance of ASP.NET Core applications. Let's explore these best practices in detail:

Handle concurrency conflicts gracefully

When multiple users attempt to modify the same data simultaneously, concurrency conflicts can occur. EF Core provides mechanisms to detect and resolve these conflicts, such as using timestamps or row versioning. Implement appropriate concurrency control strategies to handle conflicts and ensure data integrity.

// Example: Creating a User record the file is kept in Models Folder in Project EntityFrameworkCoreGuideNET8.Infrastructure.Data
using System.ComponentModel.DataAnnotations;

namespace EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Models;
public record User
{
    [Key]
    public Guid Id { get; init; }

    public List<Order> Orders { get; init; } = new List<Order>();

    [Required]
    public string FirstName { get; init; } = string.Empty;

    [Required]
    public string LastName { get; init; } = string.Empty;

    [Required]
    public string Email { get; init; } = string.Empty;

    [Required]
    public string EmailConfirmed { get; init; } = string.Empty;

    [Required]
    public string Phone { get; init; } = string.Empty;

    [Required]
    public string PhoneConfirmed { get; init; } = string.Empty;

    public bool IsActive { get; init; } = false;

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

Implement logging and error handling

Logging and error handling are essential for identifying and diagnosing issues in production environments. Use logging frameworks like Serilog or NLog to record application events, errors, and warnings. Implement structured logging to capture relevant information for troubleshooting.

using EntityFrameworkCoreGuideNET8.Business.UI.Models;
using EntityFrameworkCoreGuideNET8.Infrastructure.Domain.Interfaces;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

namespace EntityFrameworkCoreGuideNET8.Business.UI.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IUserService _userService;

        public HomeController(ILogger<HomeController> logger, IUserService userService)
        {
            _logger = logger;
            _userService = userService;
        }

        public IActionResult Index()
        {
            return View();
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [HttpGet]
        public async Task<IActionResult> GetAllActiveUser()
        {
            try
            {
                var user = await _userService.GetAllActiveUsersAsync();
                if (user == null)
                {
                    return NotFound();
                }
                return Ok(user);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred while retrieving the user.");
                return StatusCode(500, "An unexpected error occurred.");
            }
        }


        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

Regularly update EF Core and dependencies

EF Core releases updates periodically to introduce new features, enhancements, and bug fixes. Stay up-to-date with the latest EF Core versions and regularly update our project's dependencies to leverage improvements and security patches.

dotnet add package Microsoft.EntityFrameworkCore --version x.x.x

By adhering to these best practices, developers can enhance the reliability, security, and performance of their ASP.NET Core applications. Handling concurrency conflicts gracefully, implementing robust logging and error handling, and keeping EF Core and dependencies updated are critical steps in ensuring the long-term success of our application.

Summary

A cornerstone of modern .NET development, Entity Framework Core provides robust data access solutions. By using the latest features from C# 10 and .NET 8, developers can improve database interactions, boost performance, and increase productivity. By reading this comprehensive guide, you will be able to use Entity Framework Core to its fullest extent. You'll ensure the scalability, reliability, and maintainability of our .NET applications by mastering these concepts, techniques, and best practices.

Please do not forget to like this article if you have found it useful and follow me on my LinkedIn https://www.linkedin.com/in/ziggyrafiq/ also I have uploaded the source code for this article on my GitHub Repo: https://github.com/ziggyrafiq/EntityFrameworkCoreGuideNET8