Configuring Entity Framework Core in a .NET Web API: A Comprehensive Guide

Mishel Shaji
Mishel Shaji

Developers can easily create rich, data-driven APIs with Entity Framework Core in a .NET Web API project. This article walks you through the process of setting up a Web API with EF Core, building a fully functional CRUD controller, creating models, configuring a DbContext, and implementing migrations.

In addition, you will learn advanced configurations and best practices for increased performance as well as reliability.

Prerequisites

Before starting, ensure you have the following:

  • .NET 8 or newer SDK installed (use the latest version).
  • A compatible database (e.g., SQL Server, PostgreSQL, or SQLite).
  • A code editor or an IDE like Visual Studio, Visual Studio Code, or JetBrains Rider.
  • Basic knowledge of C#, ASP.NET Core, and relational databases.

Step 1: Creating a .NET 8 Web API Project

Start by creating a new ASP.NET Core Web API project using the .NET CLI.

  1. Microsoft.EntityFrameworkCore: Core EF Core package.
    • Microsoft.EntityFrameworkCore.SqlServer: SQL Server provider.
    • Microsoft.EntityFrameworkCore.Design: Required for migrations.

Add the necessary EF Core NuGet packages for SQL Server:

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

For other databases (e.g., PostgreSQL or SQLite), replace Microsoft.EntityFrameworkCore.SqlServer with the appropriate provider (e.g., Npgsql.EntityFrameworkCore.PostgreSQL).

Open a terminal and run:

dotnet new webapi -n EFCoreWebApiDemo
cd EFCoreWebApiDemo

Step 2: Defining the Models

Create models to represent database tables. For this example, we’ll use a Blog and Post model to demonstrate a one-to-many relationship.

Create a Models folder in the project and add the following classes:

namespace EFCoreWebApiDemo.Models;

public class Blog
{
    public int BlogId { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public List<Post> Posts { get; set; } = new();
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public int BlogId { get; set; }
    public Blog Blog { get; set; } = null!;
}
  • BlogId and PostId are primary keys.
  • BlogId in Post is a foreign key linking to Blog.
  • Navigation properties (Posts in Blog and Blog in Post) define the relationship.

Step 3: Creating the DbContext

The DbContext class acts as a bridge between your application and the database. Create a Data folder and add an AppDbContext class:

using EFCoreWebApiDemo.Models;
using Microsoft.EntityFrameworkCore;

namespace EFCoreWebApiDemo.Data;

public class AppDbContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasMany(b => b.Posts)
            .WithOne(p => p.Blog)
            .HasForeignKey(p => p.BlogId);
    }
}
  • DbSet properties map to database tables.
  • The constructor accepts DbContextOptions for provider configuration.
  • OnModelCreating configures the one-to-many relationship between Blog and Post.

Step 4: Configuring the DbContext in Program.cs

In .NET 8, the Program.cs file is used to configure services and the application pipeline. Update it to register the DbContext and configure the database connection.

Modify Program.cs:

using EFCoreWebApiDemo.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Configure DbContext with SQL Server
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        sqlOptions => sqlOptions.EnableRetryOnFailure()));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Add a connection string to appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=EFCoreWebApiDemo;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
  • AddDbContext registers AppDbContext with dependency injection.
  • The connection string is retrieved from appsettings.json.
  • EnableRetryOnFailure adds resiliency for transient database failures.
  • Swagger is enabled for API documentation in development.

Step 5: Creating a Controller

Create an API controller to handle CRUD operations for Blog entities. Add a Controllers/BlogsController.cs file:

