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.
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
andPostId
are primary keys.BlogId
inPost
is a foreign key linking toBlog
.- Navigation properties (
Posts
inBlog
andBlog
inPost
) 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 betweenBlog
andPost
.
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
registersAppDbContext
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.
- 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
- Open a browser and navigate to
https://localhost:<port>/swagger
to access the Swagger UI. - Test the API endpoints:
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.