DeveloperHA / Learn / C#
C# learning path

C# features by version, with practical patterns

Find the C# feature you need, understand which language version introduced it and use it without losing clarity.

Foundation to practical35–50 min4 lessons

Outcome: Choose language features deliberately, distinguish compiler version from runtime target and apply modern patterns to everyday .NET code.

Your local progress0 / 4 complete

Use the version table as a quick lookup, then open the cards for practical trade-offs. A newer feature is useful only when your SDK, target framework and team conventions support it.

C# language version quick reference
Language versionUseful capabilitiesGood fit when…
C# 8Nullable reference types, async streams, ranges and indices, switch expressionsSafer APIs and more expressive iteration in modern .NET projects.
C# 9Records, init-only setters, top-level statements, improved pattern matchingConcise data models and simpler small applications.
C# 10File-scoped namespaces, global using directives, record structsLess ceremony without changing the runtime model.
C# 11Raw string literals, required members, list patterns, generic mathReadable embedded text and explicit initialization.
C# 12Primary constructors, collection expressions, inline arraysReduce boilerplate while keeping allocation choices visible.

Work through the path

Open a card when you need it. Marking a lesson complete is optional and stays only in this browser.

1Separate C# language version from .NET runtime

Version choice5 min

The compiler language version controls syntax and features; the target framework controls the runtime and base libraries.

A project can target one .NET version while using a compatible C# language version. The SDK normally chooses a sensible default, but a repository can pin LangVersion for repeatable builds.

When a feature fails to compile, check the SDK, TargetFramework, LangVersion and project SDK style before assuming the runtime is missing the feature.

Try it

Record the target framework and language version in the README so upgrade conversations are concrete.

Watch outDo not pin a newer language version unless every build agent and IDE supports it.
Generate C# from JSON →
Example
<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <LangVersion>12.0</LangVersion>
  <Nullable>enable</Nullable>
</PropertyGroup>

2Choose records and classes by identity

Data modeling6 min

Records communicate value-like data and generated equality; classes usually communicate identity, lifecycle or mutable behavior.

A request or response DTO often benefits from a record because its purpose is to carry values. An EF Core entity usually has identity and lifecycle rules that deserve a class.

The syntax is not the design decision. Ask whether two instances with the same values should be considered the same value in your domain.

Try it

Take one DTO and one EF Core entity and write down what equality and mutation should mean for each.

Watch outA record does not make nested collections immutable and does not replace validation.
Generate a model starting point →
Example
public sealed record CreateOrderRequest(
    Guid CustomerId,
    IReadOnlyList<CreateOrderLine> Lines);

public sealed class Order
{
    public Guid Id { get; private set; }
    public OrderStatus Status { get; private set; } = OrderStatus.Draft;
    public void Submit() => Status = OrderStatus.Submitted;
}

3Treat nullable annotations as a design conversation

Safety5 min

Nullable reference types tell the compiler where null is expected; they improve feedback but do not add runtime validation.

With nullable enabled, string communicates that null is not expected while string? communicates that it may be missing. The compiler points out paths where the guarantee is not established.

Boundaries still need real validation: model binding, databases, deserialization and external APIs can all violate assumptions at runtime.

Try it

Turn nullable on for one project and fix warnings at boundaries before reaching for the ! suppression operator.

Watch outNullable annotations are guidance, not a security or runtime validation boundary.
Check generated nullable properties →
Example
public sealed record CustomerResponse(
    Guid Id, string DisplayName, string? PreferredName);

var name = customer.PreferredName ?? customer.DisplayName;

4Make asynchronous work cancellable

Async6 min

Pass a CancellationToken from the request boundary into I/O operations so abandoned work can stop early.

Async releases a thread while the application waits for I/O. Cancellation completes the picture: a disconnected client or timed-out job should not keep doing work nobody will consume.

Keep the token flowing through your methods and pass it to APIs that accept one. Do not turn cancellation into a generic success response.

Try it

Find an I/O method with no token. Add the token at the public boundary, then follow compiler errors inward.

Watch outCancellation is cooperative; a provider must observe the token.
Generate an HTTP client starting point →
Example
public async Task<OrderDto?> GetAsync(
    Guid id, CancellationToken token)
{
    return await db.Orders
        .AsNoTracking()
        .Where(order => order.Id == id)
        .Select(order => new OrderDto(order.Id, order.Status))
        .SingleOrDefaultAsync(token);
}

No lesson matches that phrase.

Related tools

Turn the concept into a working example with the browser-based tools.

Need a second pair of eyes?

When the quick answer turns into an architecture or delivery decision, use a fixed-price review or talk to DeveloperHA.

See packages All learning paths