I'm always excited to take on new projects and collaborate with innovative minds.
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 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);
?. 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 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
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)
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’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.
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();
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();
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
dotnet run app.cs lowers the friction to try ideas. (Microsoft for Developers)Your email address will not be published. Required fields are marked *