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.
Name the domain outcome before writing the response
A method can execute successfully while discovering that its input is invalid or its requested entity is absent. Treat those findings as explicit outcomes rather than returning 200 for every completed branch. The portable companion uses a dictionary and three identifiers to keep the decision visible without HTTP dependencies. The external project maps the same categories to real ASP.NET Core typed results. This separation lets a domain policy be reused in another adapter while preserving a precise HTTP surface. It also makes the endpoint's alternatives visible to a reader before they inspect every branch of its body.
Follow invalid, found and missing inputs
The console's identifier zero is nonpositive, so it prints invalid. Identifier one exists with the value rune and prints found:rune. Identifier nine does not exist and prints missing. The external endpoint returns an Ok result containing an item for one, a NotFound result for nine and a Problem result with status 400 for zero. Those are three deliberately independent inputs. The example is a tiny fixed catalog; its successful result means this known item was selected, not that a database query, authorization check or persisted mutation happened behind the response.
Typed alternatives expose supported result shapes
The handler declares a union of Ok<Item>, NotFound and ProblemHttpResult. Each returned typed helper belongs to that declared set. These types assist compile-time expression of the endpoint's alternatives and contribute useful response metadata, but they do not make the domain predicate correct automatically. A future branch or changed status must still match the intended API contract. Avoid hiding all alternatives behind an opaque successful wrapper merely to give the handler one apparent return type. The status and representation are meaningful parts of the result another client has to interpret.
ProblemDetails describes a problem without exposing internals
The invalid-identifier branch deliberately sets status 400 and a stable title. Its content type is application/problem+json, while the successful branch returns the item representation. The not-found result in this sample need not contain the same body shape. A standardized problem format is useful for clients, but it does not justify leaking stack traces, configuration values or credentials as details. Likewise, registering ProblemDetails services does not turn every conceivable failure into this exact branch. Decide where unexpected exceptions are translated separately from known input and lookup outcomes, and inspect actual status and content type in tests.
Practice: prove each branch independently
Create an independent table for identifiers minus one, zero, one and two, with expected status and body category. Request each against the real server and compare parsed properties rather than localized exception messages. Then change the catalog's existing identifier to two and adjust only the independently intended success and missing cases. Keep the invalid-identifier rows unchanged. This tests that an implementation change has not accidentally merged absence with invalidity. The portable companion can validate the lookup categories, while the external requests must still validate that those categories map to the expected HTTP results.
Practice: add one deliberate domain conflict
Extend the endpoint with a separately specified temporarily unavailable item and choose an explicit conflict outcome for it. Add that typed alternative to the signature rather than silently changing a successful item's status. Write the expected problem title, status and client action before implementing the branch. Then inspect the generated metadata if OpenAPI is added later. The hint is to choose a domain reason that differs from both missing and invalid input, so its test cannot pass merely because an earlier generic guard rejected everything. Keep the example read-only instead of inventing a purchase or reservation side effect.
Verify the contract seen by a client
A useful endpoint review can answer which statuses are possible, which carry representations, which representation schema applies and whether each category is ordinary application behavior or unexpected failure. The three original requests provide a small executable basis for those answers. Typed signatures help describe the supported surface, while request tests confirm the actual adapter. Keep those forms of evidence complementary. The console intentionally cannot prove HTTP formatting, and a matching HTTP transcript cannot exhaustively prove a larger domain policy. Preserve both when adding more items, richer validation or an external repository to this small example.
- Use statuses and body shapes intentionally rather than treating every completed method as success.
- Inspect problem content type and stable properties instead of matching localized diagnostic text.
Typed results and ProblemDetails — 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 each of /items/1, /items/9 and /items/0 with curl -i; compare status and content type.
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 ↗using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder(args); builder.Services.AddProblemDetails(); var app = builder.Build();
app.MapGet("/items/{id:int}", Results<Ok<Item>, NotFound, ProblemHttpResult> (int id) => {
if (id <= 0) return TypedResults.Problem(statusCode: 400, title: "Identifier must be positive");
return id == 1 ? TypedResults.Ok(new Item(1, "rune")) : TypedResults.NotFound();
});
app.Run();
public sealed record Item(int Id, string Name);
Expected result
GET /items/1 is 200 JSON; /items/9 is 404; /items/0 is 400 application/problem+json with the explicit title.
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;
var catalog = new Dictionary<int, string> { [1] = "rune" };
foreach (int id in new[] { 0, 1, 9 }) Console.WriteLine(id <= 0 ? "invalid" : catalog.TryGetValue(id, out string? name) ? "found:" + name : "missing");
C# practice output
invalid found:rune missing
Typed results describe response alternatives; the domain still decides which branch applies.