Neu in .NET 10.0 [42]: JSON Patch in ASP.NET-Core-WebAPIs
Die neue JSON-Patch-Implementierung in .NET 10.0 lässt sich auch in WebAPIs verwenden.
(Bild: Pincasso / Shutterstock.com)
- Dr. Holger Schwichtenberg
Seit .NET 10.0 ist es möglich, die JSON-Patch-Implementierung in WebAPIs zu verwenden. Das folgende Beispiel zeigt, wie man das neue NuGet-Paket Microsoft.AspNetCore.JsonPatch.SystemTextJson in einer ASP.NET-Core-10.0-basierten WebAPI anwendet, um ein JSON-Patch-Dokument zu verarbeiten.
Die Datenklassen sind dieselben wie in dem Beispiel zum Beitrag „JSON Patch“ in dieser Blogserie: Person, Address und PhoneNumber.
Die WebAPI-Aktionsmethode besitzt einen Parameter vom Typ JsonPatchDocument<Person>. Das JSON-Patch-Dokument wird im Body des HTTP-Aufrufs erwartet.
(Bild:Â King / stock. adobe.com)
Das ist neu in .NET 11.0: Dr. Holger Schwichtenberg und weitere Experten präsentieren am 17. November 2026 auf der Online-Konferenz betterCode() .NET 11.0 die Änderungen für Entwicklerinnen und Entwickler. Tickets zum Frühbucherpreis sind bis 20.10. im Online-Shop verfügbar.
Leider funktioniert das neue JSON-Patch-Paket nicht in Verbindung mit dem NativeAOT-Compiler, sondern nur mit dem Just-in-Time-Compiler.
Folgender Code zeigt JSON Patch mit Microsoft.AspNetCore.JsonPatch.SystemTextJson:
using System;
using System.Formats.Asn1;
using System.Net.ServerSentEvents;
using System.Runtime.CompilerServices;
using ITVisions;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
using Microsoft.AspNetCore.Mvc;
namespace NET10_WebAPIController.Controllers;
/// <summary>
/// Personendaten holen und aktualisieren mit einem JSON-Patch-Dokument
/// </summary>
[ApiController]
[Route("[controller]")]
public class PersonController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public PersonController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
/// <summary>
/// Diese WebAPI-Operation liefert die Daten zu einer Person anhand der ID
/// </summary>
/// <remarks>Es wird aktuell immer die gleiche Person geliefert ;-(</remarks>
/// <param name="id">ID der Person</param>
/// <returns>Person-Objekt mit Adresse</returns>
[HttpGet]
[Route("/Person_OhneTypedResult/{id}")]
public async Task<Person> Get1(int id = 0)
{
var person = await new PersonManager().GetPersonByIdAsync(id);
return person;
}
/// <summary>
/// Diese WebAPI-Operation liefert die Daten zu einer Person anhand der ID
/// </summary>
/// <remarks>Es wird aktuell immer die gleiche Person geliefert ;-(</remarks>
/// <param name="id">ID der Person</param>
/// <returns>Person-Objekt mit Adresse</returns>
[HttpGet]
[Route("/Person_MitTypedResult/{id}")]
[ProducesResponseType<IEnumerable<WeatherForecast>>(StatusCodes.Status200OK, Description = "Person-Objekt mit Adresse (aus [ProducesResponseType])")]
[ProducesResponseType<ValidationProblem>(StatusCodes.Status400BadRequest, Description = "Fehlerhafte Anfrage / ID muss größer als 0 sein!")]
[ProducesResponseType<ValidationProblem>(StatusCodes.Status404NotFound, Description = "Person nicht gefunden")]
public async Task<Results<Ok<Person>, ValidationProblem, NotFound<ProblemDetails>>> Get_MitTypedResult(int id = 0)
{
if (id <= 0)
{
return TypedResults.ValidationProblem(new Dictionary<string, string[]> { { "id", new[] { "ID muss größer als 0 sein!" } } });
}
var person = await new PersonManager().GetPersonByIdAsync(id);
if (person == null)
{
return TypedResults.NotFound(new ProblemDetails { Title = "Person not found", Detail = $"Person mit ID {id} nicht gefunden!" });
}
return TypedResults.Ok(person);
}
// PATCH https://localhost:7022/person/123
[HttpPatch]
[Route("{id}")]
public async Task<Results<Ok<Person>, ValidationProblem, NotFound<ProblemDetails>>> UpdatePerson(int id, JsonPatchDocument<Person> patchDoc = null)
{
var person = await new PersonManager().GetPersonByIdAsync(id);
if (person == null)
{
return TypedResults.NotFound(new ProblemDetails { Title = "Person not found", Detail = $"No person found with ID {id}" });
}
if (patchDoc != null)
{
Dictionary<string, string[]> errors = new Dictionary<string, string[]>();
// JSON-Patch-Dokument anwenden auf das Person-Objekt
int errorCount = 0;
patchDoc!.ApplyTo(person, patchError =>
{
errors.Add(patchError.AffectedObject.GetType().Name + "#" + ++errorCount, new[] { patchError.ErrorMessage });
});
if (errors.Count > 0)
{
return TypedResults.ValidationProblem(errors);
}
}
return TypedResults.Ok(person);
}
}
Videos by heise
Folgender Beispielcode simuliert die vom obigen Controller genutzte Geschäftslogik:
public class PersonManager
{
public async Task<Person?> GetPersonByIdAsync(int id)
{
// Originalobjekt
var person = new Person
{
FirstName = "Holger",
LastName = "Schwichtenberg",
PrivateEmail = "buero@IT-Visions.de",
PrivateWebsite = "www.dotnet-doktor.de",
CompanyWebsite = "www.IT-Visions.de",
PhoneNumbers = [new PhoneNumber() { Number = "0201 649590-0", Type = PhoneNumberType.Work }],
Address = new Address
{
Street = "Fahrenberg 40b",
City = "Essen",
State = "NRW"
}
};
return await Task.FromResult(person);
}
}
Da man einen PATCH-Aufruf nicht einfach über die Adresszeile eines Webbrowsers auslösen kann, verwendet man zum Testen die HTTP-Tests in Visual Studio (.http-Dateien) oder ein externes Werkzeug wie Fiddler oder Postman. Das JSON-Patch-Dokument übergibt man via HTTP-Body.
(rme)