using EFCoreWebApiDemo.Data;
using EFCoreWebApiDemo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace EFCoreWebApiDemo.Controllers;

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

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

    // GET: api/Blogs
    [HttpGet]
    public async Task<ActionResult<IEnumerable<Blog>>> GetBlogs()
    {
        return await _context.Blogs.Include(b => b.Posts).AsNoTracking().ToListAsync();
    }

    // GET: api/Blogs/5
    [HttpGet("{id}")]
    public async Task<ActionResult<Blog>> GetBlog(int id)
    {
        var blog = await _context.Blogs.Include(b => b.Posts)
            .AsNoTracking()
            .FirstOrDefaultAsync(b => b.BlogId == id);

        if (blog == null)
        {
            return NotFound();
        }

        return blog;
    }

    // POST: api/Blogs
    [HttpPost]
    public async Task<ActionResult<Blog>> PostBlog(Blog blog)
    {
        _context.Blogs.Add(blog);
        await _context.SaveChangesAsync();

        return CreatedAtAction(nameof(GetBlog), new { id = blog.BlogId }, blog);
    }

    // PUT: api/Blogs/5
    [HttpPut("{id}")]
    public async Task<IActionResult> PutBlog(int id, Blog blog)
    {
        if (id != blog.BlogId)
        {
            return BadRequest();
        }

        _context.Entry(blog).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!BlogExists(id))
            {
                return NotFound();
            }
            throw;
        }

        return NoContent();
    }

    // DELETE: api/Blogs/5
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteBlog(int id)
    {
        var blog = await _context.Blogs.FindAsync(id);
        if (blog == null)
        {
            return NotFound();
        }

        _context.Blogs.Remove(blog);
        await _context.SaveChangesAsync();

        return NoContent();
    }

    private bool BlogExists(int id)
    {
        return _context.Blogs.Any(e => e.BlogId == id);
    }
}

This controller provides endpoints for:

  • Retrieving all blogs with their posts (GET /api/Blogs).
  • Retrieving a single blog by ID (GET /api/Blogs/{id}).
  • Creating a new blog (POST /api/Blogs).
  • Updating an existing blog (PUT /api/Blogs/{id}).
  • Deleting a blog (DELETE /api/Blogs/{id}).

Step 6: Adding Migrations

Use EF Core migrations to manage database schema changes.

  1. Ensure Microsoft.EntityFrameworkCore.Design is installed.

Apply the migration to create the database:

dotnet ef database update

This creates the EFCoreWebApiDemo database with Blogs and Posts tables.

Add a migration:

dotnet ef migrations add InitialCreate

This generates migration files in a Migrations folder.

Step 7: Seeding Data (Optional)

To test the API, seed some initial data. Update Program.cs to include seed logic:

// After app.Build()
using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await dbContext.Database.EnsureCreatedAsync();

    if (!dbContext.Blogs.Any())
    {
        var blog = new Blog
        {
            Title = "Sample Blog",
            Description = "A blog about .NET 8",
            Posts = new List<Post>
            {
                new Post { Title = "First Post", Content = "Welcome to EF Core!", CreatedAt = DateTime.UtcNow }
            }
        };
        dbContext.Blogs.Add(blog);
        await dbContext.SaveChangesAsync();
    }
}

This seeds a sample blog and post when the application starts.

Step 8: Testing the API

  1. Open a browser and navigate to https://localhost:<port>/swagger to access the Swagger UI.
  2. Test the API endpoints:
  3. GET /api/Blogs: Lists all blogs with their posts.
    • POST /api/Blogs: Create a new blog (e.g., send { "title": "New Blog", "description": "Test" }).
    • GET /api/Blogs/1: Retrieve the blog with ID 1.
    • PUT /api/Blogs/1: Update the blog with ID 1.
    • DELETE /api/Blogs/1: Delete the blog with ID 1.

Run the application:

dotnet run

Step 9: Advanced Configuration

Connection Resiliency

Enhance database reliability with connection resiliency:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection"),
        sqlOptions => sqlOptions.EnableRetryOnFailure(
            maxRetryCount: 5,
            maxRetryDelay: TimeSpan.FromSeconds(30),
            errorNumbersToAdd: null)));

Performance Optimizations

Select Specific Columns: Reduce data transfer:

await _context.Blogs.Select(b => new { b.BlogId, b.Title }).ToListAsync();

AsNoTracking: Use for read-only queries to improve performance.

await _context.Blogs.AsNoTracking().ToListAsync();

Conclusion

Using Entity Framework Core in a .NET Web API project allows developers to create rich, data-driven APIs with minimal work. This post demonstrates to you how to set up a Web API using EF Core, create models, set up a DbContext, implement migrations, and build a fully functional CRUD controller. You've also learned advanced setups and recommended practices for improved reliability and performance.

For more information, read the official EF Core documentation and experiment with other database providers or advanced capabilities such as query filters, compiled queries, fluent API configuration, and owned entities to meet your project's requirements.