ASP.NET Core 10, released as part of .NET 10 LTS, delivers significant improvements for web API developers. The headline features—native Minimal API validation, OpenAPI 3.1 with YAML export, and Blazor WebAssembly prefetching—address common production pain points. This comprehensive guide explores each feature with production-ready patterns, migration strategies, and performance considerations for enterprise web applications.
What’s New in ASP.NET Core 10
| Feature | Description | Impact |
|---|---|---|
| Minimal API Validation | Built-in validation with Data Annotations | High |
| OpenAPI 3.1 Support | Latest spec with YAML export | High |
| Blazor Prefetching | Automatic framework asset preloading | Medium |
| Server-Sent Events | Enhanced SSE support in Minimal APIs | Medium |
| JSON Patch with STJ | System.Text.Json-native JSON Patch | Medium |
| QuickGrid Enhancements | RowClass, column templates | Low |
Minimal API Validation: First-Class Support
Prior to ASP.NET Core 10, Minimal APIs required manual validation or third-party libraries like FluentValidation. Now, validation is built directly into the framework with Data Annotations and custom validators.
Basic Validation with Data Annotations
using System.ComponentModel.DataAnnotations;
// Define a validated request model
public record CreateOrderRequest(
[Required, StringLength(100)] string CustomerName,
[Required, EmailAddress] string CustomerEmail,
[Required, MinLength(1)] List<OrderItem> Items,
[Range(0, 100)] decimal DiscountPercentage = 0
);
public record OrderItem(
[Required] string ProductId,
[Range(1, 1000)] int Quantity,
[Range(0.01, 100000)] decimal UnitPrice
);
// Validation is automatic when using [Validate] or the new endpoint conventions
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidation(); // New in ASP.NET Core 10
var app = builder.Build();
app.MapPost("/orders", ([Validate] CreateOrderRequest request) =>
{
// Request is guaranteed to be valid here
var order = ProcessOrder(request);
return Results.Created($"/orders/{order.Id}", order);
})
.WithValidation(); // Alternative: chain method for validation
app.Run();
Custom Validators
// Custom validation attribute
public class FutureDateAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext context)
{
if (value is DateTime date && date <= DateTime.UtcNow)
{
return new ValidationResult("Date must be in the future");
}
return ValidationResult.Success;
}
}
// IValidatableObject for complex validation
public record CreateBookingRequest(
[Required] string RoomId,
[Required, FutureDate] DateTime CheckIn,
[Required, FutureDate] DateTime CheckOut,
[Range(1, 10)] int Guests
) : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
if (CheckOut <= CheckIn)
{
yield return new ValidationResult(
"Check-out must be after check-in",
new[] { nameof(CheckIn), nameof(CheckOut) });
}
if ((CheckOut - CheckIn).TotalDays > 30)
{
yield return new ValidationResult(
"Maximum booking duration is 30 days",
new[] { nameof(CheckOut) });
}
}
}
Validation Error Responses
ASP.NET Core 10 returns standardized Problem Details responses for validation failures:
{
"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 not a valid e-mail address."],
"Items": ["The Items field requires at least 1 element."],
"DiscountPercentage": ["The field DiscountPercentage must be between 0 and 100."]
},
"traceId": "00-1234567890abcdef-abcdef123456-00"
}
OpenAPI 3.1 and YAML Support
ASP.NET Core 10 upgrades OpenAPI support to version 3.1 with native YAML export—essential for GitOps workflows and API-first development.
OpenAPI 3.1 Features
- JSON Schema 2020-12: Full JSON Schema support including
$refeverywhere - Webhooks: Define callback URLs your API will call
- PathItems $ref: Reference shared path definitions
- Nullable without wrapper:
type: ["string", "null"]instead ofnullable: true
Configuring OpenAPI
var builder = WebApplication.CreateBuilder(args);
// Configure OpenAPI 3.1
builder.Services.AddOpenApi(options =>
{
options.DocumentName = "v1";
options.Version = "1.0.0";
options.Title = "Order Management API";
options.Description = "API for managing customer orders";
// OpenAPI 3.1 specific options
options.OpenApiVersion = OpenApiVersion.V3_1;
// Add security scheme
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT"
});
// Add server URLs
options.AddServer(new OpenApiServer
{
Url = "https://api.example.com",
Description = "Production"
});
});
var app = builder.Build();
// Serve OpenAPI document in both JSON and YAML
app.MapOpenApi("/openapi/v1.json");
app.MapOpenApi("/openapi/v1.yaml", format: OpenApiFormat.Yaml); // New in 10!
// Programmatic access to the document
app.MapGet("/openapi/document", async (IOpenApiDocumentProvider provider) =>
{
var document = await provider.GetDocumentAsync("v1");
return Results.Ok(document);
});
app.Run();
Enhanced Endpoint Metadata
app.MapPost("/orders", ([Validate] CreateOrderRequest request) => { /* ... */ })
.WithName("CreateOrder")
.WithTags("Orders")
.WithSummary("Create a new order")
.WithDescription("Creates an order and returns the created resource with its ID")
.Produces<Order>(StatusCodes.Status201Created)
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.WithOpenApi(operation =>
{
operation.Deprecated = false;
operation.ExternalDocs = new OpenApiExternalDocs
{
Description = "Order creation guide",
Url = new Uri("https://docs.example.com/orders/create")
};
return operation;
});
Blazor WebAssembly Prefetching
Blazor WebAssembly applications can now automatically prefetch framework static assets, significantly reducing perceived load time.
sequenceDiagram
participant Browser
participant Server
participant Cache as Browser Cache
Browser->>Server: Request index.html
Server-->>Browser: HTML with prefetch hints
par Parallel Prefetch
Browser->>Server: Prefetch dotnet.wasm
Browser->>Server: Prefetch blazor.boot.json
Browser->>Server: Prefetch app assemblies
end
Note over Browser: User sees loading UI
Browser->>Cache: Assets cached
Browser->>Browser: Blazor app starts (fast!)
Enabling Prefetching
// In Program.cs for Blazor WebAssembly
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Enable automatic prefetching (new in ASP.NET Core 10)
builder.Services.ConfigureBlazorWebAssembly(options =>
{
options.EnableStaticAssetPrefetch = true;
options.PrefetchParallelism = 6; // Concurrent downloads
options.PrefetchPriority = FetchPriority.High;
});
await builder.Build().RunAsync();
Standalone Template Updates
The updated Blazor WebAssembly standalone template includes prefetching by default:
<!-- Auto-generated in index.html -->
<link rel="modulepreload" href="_framework/dotnet.10.0.0.wasm" />
<link rel="prefetch" href="_framework/blazor.boot.json" as="fetch" crossorigin="anonymous" />
<link rel="prefetch" href="_framework/MyApp.dll" as="fetch" crossorigin="anonymous" />
<!-- Service worker for aggressive caching -->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js');
}
</script>
Server-Sent Events in Minimal APIs
ASP.NET Core 10 enhances Server-Sent Events (SSE) support for real-time streaming:
app.MapGet("/events/orders", async (HttpContext context, CancellationToken ct) =>
{
context.Response.Headers.ContentType = "text/event-stream";
context.Response.Headers.CacheControl = "no-cache";
await foreach (var orderEvent in GetOrderEventsAsync(ct))
{
await context.Response.WriteAsync($"event: {orderEvent.Type}
", ct);
await context.Response.WriteAsync($"data: {JsonSerializer.Serialize(orderEvent)}
", ct);
await context.Response.Body.FlushAsync(ct);
}
});
// Or use the new SSE result type
app.MapGet("/events/notifications", (CancellationToken ct) =>
{
return Results.ServerSentEvents(async (writer, cancellation) =>
{
var counter = 0;
while (!cancellation.IsCancellationRequested)
{
await writer.WriteEventAsync(new ServerSentEvent
{
Event = "notification",
Data = $"Notification {++counter}",
Id = counter.ToString(),
Retry = TimeSpan.FromSeconds(5)
});
await Task.Delay(1000, cancellation);
}
});
});
JSON Patch with System.Text.Json
JSON Patch operations now work natively with System.Text.Json, eliminating the Newtonsoft.Json dependency:
using System.Text.Json.Patch;
app.MapPatch("/orders/{id}", async (
int id,
JsonPatchDocument<Order> patchDoc,
OrderDbContext db) =>
{
var order = await db.Orders.FindAsync(id);
if (order is null) return Results.NotFound();
// Apply patch operations
var result = patchDoc.ApplyTo(order);
if (!result.IsSuccess)
{
return Results.BadRequest(result.Errors);
}
await db.SaveChangesAsync();
return Results.Ok(order);
});
// Example PATCH request body:
// [
// { "op": "replace", "path": "/status", "value": "shipped" },
// { "op": "add", "path": "/trackingNumber", "value": "1Z999AA10123456784" },
// { "op": "remove", "path": "/notes" }
// ]
QuickGrid Enhancements
The QuickGrid component for Blazor receives useful additions:
@* RowClass for conditional styling *@
<QuickGrid Items="@orders" RowClass="@GetRowClass">
<PropertyColumn Property="@(o => o.Id)" Title="Order #" />
<PropertyColumn Property="@(o => o.CustomerName)" />
<PropertyColumn Property="@(o => o.Total)" Format="C" />
<TemplateColumn Title="Status">
<span class="badge @GetStatusBadgeClass(context)">
@context.Status
</span>
</TemplateColumn>
<TemplateColumn Title="Actions">
<button @onclick="() => ViewOrder(context.Id)">View</button>
</TemplateColumn>
</QuickGrid>
@code {
private string GetRowClass(Order order) => order.Status switch
{
"Cancelled" => "table-danger",
"Shipped" => "table-success",
"Pending" => "table-warning",
_ => ""
};
}
Combine QuickGrid with virtualization (Virtualize="true") and server-side paging for datasets over 1,000 rows. The built-in virtualization renders only visible rows, dramatically improving performance.
Migration Checklist
<!-- Update target framework and packages -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
</ItemGroup>
Key Takeaways
- Minimal API Validation is now built-in with Data Annotations, eliminating the need for FluentValidation in most scenarios.
- OpenAPI 3.1 support with native YAML export enables API-first development and GitOps workflows.
- Blazor Prefetching reduces perceived load time by parallelizing framework asset downloads.
- Server-Sent Events get first-class support with the new Results.ServerSentEvents helper.
- JSON Patch with System.Text.Json removes the last major Newtonsoft.Json dependency.
Conclusion
ASP.NET Core 10 continues the framework’s evolution toward developer productivity without sacrificing performance. The addition of built-in validation for Minimal APIs addresses the most common criticism of the lightweight API style, while OpenAPI 3.1 support positions ASP.NET Core as a first-class choice for API-first development. For teams already on ASP.NET Core, the upgrade path is straightforward—most applications will benefit from these features with minimal code changes.
References
- What’s New in ASP.NET Core 10 – Microsoft Docs
- Announcing ASP.NET Core 10 – .NET Blog
- OpenAPI Specification 3.1
- QuickGrid Documentation
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.