DEV Community

Jiten Shahani
Jiten Shahani

Posted on • Edited on

Repository Pattern – ASP.NET Core Web API

This guide will help you, as a newcomer to ASP.NET Core, understand how to structure a web API project that interacts with a database. The project demonstrates three different data access approaches: direct use of the DbContext, the repository pattern, and the generic repository pattern. You’ll learn not only how to follow the code, but also why each approach is useful as your projects grow.

Technology Stack & NuGet Packages

This project leverages modern .NET technologies and best practices:

.NET 9.0:                 The latest version of the .NET platform, providing enhanced performance and modern language features.
C# 13.0:                  Utilizing the latest C# language features including record types, nullable reference types, and enhanced pattern matching.
ASP.NET Core:             Microsoft's cross-platform, high-performance framework for building modern, cloud-based, internet-connected applications.
Entity Framework Core:    A lightweight, extensible, and cross-platform Object-Relational Mapper (ORM) for .NET.
SQLite:                   A self-contained, serverless, zero-configuration database engine used as the data store.
OpenAPI:                  For automatic API documentation and testing interface.
Minimal APIs:             A simplified approach to building HTTP APIs with ASP.NET Core, focusing on reducing ceremony and boilerplate code.
Enter fullscreen mode Exit fullscreen mode

This project uses the following NuGet packages:

Package Name Purpose Version
Microsoft.AspNetCore.OpenApi Adds OpenAPI/Swagger support for API docs 9.0.4
Microsoft.EntityFrameworkCore Entity Framework Core ORM 9.0.4
Microsoft.EntityFrameworkCore.Sqlite SQLite provider for EF Core 9.0.4
Microsoft.EntityFrameworkCore.Tools Design-time tools for EF Core (migrations) 9.0.4

What are NuGet packages?
NuGet is the package manager for .NET. Packages are reusable libraries you can add to your project to avoid writing everything from scratch.

Project Structure Overview

A well-structured project is easier to understand and maintain. Here’s how this demo is organized:

RepositoryPatternDemo/
├── Entities/                   # Contains models representing database entities, with properties directly mapped to table columns.
├── Data/                       # Contains the `AppDbContext`, which is your gateway to the database.
├── Dtos/                       # Defines Data Transfer Objects (DTO's) for request and response handling, ensuring data encapsulation and structured communication.
├── Repositories/               # Implements data access logic, effectively separating it from core business operations.
├── Endpoints/                  # Defines API endpoints responsible for handling HTTP requests and responses.
└── Program.cs                  # Application startup and configuration
Enter fullscreen mode Exit fullscreen mode

Entity Models

Entity models are C# classes that map directly to database tables.

Product

public sealed class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Customer

