One request, followed end to end
This is the real output of bnl-dotnet endpoint OrdersController.Checkout against the five files
in the tabs below. The diagram follows the checkout from the controller through pricing, inventory and payment -
loops, a switch, a try/catch, early returns and every call into
your own code, spliced into one picture. Scroll to zoom, drag to pan.
Controllers/OrdersController.cs1using Microsoft.AspNetCore.Mvc;
2using OrdersApi.Models;
3using OrdersApi.Services;
4
5namespace OrdersApi.Controllers;
6
7[ApiController]
8[Route("api/[controller]")]
9public class OrdersController(PricingService pricing, InventoryService inventory, PaymentService payments) : ControllerBase
10{
11 [HttpPost("checkout")]
12 public async Task<IActionResult> Checkout(CheckoutRequest request)
13 {
14 if (request.Lines.Count == 0)
15 {
16 return BadRequest("The basket is empty.");
17 }
18
19 decimal total = pricing.Calculate(request);
20
21 bool reserved = inventory.TryReserve(request.Lines);
22
23 if (!reserved)
24 {
25 return Conflict("One or more items are out of stock.");
26 }
27
28 try
29 {
30 string receipt = await payments.ChargeAsync(request.CustomerId, total);
31 return Ok(new { receipt, total });
32 }
33 catch (PaymentDeclinedException)
34 {
35 inventory.Release(request.Lines);
36 return StatusCode(StatusCodes.Status402PaymentRequired, "The payment was declined.");
37 }
38 }
39}
1using OrdersApi.Models;
2
3namespace OrdersApi.Services;
4
5public class PricingService
6{
7 private const decimal TaxRate = 0.20m;
8
9 public decimal Calculate(CheckoutRequest request)
10 {
11 decimal subtotal = 0;
12
13 foreach (OrderLine line in request.Lines)
14 {
15 subtotal += line.UnitPrice * line.Quantity;
16 }
17
18 decimal discount = DiscountFor(subtotal, request.Coupon);
19
20 return Math.Round((subtotal - discount) * (1 + TaxRate), 2);
21 }
22
23 private static decimal DiscountFor(decimal subtotal, string? coupon)
24 {
25 switch (coupon)
26 {
27 case "WELCOME10":
28 return subtotal * 0.10m;
29
30 case "BULK":
31 return subtotal > 500 ? subtotal * 0.15m : 0;
32
33 default:
34 return 0;
35 }
36 }
37}
1using OrdersApi.Models;
2
3namespace OrdersApi.Services;
4
5public class InventoryService
6{
7 private readonly Dictionary<string, int> _stock = new() { ["WIDGET"] = 25, ["GADGET"] = 8 };
8
9 public bool TryReserve(IReadOnlyList<OrderLine> lines)
10 {
11 bool inStock = lines.All(line => _stock.GetValueOrDefault(line.Sku) >= line.Quantity);
12
13 if (!inStock)
14 {
15 return false;
16 }
17
18 foreach (OrderLine line in lines)
19 {
20 _stock[line.Sku] -= line.Quantity;
21 }
22
23 return true;
24 }
25
26 public void Release(IReadOnlyList<OrderLine> lines)
27 {
28 foreach (OrderLine line in lines)
29 {
30 _stock[line.Sku] += line.Quantity;
31 }
32 }
33}
1using System.Net;
2using OrdersApi.Models;
3
4namespace OrdersApi.Services;
5
6public class PaymentService(HttpClient gateway)
7{
8 public async Task<string> ChargeAsync(string customerId, decimal amount)
9 {
10 if (amount <= 0)
11 {
12 throw new ArgumentOutOfRangeException(nameof(amount));
13 }
14
15 HttpResponseMessage response = await gateway.PostAsJsonAsync("/charges", new { customerId, amount });
16
17 if (response.StatusCode == HttpStatusCode.PaymentRequired)
18 {
19 throw new PaymentDeclinedException();
20 }
21
22 response.EnsureSuccessStatusCode();
23
24 return await response.Content.ReadAsStringAsync();
25 }
26}
1namespace OrdersApi.Models;
2
3public record OrderLine(string Sku, int Quantity, decimal UnitPrice);
4
5public record CheckoutRequest(string CustomerId, IReadOnlyList<OrderLine> Lines, string? Coupon);
6
7public class PaymentDeclinedException : Exception;