This post walks through .NET 10 and C# 14 updates from the perspective of an API developer using a real-world example: an order management API with validation, OpenAPI docs and Entity Framework Core.
It’s that time of year: A new version of the .NET platform has shipped. .NET 10 landed last month as an LTS release, with support through November 2028.
To complement Jon Hilton’s .NET 10 Has Arrived—Here’s What Changed for Blazor article and Assis Zang’s What’s New in .NET 10 for ASP.NET Core, let’s look at .NET 10 improvements through the viewpoint of an API developer.
Throughout this post, we’ll walk through updates using a real-world example: an order management API with validation, OpenAPI docs and Entity Framework Core.
NOTE: The examples use Minimal APIs for brevity, but most of these improvements can be used for controller-based APIs as well.
Built in Validation for Minimal APIs
Before .NET 10, teams building Minimal APIs ended up rolling their own validation. The result? Endpoint code that was more about policing inputs than implementing business logic.
Here’s a simplified example that shows the problem.
public static class OrderEndpoints
{
public static void MapOrderEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/orders");
group.MapPost("/", CreateOrder);
group.MapGet("/{id}", GetOrder);
}
private static async Task<IResult> CreateOrder(
CreateOrderRequest request,
OrderDbContext db)
{
if (string.IsNullOrWhiteSpace(request.CustomerEmail))
return Results.BadRequest("Customer email is required");
if (request.Items is null || request.Items.Count == 0)
return Results.BadRequest("Order must contain at least one item");
foreach (var item in request.Items)
{
if (item.Quantity < 1)
return Results.BadRequest("Quantity must be at least 1");
if (item.ProductId <= 0)
return Results.BadRequest("Invalid product ID");
}
var order = new Order
{
CustomerEmail = request.CustomerEmail,
Items = request.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity
}).ToList(),
CreatedAt = DateTime.UtcNow
};
db.Orders.Add(order);
await db.SaveChangesAsync();
return Results.Created($"/api/orders/{order.Id}", order);
}
private static async Task<IResult> GetOrder(int id, OrderDbContext db)
{
var order = await db.Orders.FindAsync(id);
return order is null ? Results.NotFound() : Results.Ok(order);
}
}
There were ways around this: you could use filters, helper methods or third-party validators. Even so, it was frustrating that Minimal APIs didn’t have the baked-in validation experience you have with controller-based APIs.
.NET 10 adds built-in validation support for Minimal APIs. You can enable it with one registration call:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<OrderDbContext>();
builder.Services.AddValidation();
var app = builder.Build();
Once enabled, ASP.NET Core automatically applies DataAnnotations validation to Minimal API parameters. This includes query, header and request body binding.
You can also disable validation for a specific endpoint using DisableValidation(), which is handy for internal endpoints or partial updates where you intentionally accept incomplete payloads.
With validation handled by the framework and the attributes added to our models, endpoints can focus on business logic.
Validation Error Responses
When validation fails, ASP.NET Core returns a standardized ProblemDetails response with an errors dictionary.
A typical response looks like this.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"CustomerEmail": [
"The CustomerEmail field is required."
],
"Items[0].Quantity": [
"Quantity must be between 1 and 1000"
]
}
}
OpenAPI 3.1: Modernizing API Documentation
.NET 10’s built-in OpenAPI document generation supports OpenAPI 3.1 and JSON Schema 2020-12. The default OpenAPI version for generated documents is now 3.1.
OpenAPI 3.1 aligns better with modern JSON schema expectations and improves how tools interpret your schemas.
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi(options =>
{
options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info = new OpenApiInfo
{
Title = "Order Management API",
Version = "v1",
Description = "Enterprise order processing system",
Contact = new OpenApiContact
{
Name = "API Support",
Email = "api-support@company.com"
}
};
return Task.CompletedTask;
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapOpenApi("/openapi/{documentName}.yaml");
}
app.Run();
Note the YAML route: in .NET 10, you generate YAML by using a route ending in .yaml or .yml, typically with {documentName} in the path.
Schema Improvements in Practice
OpenAPI 3.0 often expressed nullability using nullable: true.
components:
schemas:
ShippingAddress:
type: object
nullable: true
properties:
street:
type: string
nullable: true
city:
type: string
OpenAPI 3.1 allows us to use union types:
components:
schemas:
ShippingAddress:
type: ["object", "null"]
properties:
street:
type: ["string", "null"]
city:
type: string
This tends to play nicer with tooling that relies heavily on JSON Schema semantics such as OpenAPI Generator and NSwag.
EF Core 10: Named Query Filters
Global query filters are a staple for multi-tenant apps and soft deletes. The classic problem was granularity: IgnoreQueryFilters() disabled all filters at once.
EF Core 10 introduces named query filters, so you can selectively disable one filter while keeping another.
public class OrderDbContext : DbContext
{
private readonly int _tenantId;
public OrderDbContext(DbContextOptions<OrderDbContext> options, ITenantProvider tenant)
: base(options)
=> _tenantId = tenant.TenantId;
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasQueryFilter("SoftDelete", o => !o.IsDeleted)
.HasQueryFilter("TenantIsolation", o => o.TenantId == _tenantId);
}
}
Now an admin endpoint can disable soft delete without disabling tenant isolation:
public static class AdminEndpoints
{
public static void MapAdminEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/admin")
.RequireAuthorization("Admin");
group.MapGet("/orders/deleted", GetDeletedOrders)
.WithSummary("Gets deleted orders for the current tenant only");
}
private static async Task<IResult> GetDeletedOrders(OrderDbContext db)
{
var deletedOrders = await db.Orders
.IgnoreQueryFilters(new[] { "SoftDelete" })
.Where(o => o.IsDeleted)
.Select(o => new { o.Id, o.CustomerEmail, o.DeletedAt })
.ToListAsync();
return Results.Ok(deletedOrders);
}
}
This capability is small, but it’s exactly the kind of real-world safety improvement that helps prevent cross-tenant data leaks.
C# 14 Improvements
.NET 10 ships alongside C# 14. For API developers, a few features immediately reduce boilerplate and improve readability.
The field Keyword: Eliminate Backing-field Boilerplate
C# 14 introduces field-backed properties, where you can reference the compiler-generated backing field directly using the field keyword.
public sealed class Order
{
public string CustomerEmail
{
get;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Email cannot be empty.", nameof(value));
field = value.Trim().ToLowerInvariant();
}
}
}
Null-conditional Assignments
C# 14 allows null-conditional operators (?. and ?[]) on the left-hand side of assignments and compound assignments. The right-hand side is evaluated only when the receiver isn’t null. This is great for processing patches.
public sealed record OrderPatchRequest(string? NewStatus, int? NewPriority, string? NewCity);
public static class OrderPatchService
{
public static void ApplyPatch(Order? order, OrderPatchRequest? patch)
{
if (patch is null) return;
order?.Status = patch.NewStatus ?? order?.Status;
order?.Priority = patch.NewPriority ?? order?.Priority;
order?.Shipping?.City = patch.NewCity ?? order?.Shipping?.City;
}
}
Note: Increment/decrement operators (++, --) aren’t allowed with null-conditional assignments. Compound assignments like += are supported.
For more details on this feature, check out the post Write Cleaner Code with C# 14’s Null-Conditional Assignment Operator.
Extension Members: Properties, Static Members and Operators
C# 14’s headline feature is extension members, which add extension properties, static extension members and even operators using the new extension block syntax.
Don’t you just love the syntax?
public static class OrderExtensions
{
extension(Order source)
{
public decimal TotalValue =>
source.Items.Sum(i => i.Quantity * i.UnitPrice);
public bool IsHighValue => source.TotalValue > 1000m;
public string Summary =>
$"Order #{source.Id}: {source.Items.Count} items, ${source.TotalValue:F2} total";
}
extension(Order)
{
public static Order CreateEmpty(int tenantId) => new Order
{
TenantId = tenantId,
CreatedAt = DateTime.UtcNow,
Items = new List<OrderItem>(),
Status = "Draft"
};
}
}
For more details on extension members, check out Extension Properties: C# 14’s Game-Changing Feature for Cleaner Code. (I don’t get paid by the click, I swear.)
Server-Sent Events: Simplifying Real-Time Updates
ASP.NET Core in .NET 10 adds a built-in ServerSentEvents result for Minimal APIs, so you can stream updates over a single HTTP connection without manually formatting frames.
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http.HttpResults;
public record OrderStatusUpdate(int OrderId, string Status, string Message, DateTime Timestamp);
public static class OrderStreamingEndpoints
{
public static void MapStreamingEndpoints(this WebApplication app)
{
app.MapGet("/api/orders/{id:int}/status-stream", StreamOrderStatus)
.WithSummary("Stream real-time order status updates");
}
private static ServerSentEventsResult<OrderStatusUpdate> StreamOrderStatus(
int id,
OrderDbContext db,
CancellationToken ct)
{
async IAsyncEnumerable<OrderStatusUpdate> GetUpdates(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
string? last = null;
while (!cancellationToken.IsCancellationRequested)
{
var order = await db.Orders.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
if (order is null)
{
yield return new(id, "ERROR", "Order not found", DateTime.UtcNow);
yield break;
}
if (!string.Equals(order.Status, last, StringComparison.Ordinal))
{
yield return new(order.Id, order.Status, $"Order is now {order.Status}", DateTime.UtcNow);
last = order.Status;
}
if (order.Status is "Delivered" or "Cancelled")
yield break;
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
}
return TypedResults.ServerSentEvents(GetUpdates(ct), eventType: "order-status");
}
}
Client-side consumption is quite simple, too:
<script>
function trackOrderStatus(orderId) {
const es = new EventSource(`/api/orders/${orderId}/status-stream`);
es.onmessage = (event) => {
const update = JSON.parse(event.data);
console.log(update);
if (update.status === "Delivered" || update.status === "Cancelled") {
es.close();
}
};
es.onerror = () => console.error("SSE connection error");
return es;
}
</script>
Should You Upgrade to .NET 10?
.NET 10 is an LTS release. If you’re starting a new project, it’s a no-brainer.
If you’re upgrading from .NET 8 or .NET 9, a few things to keep in mind:
- Minimal API validation: If you’ve been hand-rolling validation, .NET 10’s
AddValidation support can remove a surprising amount of custom code. - OpenAPI: Built-in OpenAPI generation defaults to 3.1 and supports YAML endpoints via
.yaml/.yml routes. - EF Core: Named query filters are a real safety upgrade for multi-tenant apps, and JSON column support continues to improve (including bulk update support).
- C# 14: You can adopt new features incrementally. Even if you ignore extension members entirely,
field and null-conditional assignment will show up in your codebase quickly.
The upgrade path is generally smooth: change the target in your .csproj, run tests, fix warnings and ship.
Wrapping Up
.NET 10 delivers meaningful improvements for API developers through thoughtful enhancements rather than revolutionary changes. The combination of built-in Minimal API validation, OpenAPI 3.1 and C# 14 quality-of-life features add up to a more productive and safer development experience.
Happy coding!
References
ASP.NET Core/API Updates
Language Updates