Learn / .NET API Engineering Path
Modern C# API Foundations
Use records, nullable references, async methods, and options to establish reliable service contracts.
Course: .NET API Engineering Path. Level: Advanced. Topic: Typed backend.
Stage: basic - Basic foundation - C# and ASP.NET Core service contracts. Start with language and host fundamentals that make API contracts explicit before any framework shortcuts.
Outcomes
- Model request shapes with records.
- Propagate cancellation tokens.
- Validate options at startup.
- Distinguish C#, .NET, the SDK, the runtime, and ASP.NET Core.
- Explain what happens when a C# project builds and runs.
- Recognize common application types in the .NET ecosystem.
- Use value types, reference types, nullable annotations, and generics correctly.
- Write expressive control flow with pattern matching.
- Choose exceptions, results, and guards for different error situations.
- Design classes that protect invariants.
- Use interfaces to describe capabilities and dependencies.
- Use records for immutable data and value-like equality.
- Explain what async and await do in C#.
- Propagate CancellationToken through async call chains.
- Avoid sync-over-async, fire-and-forget bugs, and unnecessary Task.Run usage.
Concepts
- record
- nullable reference type
- async Task
- options pattern
- C# and .NET Orientation
- The platform pieces
- From source to execution
- Modern C# and .NET foundations
- Guided practice
- Syntax and Type System
- Types communicate intent
- Generics and pattern matching
- Object-Oriented Design and Records
- Classes own behavior and invariants
- Records are data models, not service objects
- Async and Await
- Async is about waiting efficiently
- Composition and failure
Concept flow
Show how c# and asp.net core service contracts moves from trigger to implementation outcome in .NET APIs.
- Request DTO
- Endpoint
- Application service
- Options
- Logger
Session flow
- Model record (concept, 9 min) — Name the decisions behind record before writing code.
- Model request shapes with records.
- Explain where record belongs in order management API.
- Build the vertical slice (walkthrough, 15 min) — Implement the smallest useful slice in Orders/CreateOrder.cs.
- Propagate cancellation tokens.
- Connect nullable reference type to a working example.
- Verify and harden (exercise, 10 min) — Thread CancellationToken through a service method.
- Validate options at startup.
- Record one risk or follow-up before moving on.
- The C# and .NET Map: The platform pieces (concept, 28 min) — C# is the language. .NET is the developer platform. The SDK contains compilers, templates, build tools, and the CLI. The runtime executes compiled assemblies. ASP.NET Core is the web framework built on top of the same runtime. A single solution can contain console apps, web APIs, worker services, test projects, and class libraries.
- Language: C# syntax, type system, and compiler rules.
- Base class library: strings, collections, dates, I/O, networking, threading, and more.
- Runtime: JIT compilation, garbage collection, assembly loading, and diagnostics.
- Frameworks: ASP.NET Core, EF Core, MAUI, Orleans, Aspire, and worker services.
- Retained source example: Inspect the installed SDKs
dotnet --list-sdks
dotnet --info
The first command lists installed SDK versions. The second prints runtime, RID, workload, and environment details that are useful when debugging builds.
- The C# and .NET Map: From source to execution (walkthrough, 28 min) — A C# file is compiled into an assembly containing Intermediate Language and metadata. At runtime, the CLR loads assemblies, verifies types, JIT-compiles methods to native code, manages memory, and coordinates exceptions and async continuations.
- Retained source example: Smallest useful program
using System;
Console.WriteLine("Hello from .NET");
Console.WriteLine(Environment.Version);
Expected output: Hello from .NET
<runtime version>
Modern C# supports top-level statements, so a simple program does not need an explicit Program class. The compiler still generates one behind the scenes.
- The C# and .NET Map: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Install SDK versions intentionally and document the expected version with global.json when consistency matters.
- Practice: Prefer current SDK tooling even when targeting an older supported runtime.
- Practice: Use one solution file to group related application, library, and test projects.
- Avoid: Confusing the SDK with the runtime and installing only the runtime on a development machine.
- Avoid: Assuming ASP.NET Core is separate from .NET instead of a framework within the .NET ecosystem.
- Avoid: Treating top-level statements as a different execution model rather than compiler convenience.
- The C# and .NET Map: references (review, 2 min) — Original references retained from the legacy library.
- .NET documentation: https://learn.microsoft.com/dotnet/
- C# documentation: https://learn.microsoft.com/dotnet/csharp/
- Values, Nullability, Generics, and Patterns: Types communicate intent (concept, 45 min) — C# is statically typed. Value types such as int, bool, decimal, DateTime, and structs generally store their data directly. Reference types such as string, arrays, classes, and delegates refer to objects. Nullable reference types let the compiler warn when a reference may be missing.
- Retained source example: Nullable reference types
static int NameLength(string? name)
{
if (string.IsNullOrWhiteSpace(name))
{
return 0;
}
return name.Length;
}
The question mark on string? means callers may pass null. The guard narrows the type so the compiler knows name is not null after the if block.
- Values, Nullability, Generics, and Patterns: Generics and pattern matching (walkthrough, 45 min) — Generics let you write reusable code without losing type safety. Pattern matching lets you branch on shape, type, constants, and property values without long chains of casts.
- Retained source example: Generic result with pattern matching
public abstract record Result<T>;
public sealed record Success<T>(T Value) : Result<T>;
public sealed record Failure<T>(string Error) : Result<T>;
static string Describe(Result<int> result) => result switch
{
Success<int> { Value: > 0 } ok => $"Positive: {ok.Value}",
Success<int> ok => $"Number: {ok.Value}",
Failure<int> failed => $"Error: {failed.Error}",
_ => "Unknown"
};
The switch expression handles both type and property patterns. Records make small immutable data shapes concise, and generics keep the result reusable.
- Values, Nullability, Generics, and Patterns: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Enable nullable reference types for new projects.
- Practice: Use decimal for money-like values, not double.
- Practice: Prefer clear guard clauses near the top of methods.
- Practice: Use pattern matching when it improves readability, not to compress unrelated logic.
- Avoid: Using null-forgiving operators to hide warnings instead of fixing contracts.
- Avoid: Catching Exception too broadly and losing actionable failure information.
- Avoid: Using object or dynamic when a generic type would be safer.
- Values, Nullability, Generics, and Patterns: references (review, 2 min) — Original references retained from the legacy library.
- C# type system: https://learn.microsoft.com/dotnet/csharp/fundamentals/types/
- Pattern matching: https://learn.microsoft.com/dotnet/csharp/fundamentals/functional/pattern-matching
- Classes, Interfaces, Composition, and Records: Classes own behavior and invariants (concept, 43 min) — A class should not merely expose public fields. It should protect valid state and place behavior near the data it changes. Constructors establish required values, methods express operations, and private members hide implementation details.
- Retained source example: Class with an invariant
public sealed class InventoryItem
{
public InventoryItem(string sku, int quantity)
{
if (string.IsNullOrWhiteSpace(sku)) throw new ArgumentException("SKU is required", nameof(sku));
if (quantity < 0) throw new ArgumentOutOfRangeException(nameof(quantity));
Sku = sku;
Quantity = quantity;
}
public string Sku { get; }
public int Quantity { get; private set; }
public void Reserve(int amount)
{
if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
if (amount > Quantity) throw new InvalidOperationException("Insufficient inventory");
Quantity -= amount;
}
}
The item can never be created with invalid quantity, and reservation rules are enforced in one place.
- Classes, Interfaces, Composition, and Records: Records are data models, not service objects (walkthrough, 43 min) — Records are ideal for commands, events, DTOs, and small immutable value objects. They provide value-based equality and concise copying with with-expressions. They are less suitable for mutable services with hidden state.
- Retained source example: Record value object
public sealed record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
{
throw new InvalidOperationException("Currency mismatch");
}
return this with { Amount = Amount + other.Amount };
}
}
The with-expression creates a copy rather than mutating the original value.
- Classes, Interfaces, Composition, and Records: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Prefer composition and interfaces for behavior that changes independently.
- Practice: Make domain objects responsible for their invariants.
- Practice: Use records for immutable data, commands, events, DTOs, and value objects.
- Practice: Seal classes by default when inheritance is not part of the design.
- Avoid: Creating an interface for every class without a real variation point.
- Avoid: Using public setters everywhere and scattering validation across callers.
- Avoid: Using inheritance to share two lines of code while creating long-term coupling.
- Classes, Interfaces, Composition, and Records: references (review, 2 min) — Original references retained from the legacy library.
- Classes and objects: https://learn.microsoft.com/dotnet/csharp/fundamentals/tutorials/classes
- Records: https://learn.microsoft.com/dotnet/csharp/fundamentals/types/records
- Tasks, Cancellation, and Async Error Handling: Async is about waiting efficiently (concept, 45 min) — Async/await does not magically make CPU work faster. It lets a thread return to the pool while an I/O operation is pending. That is why async shines for HTTP, databases, files, queues, and timers.
- Retained source example: Async HTTP call
public sealed class WeatherClient(HttpClient httpClient)
{
public async Task<Forecast?> GetForecastAsync(string city, CancellationToken cancellationToken)
{
var path = $"/weather/{Uri.EscapeDataString(city)}";
return await httpClient.GetFromJsonAsync<Forecast>(path, cancellationToken);
}
}
The method returns a Task that completes later. The cancellation token lets callers abandon work when a request is aborted or a timeout expires.
- Tasks, Cancellation, and Async Error Handling: Composition and failure (walkthrough, 45 min) — Await rethrows exceptions from the asynchronous operation. Task.WhenAll composes independent operations and finishes when all have completed. Cancellation should be cooperative and should not be treated as an unexpected crash.
- Retained source example: Run independent calls together
var customerTask = customerClient.GetAsync(customerId, cancellationToken);
var ordersTask = orderClient.ListRecentAsync(customerId, cancellationToken);
await Task.WhenAll(customerTask, ordersTask);
var profile = new CustomerProfile(
await customerTask,
await ordersTask);
Start independent work before awaiting so the operations overlap. Keep the shared cancellation token flowing.
- Tasks, Cancellation, and Async Error Handling: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Use async all the way through the call chain.
- Practice: Accept and pass CancellationToken for I/O operations.
- Practice: Use Task.WhenAll for independent operations.
- Practice: Use ValueTask only when a measured hot path benefits from it.
- Avoid: Using async void except for event handlers.
- Avoid: Wrapping I/O-bound work in Task.Run instead of using native async APIs.
- Avoid: Forgetting to observe or log failures from background tasks.
- Tasks, Cancellation, and Async Error Handling: references (review, 2 min) — Original references retained from the legacy library.
- Asynchronous programming with async and await: https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/
- Task-based asynchronous pattern: https://learn.microsoft.com/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap
Code example
C# in Orders/CreateOrder.cs.
public sealed record CreateOrderRequest(Guid CustomerId, int TotalCents)
{
public bool IsValid => CustomerId != Guid.Empty && TotalCents > 0;
}
public interface IOrders
{
Task<OrderDto?> FindAsync(Guid id, CancellationToken ct);
}
Walkthrough examples
- Modern C# API Foundations in a order management API — A team is extending a production-grade ASP.NET Core order API and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: Orders/CreateOrder.cs
- File: src/Orders.Application/modern-csharp-api-foundations.cs
- File: tests/Orders.Api.Tests/modern-csharp-api-foundations.Tests.cs
- File: docs/dotnet-advanced/modern-csharp-api-foundations.md
- Start from the provided C# snippet and make the intent visible in names and boundaries.
- Apply the checklist item "No nullable warnings ignored" before adding extra behavior.
- Write down how the implementation changes when nullable reference type fails or becomes slow.
- Inspect the installed SDKs — The first command lists installed SDK versions. The second prints runtime, RID, workload, and environment details that are useful when debugging builds.
- Retained source code:
dotnet --list-sdks
dotnet --info
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Smallest useful program — Modern C# supports top-level statements, so a simple program does not need an explicit Program class. The compiler still generates one behind the scenes.
- Retained source code:
using System;
Console.WriteLine("Hello from .NET");
Console.WriteLine(Environment.Version);
- Expected output: Hello from .NET
<runtime version>
- Compare the example with the canonical PTLearn implementation.
- A minimal typed entry point — Top-level statements are convenient, but explicit entry points still matter for some tools, older codebases, and cases where returning an exit code is part of the contract.
- Retained source code:
using System;
public static class Program
{
public static int Main(string[] args)
{
Console.WriteLine($"Arguments: {args.Length}");
return 0;
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Nullable reference types — The question mark on string? means callers may pass null. The guard narrows the type so the compiler knows name is not null after the if block.
- Retained source code:
static int NameLength(string? name)
{
if (string.IsNullOrWhiteSpace(name))
{
return 0;
}
return name.Length;
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Generic result with pattern matching — The switch expression handles both type and property patterns. Records make small immutable data shapes concise, and generics keep the result reusable.
- Retained source code:
public abstract record Result<T>;
public sealed record Success<T>(T Value) : Result<T>;
public sealed record Failure<T>(string Error) : Result<T>;
static string Describe(Result<int> result) => result switch
{
Success<int> { Value: > 0 } ok => $"Positive: {ok.Value}",
Success<int> ok => $"Number: {ok.Value}",
Failure<int> failed => $"Error: {failed.Error}",
_ => "Unknown"
};
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Guard clause for invalid input — Use exceptions for programmer errors or invalid states that should not continue silently. The relational pattern makes the percent range readable.
- Retained source code:
public static decimal ApplyDiscount(decimal price, decimal percent)
{
if (price < 0) throw new ArgumentOutOfRangeException(nameof(price));
if (percent is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(percent));
return price * (1 - percent / 100m);
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Class with an invariant — The item can never be created with invalid quantity, and reservation rules are enforced in one place.
- Retained source code:
public sealed class InventoryItem
{
public InventoryItem(string sku, int quantity)
{
if (string.IsNullOrWhiteSpace(sku)) throw new ArgumentException("SKU is required", nameof(sku));
if (quantity < 0) throw new ArgumentOutOfRangeException(nameof(quantity));
Sku = sku;
Quantity = quantity;
}
public string Sku { get; }
public int Quantity { get; private set; }
public void Reserve(int amount)
{
if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
if (amount > Quantity) throw new InvalidOperationException("Insufficient inventory");
Quantity -= amount;
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Record value object — The with-expression creates a copy rather than mutating the original value.
- Retained source code:
public sealed record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
{
throw new InvalidOperationException("Currency mismatch");
}
return this with { Amount = Amount + other.Amount };
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Interface for a dependency — An interface is useful when the implementation varies by environment, such as production time versus test time.
- Retained source code:
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
public sealed class SystemClock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Async HTTP call — The method returns a Task that completes later. The cancellation token lets callers abandon work when a request is aborted or a timeout expires.
- Retained source code:
public sealed class WeatherClient(HttpClient httpClient)
{
public async Task<Forecast?> GetForecastAsync(string city, CancellationToken cancellationToken)
{
var path = $"/weather/{Uri.EscapeDataString(city)}";
return await httpClient.GetFromJsonAsync<Forecast>(path, cancellationToken);
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Run independent calls together — Start independent work before awaiting so the operations overlap. Keep the shared cancellation token flowing.
- Retained source code:
var customerTask = customerClient.GetAsync(customerId, cancellationToken);
var ordersTask = orderClient.ListRecentAsync(customerId, cancellationToken);
await Task.WhenAll(customerTask, ordersTask);
var profile = new CustomerProfile(
await customerTask,
await ordersTask);
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Timeout with cancellation — Linked tokens combine caller cancellation with a local timeout policy.
- Retained source code:
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
requestAborted,
timeout.Token);
var result = await service.LoadAsync(linked.Token);
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Enable nullable reference types.
- Create one request record.
- Thread CancellationToken through a service method.
- Identify the platform role: Given the terms C#, SDK, runtime, ASP.NET Core, and NuGet, classify each as language, tooling, execution environment, web framework, or package ecosystem.
- Starter code: C# = ?
SDK = ?
runtime = ?
ASP.NET Core = ?
NuGet = ?
- Expected output: language; tooling; execution environment; web framework; package ecosystem
- Hint: Think about which part writes code, builds code, executes code, hosts web code, and shares libraries.
- Reference solution: C# is the language. The SDK is tooling. The runtime is the execution environment. ASP.NET Core is the web framework. NuGet is the package ecosystem.
- Accepted answers: language tooling execution environment web framework package ecosystem
- State a nullability contract: Write the method signature for a function that accepts a possibly missing email and returns true when it is valid.
- Starter code: static bool IsValidEmail(____ email)
- Expected output: static bool IsValidEmail(string? email)
- Hint: A possibly missing reference type uses a question mark after the type name.
- Reference solution: static bool IsValidEmail(string? email)
- Accepted answers: static bool IsValidEmail(string? email) | bool IsValidEmail(string? email)
- Choose record or class: Choose the better construct for an immutable CreateOrderCommand carrying CustomerId and Lines.
- Starter code: record or class?
- Expected output: record
- Hint: Commands are usually immutable data carriers with value-like equality.
- Reference solution: Use a record, for example public sealed record CreateOrderCommand(Guid CustomerId, IReadOnlyList<OrderLine> Lines);
- Accepted answers: record
- Choose the return type: A method asynchronously fetches a Product and may fail by throwing. What return type should it usually expose?
- Starter code: public async ____ GetProductAsync(Guid id, CancellationToken cancellationToken)
- Expected output: Task<Product>
- Hint: Async methods that return a value usually return Task<T>.
- Reference solution: public async Task<Product> GetProductAsync(Guid id, CancellationToken cancellationToken)
- Accepted answers: Task<Product> | Task<Product?>
Checklist
- No nullable warnings ignored
- Records represent command data
- Async calls accept cancellation
- Options are validated
Quiz prompts
- Why pass CancellationToken into EF Core calls? — Cancellation helps release resources when callers disconnect or shutdown begins.
- A teammate wants to hide record inside a convenient helper. What should you check first? — Place record at the boundary that keeps order management API behavior explicit, testable, and reviewable.
- Which artifact best proves this .NET APIs lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Basic foundation: a teammate says the happy path works, but "Records and nullable references" is still implicit. What should you ask for before merging? — Records and nullable references belongs in the basic stage only when the decision is visible, testable, and tied to a realistic failure mode.
- A reviewer has five minutes to evaluate this basic .NET APIs slice. Which evidence is strongest? — Create the basic order API contract layer: request records, validated options, cancellation-aware service interfaces, and a short note explaining which invariants belong at this layer.
- Which component contains the compiler and project templates? — The SDK includes the CLI, compiler, templates, MSBuild targets, and other development tools.
- Why prefer generics over object parameters for reusable collections or results? — Generics let the compiler verify types without forcing callers to cast from object.
- What is the main advantage of composition over deep inheritance? — Composition keeps roles smaller and makes change easier than fragile inheritance trees.
- Why is .Result or .Wait() risky in async code? — Blocking on async work defeats the scalability benefit and can cause deadlocks in synchronization-context environments.
Flashcards
- Basic foundation: what decision does "Records and nullable references" force you to make? Use immutable request shapes and compiler feedback to remove ambiguous input handling. Evidence prompt: Convert one mutable request class into a record and resolve every nullable warning intentionally.
- Basic foundation: what decision does "Async boundaries, options, and cancellation" force you to make? Thread cancellation and validated configuration through the first service boundary. Evidence prompt: Make one service method cancellation-aware and validate its options at startup.
- Basic foundation: what decision does "Project shape and dependency direction" force you to make? Keep API, application, and infrastructure references pointed in one reviewable direction from the first commit. Evidence prompt: Draw the project references and move one misplaced framework concern back to the API edge.
- In .NET APIs, what should you remember about record? record matters here because it supports "Model request shapes with records.".
- In .NET APIs, what should you remember about nullable reference type? nullable reference type matters here because it supports "Propagate cancellation tokens.".
- In .NET APIs, what should you remember about async Task? async Task matters here because it supports "Validate options at startup.".
- In .NET APIs, what should you remember about options pattern? options pattern matters here because it supports "Model request shapes with records.".
Labs
- Ship a modern c# api foundations slice — Extend a production-grade ASP.NET Core order API with a small but reviewable feature that proves the lesson's architecture in code.
- Create the basic order API contract layer: request records, validated options, cancellation-aware service interfaces, and a short note explaining which invariants belong at this layer.
- Convert one mutable request class into a record and resolve every nullable warning intentionally.
- Make one service method cancellation-aware and validate its options at startup.
- Draw the project references and move one misplaced framework concern back to the API edge.
- Enable nullable reference types.
- Create one request record.
- The lab demonstrates the basic foundation outcome without skipping earlier contract evidence.
- Each subtopic has a concrete artifact: Records and nullable references, Async boundaries, options, and cancellation, Project shape and dependency direction.
- The review notes explain how this level changes ownership, verification, or operational risk.
Challenge
- Review-ready modern c# api foundations (Stretch) — Create the basic order API contract layer: request records, validated options, cancellation-aware service interfaces, and a short note explaining which invariants belong at this layer.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from Orders/CreateOrder.cs plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL