Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
Run the right program in the right environment
This lesson deliberately contains two programs with different responsibilities. The portable C# companion tests a quantity rule over values that already exist. The complete external project starts real ASP.NET Core 10 and lets routing, binding and response execution operate on HTTP messages. SharpRune's embedded console does not run Kestrel. Save the external files together, restore from the stated public NuGet source and start the project on loopback before using curl. A successful console run establishes the predicate's behavior; only requests to the running server establish how that predicate connects to an HTTP contract.
Route matching happens before the domain decision
The pattern /items/{id:int} requires a path segment that satisfies the integer route constraint. A request to /items/nope does not reach this handler and returns 404 in this application. That constraint is a matching decision, not a positive-identifier rule. Negative integers still satisfy the integer shape. The handler receives id from the route and an optional quantity from query binding. Explicit route patterns and scalar parameters keep the example small, but a growing API should make its intended binding sources clear rather than assuming every string or complex object comes from the same place.
Trace the portable values and real request outcomes
The console supplies null, three and zero to Choose. Null becomes the chosen default one; three is within the inclusive range; zero is rejected. Its output is therefore 1, 3, rejected. On the external server, /items/7 returns id seven and quantity one, while ?quantity=3 returns quantity three. A query containing quantity=abc fails integer binding and returns 400 before the application range predicate can accept it. A bound zero reaches the predicate and receives a 400 ProblemDetails result. The identical status does not make these two failure causes identical.
Defaults and required data are explicit API choices
Making quantity nullable allows an omitted query value and gives the application a visible place to choose its default. A required nonnullable parameter without an appropriate default would have a different missing-input contract. Decide whether omitted, empty, malformed and out-of-range values should be equivalent before publishing an API. The sample intentionally limits quantity but does not validate the identifier's business range or check a catalog. Its 200 response is only an echo of accepted request values. Do not describe that response as proof that item seven exists or that an order was created.
Practice: distinguish matching from conversion
First remove only the :int constraint from the route while keeping the handler parameter typed as integer. Predict the result for /items/nope, then test it against a freshly built external server. Compare that observation with the original constrained route. Keep /items/7?quantity=abc as a separate control case. The useful hint is that matching an unconstrained path can select the handler before conversion fails. Do not change both the parameter type and route pattern at once, because that would make it unclear which boundary explains the changed response.
Practice: specify both range edges and omission
Add independent requests for omitted quantity, quantity one, quantity ten, zero and eleven. Write the expected accepted quantity or rejection before executing them. Then change the default from one to two without changing the accepted range, updating the portable companion's null case independently of the explicit-value cases. Inspect status and response content rather than comparing localized error strings. These changes exercise two separate contracts: the value chosen when input is absent, and the values allowed when input is present. Neither contract should be inferred from a convenient client default.
Keep the evidence for each layer separate
Review the endpoint as a sequence: method and route select a handler, binding supplies values, the application chooses a default and applies its range rule, then a real framework result writes the response. Each layer has independent counterexamples. Preserve the simple portable checks for domain behavior, but add external request checks when changing attributes, route patterns, parameter nullability or response results. The supplied project uses no database and persists nothing. Its strength is that every observed outcome comes from an actual ASP.NET pipeline while its narrower domain rule remains easy to inspect and run independently.
- Keep a route mismatch, a conversion failure and a domain rejection as separate test cases.
- Document nullable input and chosen defaults alongside the accepted value range.
Minimal routes and explicit binding — real ASP.NET Core 10 project
Set up the SDK project
- Save all files into one new empty directory, preserving their relative paths. Install the official .NET 10 SDK if it is not already available.
- From that directory run: dotnet restore WebLab.csproj --source https://api.nuget.org/v3/index.json
- Start only on loopback: ASPNETCORE_ENVIRONMENT=Development dotnet run --project WebLab.csproj --no-launch-profile --urls http://127.0.0.1:5080
- Use another terminal for the requests below, then press Ctrl+C in this project’s terminal to stop its server.
- Request: curl -i "http://127.0.0.1:5080/items/7?quantity=3"
- Compare malformed query quantity=abc with the nonmatching path /items/nope.
Download complete SDK project ↗
Project files
WebLab.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Program.cs
Download file ↗var builder = WebApplication.CreateBuilder(args); var app = builder.Build();
app.MapGet("/items/{id:int}", (int id, int? quantity) => {
int amount = quantity ?? 1;
return amount is >= 1 and <= 10 ? Results.Ok(new { id, quantity = amount }) : Results.Problem(statusCode: 400, title: "Quantity must be 1 through 10");
});
app.Run();
Expected result
GET /items/7 returns 200 with id 7 and quantity 1; ?quantity=3 returns quantity 3; ?quantity=abc is 400; /items/nope is 404; quantity=0 is a 400 application problem.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
foreach (int? amount in new int?[] { null, 3, 0 }) Console.WriteLine(Choose(amount)?.ToString(CultureInfo.InvariantCulture) ?? "rejected");
static int? Choose(int? amount) { int value = amount ?? 1; return value is >= 1 and <= 10 ? value : null; }
C# practice output
1 3 rejected
A route constraint rejects a path shape; a bound integer still needs an application range rule.