ASP.NET Core 10: Minimal API Validation, OpenAPI 3.1 YAML, and Blazor Prefetching

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

FeatureDescriptionImpact
Minimal API ValidationBuilt-in validation with Data AnnotationsHigh
OpenAPI 3.1 SupportLatest spec with YAML exportHigh
Blazor PrefetchingAutomatic framework asset preloadingMedium
Server-Sent EventsEnhanced SSE support in Minimal APIsMedium
JSON Patch with STJSystem.Text.Json-native JSON PatchMedium
QuickGrid EnhancementsRowClass, column templatesLow

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 $ref everywhere
  • Webhooks: Define callback URLs your API will call
  • PathItems $ref: Reference shared path definitions
  • Nullable without wrapper: type: ["string", "null"] instead of nullable: 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",
        _ => ""
    };
}
💡
PERFORMANCE TIP

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


Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.