I'm always excited to take on new projects and collaborate with innovative minds.

Social Links

C# 14 & .NET 10 — Extension Members, SSE, JSON Mapping, and More

C# 14 and .NET 10 bring major quality-of-life updates — from extension members and null-conditional assignment to minimal API validation, SSE streaming, EF Core JSON mapping, and single-file execution.

C# 14 — extension members, null-conditional assignment, expression-tree improvements

Extension members (think: extension everything)

C# 14 adds an extension block syntax so you can group several extension members (methods, properties, indexers, and even static extension members) in one place. It’s mostly syntactic sugar over classic extension methods, but it makes adding properties and grouping related extensions much nicer. (Microsoft Learn)

Example — instance extension property + method (new syntax):

using System.Linq;

public static class EnumerableExtensions
{
    // old-style still works:
    public static bool IsEmpty<T>(this IEnumerable<T> src) => !src.Any();

    // new C# 14 extension block:
    extension<T>(IEnumerable<T> src)
    {
        // becomes an instance-style member on IEnumerable<T>
        public bool HasAtLeast(int n) => src.Skip(n - 1).Any();
    }
}

// Usage:
var list = new[] {1,2,3};
bool emptyOld = list.IsEmpty();
bool hasTwo  = list.HasAtLeast(2);

Null-conditional assignment (?. on the left side)

You can now use ?. (and ?[]) on the left side of assignments — the right-hand side only executes if the receiver is non-null. This removes a lot of small if (x != null) blocks. (Microsoft Learn)

Example:

Customer? customer = GetCustomerOrNull();

// Only assigns when 'customer' is non-null
customer?.Order = FetchCurrentOrder();

// You can chain safely:
a?.b?.c = other?.d;

Expression trees accept named & optional parameters

Expression trees no longer choke on named or optional arguments — so expressions that use comparer: null or omit optional args now work in LINQ providers or Expression-based APIs. This reduces awkward workarounds when building expression trees. (Microsoft Learn)

Example:

using System;
using System.Linq.Expressions;
using System.Collections.Generic;

Expression<Func<IEnumerable<int>, int, bool>> expr =
    (seq, value) => seq.Contains(value, comparer: null);

// Expression now allowed to include named/optional args
Console.WriteLine(expr); // prints tree — works without earlier errors

ASP.NET Core — minimal API validation & Server-Sent Events (SSE)

Minimal API validation (DataAnnotation style)

Minimal APIs can now participate in the same model validation flow as controllers, so DTOs with [Required], [Range], etc. are validated automatically and you can customize error formatting (for example via IProblemDetailsService). This gives minimal endpoints first-class model validation. (Microsoft Learn)

Example:

using System.ComponentModel.DataAnnotations;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

record CreateCustomerDto([Required] string Name, int Age);

app.MapPost("/customers", (CreateCustomerDto dto) =>
{
    // If dto is invalid, framework returns a validation error response automatically.
    return Results.Created($"/customers/123", dto);
});

app.Run();

(You can register services to customize the validation error shape with IProblemDetailsService if you want consistent ProblemDetails responses.) (Microsoft Learn)

Server-Sent Events (SSE) helper

For simple one-way push from server → browser, ASP.NET Core exposes SSE helpers so you can stream events from a Minimal API endpoint without SignalR overhead. (Khalid Abuhakmeh’s Blog)

Example (Minimal API SSE):

// Example: stream a heartbeat every second
using System.Threading.Channels;

app.MapGet("/sse/heartbeat", (CancellationToken ct) =>
{
    async IAsyncEnumerable<string> Stream()
    {
        var i = 0;
        while (!ct.IsCancellationRequested)
        {
            await Task.Delay(1000, ct);
            yield return $"data: heartbeat {i++}\n\n";
        }
    }

    // TypedResults.ServerSentEvents (helper) or similar API returns SSE content
    return TypedResults.ServerSentEvents(Stream(), eventType: "heartbeat");
});

On the browser side a simple EventSource("/sse/heartbeat") listens for events.


Blazor — cleaner NotFound routing

Blazor’s router lets you declare a dedicated NotFound component (instead of inline markup inside App.razor), which tidies up the router markup and centralizes 404 handling for static & client routing. (Microsoft Learn)

App.razor example:

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <NotFoundPage />  @* Dedicated component for 404s *@
    </NotFound>
</Router>

NotFoundPage.razor can set headers or render SEO-friendly content as needed.


EF Core — LeftJoin/RightJoin and JSON mapping

LeftJoin / RightJoin (LINQ operator)

LINQ in .NET 10 adds LeftJoin/RightJoin style operators (and EF Core translates them to SQL), so you no longer need the long GroupJoin + SelectMany + DefaultIfEmpty dance for outer joins. This reads closer to SQL and reduces boilerplate. (Microsoft Learn)

Example (LINQ/EF style):

// Assume DbSet<Customer> Customers and DbSet<Order> Orders
var q = db.Customers.LeftJoin(
    db.Orders,
    customer => customer.Id,
    order => order.CustomerId,
    (customer, order) => new { customer, order }  // order can be null
);

var results = await q.ToListAsync();

JSON column mapping (map complex CLR properties to a JSON column)

EF Core gained richer JSON mapping so you can map owned/complex CLR types directly into a JSON column, query into their inner properties via LINQ, and even update subfields efficiently. This is useful for semi-structured data inside relational databases. (Microsoft Learn)

Example — OwnsOne mapped to JSON column:

public class Product
{
    public int Id { get; set; }
    public ProductMetadata Metadata { get; set; } = new();
}

public class ProductMetadata
{
    public string? Color { get; set; }
    public Dimensions Size { get; set; } = new();
}

modelBuilder.Entity<Product>(e =>
{
    e.OwnsOne(p => p.Metadata, md =>
    {
        md.ToJson(); // map the owned type to a single JSON column
    });
});

// Querying:
var blueProducts = await db.Products
    .Where(p => p.Metadata.Color == "blue")
    .ToListAsync();

Runtime / CLI — run single C# file (dotnet run app.cs)

You can run a single .cs file directly with the CLI (dotnet run app.cs) — no .csproj needed — making quick demos, throwaway scripts and teaching scenarios far simpler. (Microsoft for Developers)

Usage (shell):

# create a single-file app
cat > hello.cs <<'CS'
using System;
Console.WriteLine("Hello from a single C# file!");
CS

dotnet run hello.cs

Quick recap — why these matter

  • Cleaner syntax & ergonomics: extension members and null-conditional assignment reduce ceremony. (Andrew Lock | .NET Escapades)
  • Better server tooling: minimal API validation and SSE reduce glue-code for APIs and lightweight streaming. (Microsoft Learn)
  • Stronger data story: LeftJoin/RightJoin + JSON mapping simplify complex queries and semi-structured storage in EF Core. (Microsoft Learn)
  • Faster experimentation: dotnet run app.cs lowers the friction to try ideas. (Microsoft for Developers)
5 min read
Oct 24, 2025
By Dheer Gupta
Share

Leave a comment

Your email address will not be published. Required fields are marked *