Quoi de neuf avec les API dans .NET 10 : de réelles améliorations

Cet article présente les mises à jour .NET 10 et C# 14 du point de vue d’un développeur d’API à l’aide d’un exemple concret : une API de gestion des commandes avec validation, des documents OpenAPI et Entity Framework Core.
C’est cette période de l’année : une nouvelle version de la plateforme .NET a été livrée. .NET10 atterri le mois dernier comme une version LTSavec un soutien jusqu’en novembre 2028.
Pour compléter celui de Jon Hilton .NET 10 est arrivé : voici ce qui a changé pour Blazor article et celui d’Assis Zang Quoi de neuf dans .NET 10 pour ASP.NET Coreexaminons les améliorations de .NET 10 du point de vue d’un développeur d’API.
Tout au long de cet article, nous passerons en revue les mises à jour à l’aide d’un exemple concret : une API de gestion des commandes avec validation, la documentation OpenAPI et Entity Framework Core.
NOTE: Les exemples utilisent des API minimales par souci de concision, mais la plupart de ces améliorations peuvent également être utilisées pour les API basées sur des contrôleurs.
Validation intégrée pour les API minimales
Avant .NET 10, les équipes créant des API minimales finissaient par lancer leur propre validation. Le résultat ? Code de point de terminaison qui concernait davantage le contrôle des entrées que la mise en œuvre de la logique métier.
Voici un exemple simplifié qui montre le problème.
public static class OrderEndpoints
{
public static void MapOrderEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/orders");
group.MapPost("https://www.telerik.com/", 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);
}
}
Il existe des moyens de contourner ce problème : vous pouvez utiliser des filtres, des méthodes d’assistance ou des validateurs tiers. Malgré cela, il était frustrant que les API Minimal n’aient pas l’expérience de validation intégrée que vous avez avec les API basées sur un contrôleur.
.NET 10 ajoute la prise en charge de la validation intégrée pour les API minimales. Vous pouvez l’activer avec un seul appel d’inscription :
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<OrderDbContext>();
builder.Services.AddValidation();
var app = builder.Build();
Une fois activé, ASP.NET Core s’applique automatiquement DataAnnotations validation des paramètres API minimaux. Cela inclut la liaison de requête, d’en-tête et de corps de requête.
Vous pouvez également désactiver la validation pour un point de terminaison spécifique en utilisant DisableValidation()ce qui est pratique pour les points de terminaison internes ou les mises à jour partielles où vous acceptez intentionnellement des charges utiles incomplètes.
Avec la validation gérée par le framework et les attributs ajoutés à nos modèles, les points finaux peuvent se concentrer sur la logique métier.
Réponses aux erreurs de validation
Lorsque la validation échoue, ASP.NET Core renvoie un message standardisé ProblemDetails réponse avec un errors dictionnaire.
Une réponse typique ressemble à ceci.
{
"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 : Moderniser la documentation des API
La génération de documents OpenAPI intégrée à .NET 10 prend en charge OpenAPI 3.1 et Schéma JSON 2020-12. La version OpenAPI par défaut pour les documents générés est désormais la 3.1.
OpenAPI 3.1 s’aligne mieux sur les attentes modernes des schémas JSON et améliore la façon dont les outils interprètent vos schémas.
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();
Notez la route YAML : dans .NET 10, vous générez YAML en utilisant une route se terminant par .yaml ou .ymlgénéralement avec {documentName} dans le chemin.
Améliorations des schémas dans la pratique
OpenAPI 3.0 exprimait souvent la nullité en utilisant nullable: true.
components:
schemas:
ShippingAddress:
type: object
nullable: true
properties:
street:
type: string
nullable: true
city:
type: string
OpenAPI 3.1 nous permet d’utiliser des types d’union :
components:
schemas:
ShippingAddress:
type: ["object", "null"]
properties:
street:
type: ["string", "null"]
city:
type: string
Cela a tendance à être plus agréable avec des outils qui s’appuient fortement sur la sémantique du schéma JSON tels que OpenAPI Generator et NSwag.
EF Core 10 : filtres de requête nommés
Les filtres de requêtes globales sont un incontournable pour les applications multi-locataires et les suppressions logicielles. Le problème classique était la granularité : IgnoreQueryFilters() désactivé tous les filtres à la fois.
EF Core 10 présente filtres de requête nommésvous pouvez donc désactiver sélectivement un filtre tout en en conservant un autre.
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);
}
}
Désormais, un point de terminaison administrateur peut désactiver la suppression logicielle sans désactiver l’isolation des locataires :
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);
}
}
Cette capacité est modeste, mais c’est exactement le genre d’amélioration de la sécurité dans le monde réel qui permet d’éviter les fuites de données entre locataires.
Améliorations de C#14
.NET 10 est livré avec C# 14. Pour les développeurs d’API, quelques fonctionnalités réduisent immédiatement le passe-partout et améliorent la lisibilité.
Le mot-clé du champ : Éliminer le passe-partout du champ d’appui
C# 14 introduit des propriétés basées sur des champs, dans lesquelles vous pouvez référencer le champ de sauvegarde généré par le compilateur directement à l’aide du field mot-clé.
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();
}
}
}
Affectations sans conditions
C# 14 autorise les opérateurs conditionnels nuls (?. et ?[]) sur le côté gauche des affectations et des affectations composées. Le côté droit est évalué uniquement lorsque le récepteur n’est pas nul. C’est idéal pour traiter les correctifs.
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: Opérateurs d’incrémentation/décrémentation (++, --) ne sont pas autorisés avec les affectations conditionnelles nulles. Missions composées comme += sont pris en charge.
Pour plus de détails sur cette fonctionnalité, consultez l’article Écrivez du code plus propre avec l’opérateur d’affectation conditionnelle nulle de C# 14.
Membres d’extension : propriétés, membres statiques et opérateurs
La fonctionnalité principale de C# 14 concerne les membres d’extension, qui ajoutent des propriétés d’extension, des membres d’extension statiques et même des opérateurs utilisant le nouveau extension syntaxe de bloc.
Ne fais-tu pas juste amour la syntaxe ?
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"
};
}
}
Pour plus de détails sur les membres d’extension, consultez Propriétés d’extension : fonctionnalité révolutionnaire de C# 14 pour un code plus propre. (Je ne suis pas payé au clic, je le jure.)
Événements envoyés par le serveur : simplifier les mises à jour en temps réel
ASP.NET Core dans .NET 10 ajoute un ServerSentEvents résultat pour les API minimales, afin que vous puissiez diffuser les mises à jour via une seule connexion HTTP sans formater manuellement les images.
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");
}
}
La consommation côté client est également assez simple :
<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>
Devriez-vous passer à .NET 10 ?
.NET 10 est une version LTS. Si vous démarrez un nouveau projet, c’est une évidence.
Si vous effectuez une mise à niveau depuis .NET 8 ou .NET 9, gardez quelques points à l’esprit :
- Validation minimale de l’API: Si vous avez effectué une validation manuelle, .NET 10
AddValidationle support peut supprimer une quantité surprenante de code personnalisé. - OuvrirAPI: La génération OpenAPI intégrée est par défaut 3.1 et prend en charge les points de terminaison YAML via
.yaml/.ymlitinéraires. - Noyau EF: Les filtres de requêtes nommées constituent une véritable mise à niveau de sécurité pour les applications multi-locataires, et la prise en charge des colonnes JSON continue de s’améliorer (y compris la prise en charge des mises à jour groupées).
- C#14: Vous pouvez adopter de nouvelles fonctionnalités progressivement. Même si vous ignorez complètement les membres de l’extension,
fieldet l’affectation conditionnelle nulle apparaîtra rapidement dans votre base de code.
Le chemin de mise à niveau est généralement fluide : changez la cible dans votre .csprojexécutez des tests, corrigez les avertissements et expédiez.
Conclusion
.NET 10 offre des améliorations significatives aux développeurs d’API grâce à des améliorations réfléchies plutôt que des changements révolutionnaires. La combinaison de la validation API minimale intégrée, des fonctionnalités de qualité de vie OpenAPI 3.1 et C# 14 s’ajoute à une expérience de développement plus productive et plus sûre.
Bon codage !
Références
Mises à jour de la plateforme
Mises à jour ASP.NET Core/API
Mises à jour linguistiques
Source link
