When building web APIs, data efficiency is crucial. If your app retrieves more data than needed, it affects memory, speed, and even user experience. Lazy Loading is a smart strategy that allows you to defer the loading of related data until you actually need it.
In this article, we’ll walk through everything you need to know about Lazy Loading in ASP.NET Core Web API using Entity Framework Core with a full implementation example.
What is Lazy Loading?
Lazy Loading is a technique where related data is not loaded from the database until it is specifically requested. This helps in reducing the initial loading time and memory usage of an application.
In EF Core, Lazy Loading works by intercepting access to navigation properties and loading them only when they’re used.
Comparison Table
Loading Type |
Definition |
When to Use |
Eager Loading |
Load related data immediately using.Include() |
You know you’ll need related data |
Lazy Loading |
Load related data only when it's accessed |
You don’t always need related data |
Explicit Loading |
Manually load related data via code |
You want full control |
Step 1. Create a New ASP.NET Core Web API Project.
dotnet new webapi -n LazyLoadingDemo
cd LazyLoadingDemo
Step 2. Install Required NuGet Packages.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Proxies
Step 3. Define Your Models.
Models/Product.cs
namespace LazyLoadingDemo.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public int CategoryId { get; set; }
// Lazy-loaded navigation property
public virtual Category Category { get; set; }
}
}
Models/Category.cs
namespace LazyLoadingDemo.Models
{
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
// Lazy-loaded navigation property
public virtual ICollection<Product> Products { get; set; }
}
}
Step 4. Create DbContext and Enable Lazy Loading.
Data/AppDbContext.cs
using LazyLoadingDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace LazyLoadingDemo.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLazyLoadingProxies();
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
}
}
Step 5. Register DbContext in Program.cs.
using LazyLoadingDemo.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register DbContext with Lazy Loading
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseLazyLoadingProxies()
.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();
Add the connection string in appsettings.json.
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=LazyLoadingDb;Trusted_Connection=True;"
}
Step 6. Create and Apply Migrations.
dotnet ef migrations add InitialCreate
dotnet ef database update
Step 7. Create DTOs to Avoid Infinite Loops.
DTOs/ProductDto.cs
namespace LazyLoadingDemo.DTOs
{
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
public string CategoryName { get; set; }
}
}
Step 8. Create an API Controller.
Controllers/ProductsController.cs
using LazyLoadingDemo.Data;
using LazyLoadingDemo.DTOs;
using LazyLoadingDemo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace LazyLoadingDemo.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
// GET: api/products
[HttpGet]
public ActionResult<IEnumerable<ProductDto>> GetAllProducts()
{
var products = _context.Products.ToList();
var result = products.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
CategoryName = p.Category?.Name // Triggers lazy loading
}).ToList();
return Ok(result);
}
// GET: api/products/1
[HttpGet("{id}")]
public ActionResult<ProductDto> GetProductById(int id)
{
var product = _context.Products.Find(id);
if (product == null)
return NotFound();
// Accessing the Category navigation property triggers lazy loading
var result = new ProductDto
{
Id = product.Id,
Name = product.Name,
CategoryName = product.Category?.Name
};
return Ok(result);
}
// POST: api/products
[HttpPost]
public async Task<ActionResult<Product>> CreateProduct(Product product)
{
// Validate Category exists
var category = await _context.Categories.FindAsync(product.CategoryId);
if (category == null)
return BadRequest("Invalid category ID.");
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetProductById), new { id = product.Id }, product);
}
// DELETE: api/products/1
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
var product = await _context.Products.FindAsync(id);
if (product == null)
return NotFound();
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return NoContent();
}
}
}
Step 9. Seed Sample Data (Optional).
Add this to the Program.cs before app.Run().
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
if (!db.Categories.Any())
{
var electronics = new Category { Name = "Electronics" };
db.Categories.Add(electronics);
db.Products.Add(new Product { Name = "Smartphone", Category = electronics });
db.Products.Add(new Product { Name = "Laptop", Category = electronics });
db.SaveChanges();
}
}
Test with Swagger or Postman
GET https://localhost:{port}/api/products/1
GitHubProject Link
https://github.com/SardarMudassarAliKhan/LazyLoadingInAspNetCoreWebAPI2025.git
Output
![Output]()
Conclusion
Lazy loading is a powerful technique in ASP.NET Core Web API when used with Entity Framework Core. It allows your application to load related data only when it's needed, reducing initial load times and improving overall performance. By properly configuring EF Core with lazy loading proxies, using virtual navigation properties, and returning well-structured DTOs, you can take full advantage of this feature while avoiding common pitfalls like the N+1 query problem or serialization loops.
However, lazy loading isn't a one-size-fits-all solution. You should evaluate each use case to determine whether lazy loading, eager loading, or explicit loading makes the most sense. When applied correctly, lazy loading helps you build efficient, maintainable, and scalable APIs.