C# 14August 2026 · 6 min read

C# 14 Features: Modern C# in Action with Real .NET 10 Code

TL;DR

C# 14 Features look best when you see them inside a working program. This article walks one .NET 10 vertical slice end to end — Minimal API endpoint, handler, domain model, and helper — using the field keyword, params collections over spans, nameof on unbound generics, and extension members where they save real lines of code.

C# 14 Features: Modern C# in Action with Real .NET 10 Code — the fastest way to learn a language version is to watch it inside a working program. So this article walks a single .NET 10 vertical slice end to end: a Minimal API endpoint, its request and response DTOs, a domain model, and a service helper. Every snippet compiles on the .NET 10 SDK, and every snippet leans on C# 14 features you would actually reach for in daily work — not synthetic demos, but code shaped like a production order-management feature.

C# 14 Features in the Endpoint Slice

A Minimal API slice in .NET 10 is short even before C# 14. Add the new language features and it gets shorter in exactly the places that matter: fewer backing fields, no hand-written array allocation, and less ceremony around validation. The request record below uses a primary constructor, and the mapping stays deliberately thin — route, contract, and a single mediator send.

// Features/Orders/CreateOrderEndpoint.cs
using Indotalent.Orders;
using MediatR;

public sealed record CreateOrderRequest(string CustomerId, OrderLineDto[] Lines);

public static class CreateOrderEndpoint
{
    public static void Map(IEndpointRouteBuilder app) =>
        app.MapPost("/api/orders",
            async (CreateOrderRequest request, IMediator mediator) =>
                Results.Ok(await mediator.Send(
                    new CreateOrderCommand(request.CustomerId, request.Lines))));
}

The handler that receives this command is equally compact. Primary constructors inject the database context, the command is a simple record, and the whole flow stays inside one file. This is the shape every slice in an Indotalent product follows — and it is the shape C# 14 was designed to keep small.

C# 14 Features: Field and Params in Domain Logic

The real payoff shows up in the domain model. Before C# 14, a property that needs to clamp or validate its value requires a private backing field plus two accessors. With the field keyword, the compiler owns the backing storage and you keep only the behavior. The Order class below also uses a collection expression for its lines and a computed total that needs no helper method at all.

// Features/Orders/Order.cs
public sealed class Order
{
    public Guid Id { get; } = Guid.NewGuid();

    public string CustomerId { get; set; } = string.Empty;

    public string Status
    {
        get => field;
        set => field = value is "draft" or "submitted" ? value : "draft";
    }

    public IReadOnlyList<OrderLineDto> Lines { get; init; } = [];

    public decimal Total =>
        Lines.Sum(line => line.UnitPrice * line.Quantity);
}

// Features/Orders/OrderRules.cs
public static class OrderRules
{
    public static bool AreValid(params ReadOnlySpan<OrderLineDto> lines)
    {
        if (lines.Length == 0) return false;
        foreach (var line in lines)
        {
            if (line.Quantity <= 0 || line.UnitPrice < 0) return false;
        }
        return true;
    }
}

The params ReadOnlySpan<OrderLineDto> declaration is C# 14's zero-allocation params. Callers write OrderRules.AreValid(line1, line2) and the compiler packs the arguments into a stack buffer instead of heap-allocating an array. For validation helpers invoked inside a request pipeline, that removes a small but measurable allocation from every request that flows through the endpoint.

Notice what disappeared: no _status backing field, no constructor glue, no List<T> allocation where a collection expression suffices. Each feature is minor on its own; together they are the difference between a slice that fits one screen and one that sprawls across three files.

C# 14 Features: Nameof, Ref Structs, and Extension Members

Three more C# 14 features round out a modern codebase. nameof on an unbound generic type cleans up logging categories and reflection metadata. ref struct types can now implement interfaces and appear as generic arguments with the allows ref struct constraint, which keeps span-based value objects stack-only while still honoring contracts. Extension members, in preview, let you attach members to a type family without static helper classes.

// C# 14: nameof on an unbound generic type
var category = nameof(Dictionary<,>);   // "Dictionary"
logger.LogInformation("[{Category}] order submitted", category);

// C# 14: ref struct implementing an interface
public interface IUtf8Writable
{
    bool TryWrite(Span<byte> destination, out int written);
}

public ref struct OrderNumber : IUtf8Writable
{
    private readonly int _value;
    public OrderNumber(int value) => _value = value;

    public bool TryWrite(Span<byte> destination, out int written)
    {
        var text = _value.ToString(System.Globalization.CultureInfo.InvariantCulture);
        written = System.Text.Encoding.UTF8.GetBytes(text, destination);
        return true;
    }
}

// C# 14 (preview): extension members
public extension(string value)
{
    public bool IsBlank() => string.IsNullOrWhiteSpace(value);
    public string Slugify() => value.Trim().ToLowerInvariant();
}

None of this changes how the application behaves on the wire. What it changes is how many lines you must hold in your head to understand a feature. The endpoint maps a route, the handler mutates a domain object, and the domain object guards its own invariants — each step readable from top to bottom in one pass.

The same compactness extends to tests. A slice that reads top to bottom maps to a test class that reads the same way: arrange the request, act on the handler, assert on the response. When the plumbing shrinks, the seam between production code and test code becomes obvious, and a test that covers the business rule no longer needs five mock setups just to satisfy constructor parameters that exist only to feed boilerplate.

Running Modern C# on .NET 10

All of this works because the project targets .NET 10 with the C# 14 compiler. The project file is deliberately boring — enable nullable reference types and implicit usings, pin the language version, and the SDK handles the rest.

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <LangVersion>14</LangVersion>
  <ImplicitUsings>enable</ImplicitUsings>
  <Nullable>enable</Nullable>
</PropertyGroup>

Adopt the features one at a time. Start with field in the properties you touch most, then migrate hot helpers to params spans, then reach for nameof and extension members as the opportunity appears. Modern C# is not an all-or-nothing rewrite; it is a steady reduction of ceremony, and .NET 10 is the release that makes it painless.

If you are coming from a C# 12 codebase, the good news is that everything here compiles in isolation. You can retarget a single project, keep the rest on the old language version, and migrate slice by slice. Our upgrade checklist walks through the full sequence — SDK, LangVersion, packages, and breaking changes — so the transition never stalls on an unexpected diagnostic.

Key Takeaways

  • The field keyword removes backing-field boilerplate from validated properties in domain models
  • params ReadOnlySpan<T> gives you zero-allocation variadic helpers in hot paths
  • Collection expressions and primary constructors keep each vertical slice to a single file
  • nameof on unbound generics cleans up logging categories and reflection metadata
  • ref struct types can implement interfaces in C# 14, keeping span-based value objects stack-only
  • Enabling C# 14 is a two-line project change: net10.0 plus LangVersion 14

Ready to read C# 14 Features in a real .NET 10 codebase?

Every Indotalent product is a complete .NET 10 application written in C# 14, with full source code for every slice. Complete .NET 10 source code — $21 each.

Explore Products