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.
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
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; }
}
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;
}
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
);
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
}
UpdateProductRequest
public sealed record UpdateProductRequest(
Guid Id,
string Name,
string Description,
int Quantity,
decimal Price
);
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
}
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
);
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"
}
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
);
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"
}
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);
});
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);
}
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);
}
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"
}
}
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<>));
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;
});
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 thecurl
command.
Example cURL
curl -X GET https://localhost:5001/productsWithGRPattern
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
- Microsoft Docs: Repository Pattern
- Minimal APIs in ASP.NET Core
- Entity Framework Core Documentation
- Best Practices for ASP.NET Core Web APIs
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)