Back to: ASP.NET Core Web API Tutorials
Real-time ASP.NET Core Project Development for Unit Testing
In this article, I will discuss Real-time ASP.NET Core Project Development for Unit Testing. Please read our previous article, which discusses the Basic Concepts of Unit Testing.
Why a well-organized project?
A well-organized project is the backbone of scalable and maintainable software. When developing an ASP.NET Core Web API, this means clearly separating responsibilities across different layers or components:
- Controllers: Handle HTTP requests and responses, orchestrate calls to business logic.
- Services (Business Logic Layer): Contain core application logic and business rules.
- Repositories (Data Access Layer): Abstract interaction with databases or external data sources.
- DTOs (Data Transfer Objects): Shape data flowing in and out of the API, decoupling internal models from external contracts.
- Models/Entities: Represent database or domain entities.
Why is this separation necessary?
- Adding new features without breaking existing ones: When code is modular and loosely coupled, changes in one part (e.g., repository) don’t ripple unexpectedly into others (e.g., controllers).
- Writing robust unit and integration tests: Smaller, focused components are easier to test in isolation.
- Refactoring or scaling: As the project grows, modular code helps manage complexity, optimize performance, or even swap implementations (e.g., switch from SQL to NoSQL) with minimal disruption.
Think of a software project like constructing a building:
- A clear blueprint (architecture) ensures every room (module) serves its purpose, wiring (dependencies) is organized, and future expansion (new features) doesn’t mean tearing down walls.
- A messy foundation leads to leaks, confusion, and expensive repairs (bugs, regression, maintenance nightmares).
Creating a Complete Mini ASP.NET Core Web API Project to Understand Unit Testing
Now, we will create a complete Order Management Web API Project to understand Unit Testing. For a better understanding, please refer to the following diagram.
The above diagram shows the typical layered architecture of an ASP.NET Core Web API project, specifically for an Order Management system, with the following layers:
- Controller Layer: Contains API endpoints like CreateOrder and GetOrderById. This layer handles incoming HTTP requests, validates them, and forwards the requests to the service layer.
- Service Layer: Implements the core business logic, such as order validation, updating order status, and applying discount rules. It communicates with the repository layer to perform data operations. Receives input through DTOs (Data Transfer Objects), which shape the data sent to and from clients.
- Repository Layer: Handles direct interaction with the database. Performs CRUD (Create, Read, Update, Delete) operations, abstracts database-specific details from the service layer.
- Database: The physical data storage that holds persistent data like orders, products, customers, etc.
Unit Testing Scope
The diagram emphasizes the focus areas for unit testing within this architecture. Unit Tests are performed on:
- Controller Methods: Testing API endpoints’ behaviour. Verify that the controller returns correct HTTP responses, status codes, and invokes service methods correctly.
- Service Methods: Verifying business logic, validations, calculations, and workflows without relying on the database.
- Repository Methods: Ensuring data access logic works correctly (often with integration or mock tests).
Mocking Dependencies
- When testing the Service layer, the Repository layer is mocked to isolate the business logic from actual database calls.
- When testing the Controller layer, the Service layer is mocked, allowing tests to focus solely on request handling and response behavior.
Why Mock Dependencies?
Real dependencies like databases, external APIs, or file systems can be:
- Slow or unreliable (network calls, downtime).
- Hard to set up or reset the state for every test.
- Non-deterministic (data can change over time).
By mocking, we can replace real dependencies with lightweight, predictable substitutes that return controlled data. This isolation:
- Enables fast and repeatable tests.
- Helps identify bugs in business logic rather than external dependencies.
- Encourages test-driven development (TDD).
Role of DTOs
DTOs are used to transfer data between client and server, particularly between the Controller and Service layers, helping maintain a clean separation between API contracts and internal models.
To keep the application clean and maintainable, and primarily to support robust unit testing, we organize our solution into two separate projects:
Web API Project (ProductCatalog.API):
This is where the actual application code lives:
- Controllers (handle HTTP requests)
- Business logic (services)
- Data models and DTOs
- Repositories (data access)
- EF Core DbContext, configuration, etc.
Test Project (ProductCatalog.Tests):
- This contains unit tests and integration tests only. Keeping tests separate prevents test code from being included in production builds and ensures the separation of concerns.
- It also enables you to test the API as an external client would, calling public methods and ensuring the real application logic works as expected.
Create a New Blank Solution
First, create a new Blank Solution named ProductCatalog. This serves as the container for our projects. To do so, open Visual Studio. From the start window, click Create a new project. In the Create a new project dialog, use the search box and type Blank Solution. Select Blank Solution and click the Next button as shown in the image below.
On the ‘Configure your new project’ screen, provide the Project name: ProductCatalog. Location: Choose or browse to the folder where you want your workspace, and click the Create button, as shown in the image below.
Visual Studio now opens with an empty solution called ProductCatalog (no projects yet) as shown in the image below.
Add an ASP.NET Core Web API Project
In Solution Explorer, right‐click the Solution ProductCatalog node and choose Add → New Project. In the Add a new project dialog, use the search box to type Web API. Select ASP.NET Core Web API and click the Next button as shown in the image below.
On the Configure your new project window, provide the project name: ProductCatalog.API, Location: It should default to your solution folder, and then click the Next button, as shown in the image below.
On the Additional Information windows, please do the following:
- Target Framework: .NET 8.0 (Long-term support)
- Authentication type: “No Authentication”
- Configure for HTTPS: Checked (default)
- Enable Docker: Leave unchecked.
- Enable OpenAPI support (Swagger): Checked (default is fine).
- Checked the use Controllers and do not use top level statements.
Finally, click the Create button, as shown in the image below.
This will create the ProductCatalog.API project under the ProductCatalog solution with the default folder and files.
Installing NuGet Packages:
As we will be working with an SQL Server database using the EF Core Code First Approach, please execute the following commands in the Visual Studio Package Manager Console.
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
Clean Up the Default API (Optional but recommended)
To prepare for a clean, testable structure, we can delete the sample controller and model:
- In Solution Explorer, expand ProductCatalog.API → Controllers. Right‐click WeatherForecastController.cs → Delete.
- Similarly, delete WeatherForecast.cs (if it exists).
This leaves your Web API project empty, ready for you to add your own Controllers, Services, and Repositories later. With this, your solution should look as follows:
Add a Unit Test Project (xUnit)
In Solution Explorer, right‐click the Solution ProductCatalog node again and choose Add → New Project. In the “Add a new project” dialog, use the search box to type “xunit”. Select the xUnit Test Project and click the Next button as shown in the image below.
On Configure your new project, provide the Project name: ProductCatalog.Tests, Location: It should default under your solution folder, and click the Next button as shown in the image below.
On the Additional information windows, please select Target Framework: .NET 8.0 (Long-term support) and click the Create button as shown in the image below.
This will create the ProductCatalog.Tests project and now the solution contains two projects as shown in the image below:
Add Project Reference from Test Project to API Project
The test project should reference the Web API project. To do this, in Solution Explorer, expand ProductCatalog.Tests. Right‐click on Dependencies → Add Project Reference. In the Reference Manager dialog, check the box next to ProductCatalog.API and click on the OK button as shown in the image below.
Now, ProductCatalog.Tests can import and test classes from ProductCatalog.API.
ASP.NET Core Web API Project Implementations:
Now, we will set up an Order Management Web API Project with Domain Models, DTOs, Controllers, Services, Repositories, and EF Core (Code First, and SQL Server). This will enable us to test real-world scenarios, including order creation, stock validation, discount calculations, and error handling.
Creating Domain Models
First, create a folder named Models in the ProductCatalog.API Project root directory where we will create all our Models.
Product.cs
Create a class file named Product.cs within the Models folder and then copy and paste the following code.
using System.ComponentModel.DataAnnotations.Schema; namespace ProductCatalog.API.Models { public class Product { public int Id { get; set; } public string Name { get; set; } = string.Empty; // Product Name [Column(TypeName = "decimal(12,2)")] public decimal Price { get; set; } // Current price public int Stock { get; set; } // Available stock for orders public string? Description { get; set; } // Description for catalog, marketing, etc. // Navigation property public List<OrderItem> OrderItems { get; set; } = new(); } }
Customer.cs
Create a class file named Customer.cs within the Models folder and then copy and paste the following code.
namespace ProductCatalog.API.Models { public class Customer { public int Id { get; set; } public string Name { get; set; } = null!; // Full name public string Email { get; set; } = null!; // Unique email address // Navigation property public List<Order> Orders { get; set; } = new(); } }
Order.cs
Create a class file named Order.cs within the Models folder and then copy and paste the following code.
using System.ComponentModel.DataAnnotations.Schema; namespace ProductCatalog.API.Models { public class Order { public int Id { get; set; } public int CustomerId { get; set; } public Customer? Customer { get; set; } public DateTime OrderDate { get; set; } // UTC date/time [Column(TypeName = "decimal(12,2)")] public decimal BaseAmount { get; set; } // Sum of all item totals before discount [Column(TypeName = "decimal(12,2)")] public decimal DiscountAmount { get; set; } // Total discount at order level [Column(TypeName = "decimal(12,2)")] public decimal TotalAmount { get; set; } // Final amount after discount // Navigation property public List<OrderItem> OrderItems { get; set; } = new(); } }
OrderItem.cs
Create a class file named OrderItem.cs within the Models folder and then copy and paste the following code.
using System.ComponentModel.DataAnnotations.Schema; namespace ProductCatalog.API.Models { public class OrderItem { public int Id { get; set; } public int OrderId { get; set; } public Order? Order { get; set; } public int ProductId { get; set; } public Product? Product { get; set; } public int Quantity { get; set; } // Units ordered [Column(TypeName = "decimal(12,2)")] public decimal UnitPrice { get; set; } // Price at time of order [Column(TypeName = "decimal(12,2)")] public decimal LineTotal { get; set; } // Calculated: UnitPrice * Quantity } }
Creating DTOs
Use DTOs for incoming requests and outgoing responses to avoid over-posting and ensure clean API contracts. Create a folder named DTOs in the ProductCatalog.API Project root directory where we will create all our DTOs.
OrderItemDTO.cs
Create a class file named OrderItemDTO.cs within the DTOs folder and then copy and paste the following code.
using System.ComponentModel.DataAnnotations; namespace ProductCatalog.API.DTOs { public class OrderItemDTO { [Required(ErrorMessage = "ProductId is required.")] public int ProductId { get; set; } [Range(1, int.MaxValue, ErrorMessage = "Quantity must be at least 1.")] public int Quantity { get; set; } } }
OrderCreateDTO.cs
Create a class file named OrderCreateDTO.cs within the DTOs folder and then copy and paste the following code.
using System.ComponentModel.DataAnnotations; namespace ProductCatalog.API.DTOs { public class OrderCreateDTO { [Required(ErrorMessage = "CustomerId is required.")] public int CustomerId { get; set; } [Required(ErrorMessage = "Order must contain at least one item.")] [MinLength(1, ErrorMessage = "Order must have at least one item.")] public List<OrderItemDTO> Items { get; set; } = new(); } }
OrderItemResponseDTO.cs
Create a class file named OrderItemResponseDTO.cs within the DTOs folder and then copy and paste the following code.
namespace ProductCatalog.API.DTOs { public class OrderItemResponseDTO { public int OrderItemId { get; set; } public int ProductId { get; set; } public string ProductName { get; set; } = string.Empty; public string? Description { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal LineTotal { get; set; } } }
OrderResponseDTO.cs
Create a class file named OrderResponseDTO.cs within the DTOs folder and then copy and paste the following code.
namespace ProductCatalog.API.DTOs { public class OrderResponseDTO { public int OrderId { get; set; } public int CustomerId { get; set; } public string? CustomerName { get; set; } public string? CustomerEmail { get; set; } public DateTime OrderDate { get; set; } public decimal BaseAmount { get; set; } public decimal Discount { get; set; } public decimal TotalAmount { get; set; } public List<OrderItemResponseDTO> OrderItems { get; set; } = new(); } }
Creating DbContext (EF Core Code First)
Create a folder named Data in the ProductCatalog.API Project root directory where we will create our DbContext class. Create a class file named ApplicationDbContext.cs within the Data folder and copy and paste the following code.
using Microsoft.EntityFrameworkCore; using ProductCatalog.API.Models; using Microsoft.EntityFrameworkCore.Diagnostics; namespace ProductCatalog.API.Data { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Seed Products modelBuilder.Entity<Product>().HasData( new Product { Id = 1, Name = "Laptop", Price = 60000m, Stock = 20, Description = "High performance laptop" }, new Product { Id = 2, Name = "Smartphone", Price = 25000m, Stock = 50, Description = "Latest smartphone" }, new Product { Id = 3, Name = "Wireless Mouse", Price = 1500m, Stock = 100, Description = "Ergonomic wireless mouse" } ); // Seed Customers modelBuilder.Entity<Customer>().HasData( new Customer { Id = 1, Name = "Pranaya Rout", Email = "pranaya@example.com" }, new Customer { Id = 2, Name = "Sneha Das", Email = "sneha@example.com" } ); } public DbSet<Product> Products { get; set; } public DbSet<Customer> Customers { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<OrderItem> OrderItems { get; set; } } }
Creating Repositories Interface and Implementations
Repositories abstract the data access logic. We need to use interfaces for testability (to enable mocking). Create a folder named Repositories in the Product Catalog.API Project root directory where we will create our repository interfaces and their concrete implementation classes.
IOrderRepository
Create a class file named IOrderRepository.cs within the Repositories folder and then copy and paste the following code.
using ProductCatalog.API.Models; namespace ProductCatalog.API.Repositories { public interface IOrderRepository { Task<Order?> GetByIdAsync(int id); Task AddAsync(Order order); Task SaveChangesAsync(); } }
IProductRepository
Create a class file named IProductRepository.cs within the Repositories folder and then copy and paste the following code.
using ProductCatalog.API.Models; namespace ProductCatalog.API.Repositories { public interface IProductRepository { Task<Product?> GetByIdAsync(int id); Task AddAsync(Product product); Task SaveChangesAsync(); } }
ICustomerRepository
Create a class file named ICustomerRepository.cs within the Repositories folder and then copy and paste the following code.
using ProductCatalog.API.Models; namespace ProductCatalog.API.Repositories { public interface ICustomerRepository { Task<Customer?> GetByIdAsync(int id); } }
OrderRepository
Create a class file named OrderRepository.cs within the Repositories folder and then copy and paste the following code.
using ProductCatalog.API.Data; using ProductCatalog.API.Models; using Microsoft.EntityFrameworkCore; namespace ProductCatalog.API.Repositories { public class OrderRepository : IOrderRepository { private readonly ApplicationDbContext _dbContext; public OrderRepository(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<Order?> GetByIdAsync(int id) { return await _dbContext.Orders .Include(o => o.OrderItems) .ThenInclude(oi => oi.Product) .Include(o => o.Customer) .FirstOrDefaultAsync(o => o.Id == id); } public async Task AddAsync(Order order) { await _dbContext.Orders.AddAsync(order); } public async Task SaveChangesAsync() { await _dbContext.SaveChangesAsync(); } } }
ProductRepository
Create a class file named ProductRepository.cs within the Repositories folder and then copy and paste the following code.
using ProductCatalog.API.Data; using ProductCatalog.API.Models; namespace ProductCatalog.API.Repositories { public class ProductRepository : IProductRepository { private readonly ApplicationDbContext _dbContext; public ProductRepository(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<Product?> GetByIdAsync(int id) { return await _dbContext.Products.FindAsync(id); } public async Task AddAsync(Product product) { await _dbContext.Products.AddAsync(product); } public async Task SaveChangesAsync() { await _dbContext.SaveChangesAsync(); } } }
CustomerRepository
Create a class file named CustomerRepository.cs within the Repositories folder and then copy and paste the following code.
using ProductCatalog.API.Data; using ProductCatalog.API.Models; namespace ProductCatalog.API.Repositories { public class CustomerRepository : ICustomerRepository { private readonly ApplicationDbContext _dbContext; public CustomerRepository(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<Customer?> GetByIdAsync(int id) { return await _dbContext.Customers.FindAsync(id); } } }
Creating Custom Exceptions
First, create a folder named Exceptions in the ProductCatalog.API Project root directory. Inside the Exceptions folder, create a class file named NotFoundException.cs and then copy and paste the following code:
namespace ProductCatalog.API.Exceptions { public class NotFoundException : Exception { public NotFoundException(string message) : base(message) { } } }
Creating Service Interface and Implementation
The service layer contains business logic, including order creation, stock validation, total and discount calculation, error handling, and other related functions. First, create a folder named Services in the Product Catalog.API Project root directory where we will create our Service interfaces and their concrete implementation classes. For simplicity, only the Order service is implemented here; you can similarly implement other services.
IOrderService
Create a class file named IOrderService.cs within the Services folder and then copy and paste the following code.
using ProductCatalog.API.DTOs; namespace ProductCatalog.API.Services { public interface IOrderService { Task<OrderResponseDTO?> CreateOrderAsync(OrderCreateDTO orderCreateDto); Task<OrderResponseDTO?> GetOrderByIdAsync(int id); } }
OrderService
Create a class file named OrderService.cs within the Services folder and then copy and paste the following code.
using ProductCatalog.API.DTOs; using ProductCatalog.API.Exceptions; using ProductCatalog.API.Models; using ProductCatalog.API.Repositories; namespace ProductCatalog.API.Services { public class OrderService : IOrderService { private readonly IOrderRepository _orderRepository; private readonly IProductRepository _productRepository; private readonly ICustomerRepository _customerRepository; public OrderService( IOrderRepository orderRepository, IProductRepository productRepository, ICustomerRepository customerRepository) { _orderRepository = orderRepository; _productRepository = productRepository; _customerRepository = customerRepository; } public async Task<OrderResponseDTO?> CreateOrderAsync(OrderCreateDTO orderCreateDto) { var customer = await _customerRepository.GetByIdAsync(orderCreateDto.CustomerId); if (customer == null) throw new NotFoundException($"Customer with id {orderCreateDto.CustomerId} not found."); if (orderCreateDto.Items == null || !orderCreateDto.Items.Any()) throw new ArgumentException("Order must have at least one item."); try { var order = new Order { CustomerId = customer.Id, OrderDate = DateTime.UtcNow, OrderItems = new List<OrderItem>() }; decimal baseAmount = 0; foreach (var itemDto in orderCreateDto.Items) { var product = await _productRepository.GetByIdAsync(itemDto.ProductId); if (product == null) throw new NotFoundException($"Product with id {itemDto.ProductId} not found."); if (itemDto.Quantity <= 0) throw new ArgumentException("Quantity must be greater than zero."); if (product.Stock < itemDto.Quantity) throw new InvalidOperationException($"Not enough stock for product {product.Name}. Available: {product.Stock}, requested: {itemDto.Quantity}"); decimal lineTotal = product.Price * itemDto.Quantity; order.OrderItems.Add(new OrderItem { ProductId = product.Id, Quantity = itemDto.Quantity, UnitPrice = product.Price, LineTotal = lineTotal }); baseAmount += lineTotal; // Deduct stock product.Stock -= itemDto.Quantity; } order.BaseAmount = baseAmount; // Calculate discount at order level order.DiscountAmount = CalculateDiscount(baseAmount); order.TotalAmount = baseAmount - order.DiscountAmount; await _orderRepository.AddAsync(order); await _productRepository.SaveChangesAsync(); await _orderRepository.SaveChangesAsync(); return MapToOrderDto(order, customer); } catch { throw; // Let upper layer handle exceptions } } // Dummy discount logic: 5% discount for orders above 5000 private decimal CalculateDiscount(decimal baseAmount) { if (baseAmount > 5000) return baseAmount * 0.05m; return 0; } public async Task<OrderResponseDTO?> GetOrderByIdAsync(int id) { var order = await _orderRepository.GetByIdAsync(id); if (order == null) return null; var customer = order.Customer ?? await _customerRepository.GetByIdAsync(order.CustomerId); return MapToOrderDto(order, customer); } private OrderResponseDTO MapToOrderDto(Order order, Customer? customer) { return new OrderResponseDTO { OrderId = order.Id, CustomerId = order.CustomerId, CustomerName = customer?.Name ?? string.Empty, CustomerEmail = customer?.Email ?? string.Empty, OrderDate = order.OrderDate, BaseAmount = order.BaseAmount, Discount = order.DiscountAmount, TotalAmount = order.TotalAmount, OrderItems = order.OrderItems.Select(item => new OrderItemResponseDTO { OrderItemId = item.Id, ProductId = item.ProductId, ProductName = item.Product?.Name ?? string.Empty, Description = item.Product?.Description ?? string.Empty, Quantity = item.Quantity, UnitPrice = item.UnitPrice, LineTotal = item.LineTotal }).ToList() }; } } }
Creating Order API Controller
Create an Empty API Controller named OrderController within the Controllers folder and then copy and paste the following code.
using Microsoft.AspNetCore.Mvc; using ProductCatalog.API.DTOs; using ProductCatalog.API.Services; using ProductCatalog.API.Exceptions; namespace ProductCatalog.API.Controllers { [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly IOrderService _service; public OrdersController(IOrderService service) { _service = service; } [HttpPost] public async Task<IActionResult> CreateOrder([FromBody] OrderCreateDTO orderCreateDto) { if (!ModelState.IsValid) return BadRequest(ModelState); try { var order = await _service.CreateOrderAsync(orderCreateDto); return Ok(order); } catch (NotFoundException ex) { return NotFound(new { message = ex.Message }); } catch (ArgumentException ex) { return BadRequest(new { message = ex.Message }); } catch (InvalidOperationException ex) { return BadRequest(new { message = ex.Message }); } catch (Exception) { return StatusCode(500, new { message = "An unexpected error occurred." }); } } [HttpGet("{id}")] public async Task<IActionResult> GetOrder(int id) { var order = await _service.GetOrderByIdAsync(id); if (order == null) return NotFound(); return Ok(order); } } }
Configure Connection String in appsettings.json:
We will store the connection string in the appsettings.json file. So, please modify the appsettings.json file as follows.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=OrdersDB;Trusted_Connection=True;TrustServerCertificate=True;" } }
Modifying Program.cs Class:
ASP.NET Core’s built-in DI container registers:
- DbContext with SQL Server connection string.
- Repositories and Services with scoped lifetimes.
- JSON serializer options are configured to disable camel casing, respecting your DTO property naming.
- Swagger is enabled for API testing and documentation.
So, please modify the Program class as follows:
using Microsoft.EntityFrameworkCore; using ProductCatalog.API.Data; using ProductCatalog.API.Repositories; using ProductCatalog.API.Services; namespace ProductCatalog.API { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers() .AddJsonOptions(options => { //Disable camel case options.JsonSerializerOptions.PropertyNamingPolicy = null; }); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddScoped<IOrderRepository, OrderRepository>(); builder.Services.AddScoped<IProductRepository, ProductRepository>(); builder.Services.AddScoped<ICustomerRepository, CustomerRepository>(); builder.Services.AddScoped<IOrderService, OrderService>(); 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(); } } }
Creating and Applying Database Migration:
In Visual Studio, open the Package Manager Console and execute the Add-Migration and Update-Database commands as follows to generate the Migration file. Then, apply the Migration file to create the OrdersDB database and the required tables. When executing this command, please select Product Catalog.API project.
Once you execute the above commands, verify the database, and you should see the OrdersDB database with the required table as shown in the image below.
Test API Endpoints
Use Swagger UI or tools like Postman to POST orders and GET order details. Try invalid requests (wrong customer, out-of-stock, etc.) to see how error messages and validation behave.
Create Order
URL: POST http://localhost:{port}/api/orders Request Body: { "CustomerId": 1, "Items": [ { "ProductId": 1, "Quantity": 2 }, { "ProductId": 2, "Quantity": 3 } ] }
Get Order By Id
URL: GET http://localhost:{port}/api/orders/{id} Example: GET http://localhost:5000/api/orders/1 Request Body: None
Validation Error Example (Create Order with errors)
URL: POST http://localhost:{port}/api/orders Request Body: { "CustomerId": 0, "Items": [ { "ProductId": 999, "Quantity": 0 } ] }
Note: Please replace {port} with your actual API port number.
Importance of Unit Testing in ASP.NET Core APIs
Building an ASP.NET Core Web API with a clean architecture, modularized into controllers, services, repositories, data transfer objects (DTOs), and models, lays a solid foundation for scalable and maintainable software. This design makes unit testing straightforward by isolating components and enabling mocking of dependencies, which is essential because a wide range of clients consume APIs and must be reliable. Unit testing ensures correctness, robustness, and confidence while facilitating ongoing development and refactoring.
- Multi-Client Usage: Your API might serve websites, mobile apps, and third-party clients, all of which rely on consistent, error-free responses.
- Early Bug Detection: Unit tests identify bugs in business rules or response handling before integration or deployment.
- Confidence in Code Changes: When refactoring or adding new features, tests ensure you don’t break existing functionality.
- Better Documentation: Well-written tests serve as executable documentation for API behaviour.
Now that we have a solid project environment set up, with a clean architecture, robust domain models, data validation, and transactional business logic, we are ready to write effective unit tests. The next step involves creating comprehensive test cases using xUnit and mocking dependencies with libraries like Moq to verify each layer’s behaviour in isolation. This will help ensure your application is reliable, maintainable, and bug-free as it grows.
In the next article, I will discuss Unit Testing the Service Layer of our ASP.NET Core Web API Project. In this article, I created a Real-time ASP.NET Core Project for Unit Testing. I hope you enjoy this article on Real-time ASP.NET Core Project Development for Unit Testing.
Want to see this in action?
Check out our latest step-by-step video tutorial on Real-Time ASP.NET Core Project Development and Unit Testing!
Watch now on YouTube: https://www.youtube.com/watch?v=h2B5xA9pALk
Get practical insights, live demonstrations, and best practices—explained in simple language by Pranaya Rout. Don’t miss it!