public sealed class Customer
{
    public Guid Id { get; set; }
    public string CompanyName { get; set; } = string.Empty;
    public string ContactName { get; set; } = string.Empty;
    public string ContactTitle { get; set; } = string.Empty;
    public string Address { get; set; } = string.Empty;
    public string City { get; set; } = string.Empty;
    public string PostalCode { get; set; } = string.Empty;
    public string Country { get; set; } = string.Empty;
    public string Phone { get; set; } = string.Empty;
    public string? Fax { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

Data Transfer Objects (DTOs)

DTOs define the data structure for creating and updating Products and Customers via the API. They help keep your API contracts clean and separate from your database models.

CreateProductRequest

public sealed record CreateProductRequest(
    string Name,
    string Description,
    int Quantity,
    decimal Price
);
Enter fullscreen mode Exit fullscreen mode

Purpose

Used when creating a new product.

Example JSON

{
  "name": "Gaming Laptop",
  "description": "A high-performance laptop with advanced graphics for gaming.",
  "quantity": 10,
  "price": 999.99
}
Enter fullscreen mode Exit fullscreen mode

UpdateProductRequest

public sealed record UpdateProductRequest(
    Guid Id,
    string Name,
    string Description,
    int Quantity,
    decimal Price
);
Enter fullscreen mode Exit fullscreen mode

Purpose

Used when updating an existing product.

Example JSON

{
  "id": "01969b65-305c-75ae-bade-3c9d18855d5d",
  "name": "Gaming Mice",
  "description": "A high-performance low-latency lightweight mice for gaming.",
  "quantity": 22,
  "price": 68.99
}
Enter fullscreen mode Exit fullscreen mode

CreateCustomerRequest

public sealed record CreateCustomerRequest(
    string CompanyName,
    string ContactName,
    string ContactTitle,
    string Address,
    string City,
    string PostalCode,
    string Country,
    string Phone,
    string? Fax,
    string Email
);
Enter fullscreen mode Exit fullscreen mode

Purpose

Used when creating a new customer.

Example JSON:

{
  "companyName": "Dummy Company",
  "contactName": "John Doe",
  "contactTitle": "Owner",
  "address": "919 Williams Court, Brooklyn",
  "city": "New York",
  "postalCode": "11225",
  "country": "United States of America",
  "phone": "+ 1-212-391 7668",
  "fax": "",
  "email": "sales@dummyco.com"
}
Enter fullscreen mode Exit fullscreen mode

UpdateCustomerRequest

public sealed record UpdateCustomerRequest(
    Guid Id,
    string CompanyName,
    string ContactName,
    string ContactTitle,
    string Address,
    string City,
    string PostalCode,
    string Country,
    string Phone,
    string? Fax,
    string Email
);
Enter fullscreen mode Exit fullscreen mode

Purpose

Used when updating an existing customer.

Example JSON

{
  "id": "01969b6f-7399-7d3a-811e-b764befb8bd6",
  "companyName": "Dummy Company",
  "contactName": "John Doe",
  "contactTitle": "Managing Director",
  "address": "919 Williams Court, Brooklyn",
  "city": "New York",
  "postalCode": "11225",
  "country": "United States of America",
  "phone": "+ 1-212-391 7668",
  "fax": "+ 1-212-391 7669",
  "email": "sales@dummyco.com"
}
Enter fullscreen mode Exit fullscreen mode

Summary Table

DTO Name Used For Required Fields
CreateProductRequest Creating a product Name, Description, Quantity, Price
UpdateProductRequest Updating a product Id, Name, Description, Quantity, Price
CreateCustomerRequest Creating a customer CompanyName, ContactName, ContactTitle, Address, City, PostalCode, Country, Phone, Fax (optional), Email
UpdateCustomerRequest Updating a customer Id, CompanyName, ContactName, ContactTitle, Address, City, PostalCode, Country, Phone, Fax (optional), Email

Data Access Approaches

Direct DbContext Usage

What is it?

Endpoints interact directly with the database context (AppDbContext).

Example

productGroup.MapGet("/", async (AppDbContext dbContext, CancellationToken cancellationToken) =>
{
    var products = await dbContext.Products.ToListAsync(cancellationToken);
    return TypedResults.Ok(products);
});
Enter fullscreen mode Exit fullscreen mode

When to use

  • Learning or prototyping
  • Very simple applications

Repository Pattern

What is it?

You create a repository class for each entity (e.g., ProductRepository). The repository handles all database operations for that entity, keeping your endpoints clean and focused.

Example Interface

public interface IProductRepository
{
    Task<List<Product>> GetAllAsync(CancellationToken cancellationToken);
    Task<Product?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
    Task AddAsync(Product product, CancellationToken cancellationToken);
    Task UpdateAsync(Product product, CancellationToken cancellationToken);
    Task DeleteAsync(Product product, CancellationToken cancellationToken);
}
Enter fullscreen mode Exit fullscreen mode

When to use

  • Medium to large applications
  • When you want to separate concerns and improve testability

Generic Repository Pattern

What is it?

A single repository class (Repository<TEntity>) can handle all entities, using generics.

Example Interface

public interface IRepository<TEntity> where TEntity : class
{
    Task<List<TEntity>> GetAllAsync(CancellationToken cancellationToken);
    Task<TEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken);
    Task AddAsync(TEntity entity, CancellationToken cancellationToken);
    Task UpdateAsync(TEntity entity, CancellationToken cancellationToken);
    Task DeleteAsync(TEntity entity, CancellationToken cancellationToken);
}
Enter fullscreen mode Exit fullscreen mode

When to use

  • Applications with many similar entities
  • When you want to avoid code duplication

Benefits Comparison

Approach Simplicity Scalability Testability Code Duplication
Direct DbContext High Low Low High
Repository Pattern Medium High High Medium
Generic Repository Pattern Medium High High Low

API Endpoints

Your API exposes endpoints for both products and customers, grouped by the data access pattern used.

1. Product Endpoints

A. Direct DbContext Usage (/products)

HTTP Method Route Description
GET /products Get a list of all products.
GET /products/{id} Get a single product by its unique ID.
POST /products Create a new product.
PUT /products Update an existing product (all fields).
DELETE /products/{id} Delete a product by its unique ID.

B. Repository Pattern (/productsWithRPattern)

HTTP Method Route Description
GET /productsWithRPattern Get a list of all products using the repository.
GET /productsWithRPattern/{id} Get a product by ID using the repository.
POST /productsWithRPattern Create a new product using the repository.
PUT /productsWithRPattern Update a product using the repository.
DELETE /productsWithRPattern/{id} Delete a product by ID using the repository.

C. Generic Repository Pattern (/productsWithGRPattern)

HTTP Method Route Description
GET /productsWithGRPattern Get all products using the generic repository.
GET /productsWithGRPattern/{id} Get a product by ID using the generic repository.
POST /productsWithGRPattern Create a new product using the generic repository.
PUT /productsWithGRPattern Update a product using the generic repository.
DELETE /productsWithGRPattern/{id} Delete a product by ID using the generic repository.

2. Customer Endpoints

A. Repository Pattern (/customersWithRPattern)

HTTP Method Route Description
GET /customersWithRPattern Get a list of all customers using the repository.
GET /customersWithRPattern/{id} Get a customer by ID using the repository.
POST /customersWithRPattern Create a new customer using the repository.
PUT /customersWithRPattern Update a customer using the repository.
DELETE /customersWithRPattern/{id} Delete a customer by ID using the repository.

B. Generic Repository Pattern (/customersWithGRPattern)

HTTP Method Route Description
GET /customersWithGRPattern Get all customers using the generic repository.
GET /customersWithGRPattern/{id} Get a customer by ID using the generic repository.
POST /customersWithGRPattern Create a new customer using the generic repository.
PUT /customersWithGRPattern Update a customer using the generic repository.
DELETE /customersWithGRPattern/{id} Delete a customer by ID using the generic repository.

Each group supports:

  • GET: Retrieve all or by ID
  • POST: Add new
  • PUT: Update existing
  • DELETE: Remove by ID

Setup and Configuration

Connection String

The connection string tells your app where to find the database. For SQLite, it’s as simple as:

{
  "ConnectionStrings": {
    "Database": "Data Source=Data\\AppDB.db"
  }
}
Enter fullscreen mode Exit fullscreen mode

Service Registration

Register your services in Program.cs so they can be injected where needed:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("Database")));

builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
Enter fullscreen mode Exit fullscreen mode

JSON Serialization

To ensure your API responses are easy to work with:

builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = null;
    options.SerializerOptions.PropertyNameCaseInsensitive = true;
    options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
    options.SerializerOptions.WriteIndented = true;
});
Enter fullscreen mode Exit fullscreen mode

Testing the API

  • Swagger UI: Run your project and navigate to /swagger in your browser for interactive API documentation and testing.
  • Postman/cURL: You can also test endpoints using tools like Postman or the curl command.

Example cURL

curl -X GET https://localhost:5001/productsWithGRPattern
Enter fullscreen mode Exit fullscreen mode

Conclusion and Recommendations

  • Start simple with direct DbContext usage for learning or tiny projects.
  • Adopt the Repository Pattern as your application grows, to keep code organized and testable.
  • Use the Generic Repository Pattern to avoid repetition, especially with many similar entities.
  • Best Practices:
    • Use dependency injection for all services.
    • Keep your entity models and DTOs separate.
    • Always validate input and handle errors gracefully.
    • Use AsNoTracking() for read-only queries to improve performance.

Key Takeaways

  • Start simple with direct DbContext usage for learning or tiny projects.
  • Adopt the repository pattern as your application grows to keep code organized and testable.
  • Use the generic repository pattern to avoid repetition, especially with many similar entities.
  • The repository pattern improves testability and separation of concerns by abstracting data access logic.
  • Minimal APIs in ASP.NET Core allow you to build HTTP APIs with less ceremony and boilerplate code.
  • Use dependency injection and keep your API contracts (DTOs) separate from your entity models for maintainability.

Further Reading

This demo project provides a strong foundation for learning modern ASP .NET Core Web API development. By understanding and applying these patterns, you’ll be well-equipped to build scalable, maintainable, and professional web APIs.

About the Author

Hi, I’m Jiten Shahani, a passionate developer with a strong background in API development and C# programming. Although I’m new to .NET, my journey into learning ASP .NET Core began in December 2024, driven by a desire to build scalable and maintainable applications.

Feel free to connect with me to exchange ideas and learn together!

Top comments (0)