it-swarm.com.de

Web-API 2 - PATCH implementieren

Ich habe derzeit eine Web-API, die eine RESTFul-API implementiert. Das Modell für meine API sieht folgendermaßen aus:

public class Member
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Created { get; set; }
    public DateTime BirthDate { get; set; }
    public bool IsDeleted { get; set; }
}

Ich habe eine PUT-Methode zum Aktualisieren einer Zeile ähnlich der folgenden implementiert (der Kürze halber habe ich einige nicht relevante Dinge ausgelassen):

[Route("{id}")]
[HttpPut]
public async System.Threading.Tasks.Task<HttpResponseMessage> UpdateRow(int id, 
    [FromBody]Models.Member model)
{
    // Do some error checking
    // ...
    // ...

    var myDatabaseEntity = new BusinessLayer.Member(id);
    myDatabaseEntity.FirstName = model.FirstName;
    myDatabaseEntity.LastName = model.LastName;
    myDatabaseEntity.Created = model.Created;
    myDatabaseEntity.BirthDate = model.BirthDate;
    myDatabaseEntity.IsDeleted = model.IsDeleted;

    await myDatabaseEntity.SaveAsync();
}

Mit PostMan kann ich folgende JSON senden und alles funktioniert einwandfrei:

{
    firstName: "Sara",
    lastName: "Smith",
    created: '2018/05/10",
    birthDate: '1977/09/12",
    isDeleted: false
}

Wenn ich diesen Code als PUT-Anforderung an http://localhost:8311/api/v1/Member/12 sende, wird der Datensatz in meinen Daten mit der ID 12 auf das aktualisiert, was Sie in der JSON sehen.

Was ich jedoch tun möchte, ist ein PATCH-Verb zu implementieren, in dem ich Teilaktualisierungen durchführen kann. Wenn Sara heiratet, würde ich gerne diese JSON senden können:

{
    lastName: "Jones"
}

Ich möchte in der Lage sein, nur dieses JSON zu senden und JUST das Feld LastName zu aktualisieren und alle anderen Felder in Ruhe zu lassen.

Ich habe das versucht:

[Route("{id}")]
[HttpPatch]
public async System.Threading.Tasks.Task<HttpResponseMessage> UpdateRow(int id, 
    [FromBody]Models.Member model)
{
}

Mein Problem ist, dass dies alle Felder im model-Objekt zurückgibt (alle außer Nullen mit Ausnahme des LastName-Felds). Dies ist sinnvoll, da ich sage, ich möchte ein Models.Member-Objekt. Ich würde gerne wissen, ob es eine Möglichkeit gibt, herauszufinden, welche Eigenschaften tatsächlich in der JSON-Anforderung gesendet wurden, sodass ich nur diese Felder aktualisieren kann.

6
Icemanind

PATCH-Vorgänge werden aus diesem Grund normalerweise nicht mit demselben Modell wie die POST- oder PUT-Vorgänge definiert: Wie unterscheiden Sie zwischen einer null und einem don't change. Von der IETF :

Bei PATCH enthält die eingeschlossene Entität jedoch eine Menge von Anweisungen, die beschreiben, wie sich eine Ressource derzeit im .__ befindet. Der Origin-Server sollte geändert werden, um eine neue Version zu erstellen.

Sie können hier nach ihrem PATCH-Vorschlag suchen, aber sumarilly ist:

[
    { "op": "test", "path": "/a/b/c", "value": "foo" },
    { "op": "remove", "path": "/a/b/c" },
    { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
    { "op": "replace", "path": "/a/b/c", "value": 42 },
    { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
    { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
]
9
Tipx

Ich hoffe, dies hilft bei der Verwendung von Microsoft JsonPatchDocument:

.Net Core 2.1-Patch-Aktion in einen Controller:

[HttpPatch("{id}")]
public IActionResult Patch(int id, [FromBody]JsonPatchDocument<Node> value)
{
    try
    {
        //nodes collection is an in memory list of nodes for this example
        var result = nodes.FirstOrDefault(n => n.Id == id);
        if (result == null)
        {
            return BadRequest();
        }    
        value.ApplyTo(result, ModelState);//result gets the values from the patch request
        return NoContent();
    }
    catch (Exception ex)
    {
        return StatusCode(StatusCodes.Status500InternalServerError, ex);
    }
}

Knotenmodellklasse:

[DataContract(Name ="Node")]
public class Node
{
    [DataMember(Name = "id")]
    public int Id { get; set; }

    [DataMember(Name = "node_id")]
    public int Node_id { get; set; }

    [DataMember(Name = "name")]
    public string Name { get; set; }

    [DataMember(Name = "full_name")]
    public string Full_name { get; set; }
}

Ein gültiger Patch JSon zum Aktualisieren der Eigenschaften "full_name" und "node_id" besteht aus einer Reihe von Vorgängen wie:

[
  { "op": "replace", "path": "full_name", "value": "NewNameWithPatch"},
  { "op": "replace", "path": "node_id", "value": 10}
]

Wie Sie sehen, ist "op" die Operation, die Sie ausführen möchten. Die häufigste ist "replace", bei der nur der vorhandene Wert dieser Eigenschaft für die neue festgelegt wird. Es gibt jedoch noch weitere:

[
  { "op": "test", "path": "property_name", "value": "value" },
  { "op": "remove", "path": "property_name" },
  { "op": "add", "path": "property_name", "value": [ "value1", "value2" ] },
  { "op": "replace", "path": "property_name", "value": 12 },
  { "op": "move", "from": "property_name", "path": "other_property_name" },
  { "op": "copy", "from": "property_name", "path": "other_property_name" }
]

Hier ist eine Erweiterungsmethode, die ich basierend auf der Patch-Spezifikation ("replace") in C # mit Reflection erstellt habe. Mit dieser Methode können Sie jedes Objekt serialisieren, um einen Patch-Vorgang ("replace") auszuführen. Sie können auch die gewünschte Codierung übergeben Geben Sie den HTTP-Inhalt (StringContent) zurück, der zum Senden an httpClient.PatchAsync (endPoint, httpContent) bereit ist:

public static StringContent ToPatchJsonContent(this object node, Encoding enc = null)
{
    List<PatchObject> patchObjectsCollection = new List<PatchObject>();

    foreach (var prop in node.GetType().GetProperties())
    {
        var patch = new PatchObject{ Op = "replace", Path = prop.Name , Value = prop.GetValue(node) };
        patchObjectsCollection.Add(patch);                
    }

    MemoryStream payloadStream = new MemoryStream();
    DataContractJsonSerializer serializer = new DataContractJsonSerializer(patchObjectsCollection.GetType());
    serializer.WriteObject(payloadStream, patchObjectsCollection);
    Encoding encoding = enc ?? Encoding.UTF8;
    var content = new StringContent(Encoding.UTF8.GetString(payloadStream.ToArray()), encoding, "application/json");

    return content;
}

}

Es wurde festgestellt, dass tt auch diese Klasse verwendet, die ich erstellt habe, um das PatchObject mithilfe von DataContractJsonSerializer zu serialisieren:

[DataContract(Name = "PatchObject")]
class PatchObject
{
    [DataMember(Name = "op")]
    public string Op { get; set; }
    [DataMember(Name = "path")]
    public string Path { get; set; }
    [DataMember(Name = "value")]
    public object Value { get; set; }
}

Ein C # -Beispiel für die Verwendung der Erweiterungsmethode und das Aufrufen der Patch-Anforderung mit HttpClient:

    var nodeToPatch = new { Name = "TestPatch", Private = true };//You can use anonymous type
    HttpContent content = nodeToPatch.ToPatchJsonContent();//Invoke the extension method to serialize the object

    HttpClient httpClient = new HttpClient();
    string endPoint = "https://localhost:44320/api/nodes/1";
    var response = httpClient.PatchAsync(endPoint, content).Result;

Vielen Dank

7
Ernest

Die Antwort von @ Tipx bezüglich PATCH ist genau richtig, aber wie Sie wahrscheinlich bereits festgestellt haben, ist das Erreichen einer statisch typisierten Sprache wie C # eine nicht triviale Übung.

Wenn Sie eine PATCH verwenden, um eine Gruppe von Teilaktualisierungen für eine einzelne Domänenentität darzustellen (z. B. zum Aktualisieren des Vornamens und Nachnamens nur für einen Kontakt mit vielen weiteren Eigenschaften), müssen Sie etwas entlang der Zeilen von tun Schleifen Sie jede Anweisung in der 'PATCH'-Anforderung und wenden Sie diese Anweisung dann auf eine Instanz Ihrer Klasse an.

Die Anwendung einer einzelnen Anweisung umfasst dann 

  • Suchen der Eigenschaft der Instanz, die dem Namen in der Anweisung __. Entspricht, oder Behandlung von Eigenschaftennamen, die Sie nicht erwartet haben
  • Für ein Update: Der Versuch, den im Patch übermittelten Wert in die Instanz-Eigenschaft zu parsen und den Fehler zu behandeln, wenn z. Die Instanzeigenschaft ist ein Bool, aber die Patch-Anweisung enthält ein Datum
  • Entscheiden, was mit Anweisungen zum Hinzufügen geschehen soll, da keine neuen Eigenschaften zu einer statisch typisierten C # -Klasse hinzugefügt werden können. Ein Ansatz besteht darin, zu sagen, dass Add "den Wert der Eigenschaft der Instanz nur dann setzt, wenn der vorhandene Wert der Eigenschaft null ist".

Bei Web API 2 für das vollständige .NET Framework scheint das JSONPatch-github-Projekt einen Versuch zu unternehmen, diesen Code bereitzustellen, obwohl es nicht so aussieht, als sei das Repo in letzter Zeit viel weiterentwickelt worden, und die Readme-Datei wird angezeigt : 

Dies ist immer noch ein sehr frühes Projekt, verwenden Sie es nicht in der Produktion es sei denn, Sie verstehen die Quelle und haben nichts dagegen, ein paar Fehler zu beheben. ;)

In .NET Core sind die Dinge einfacher, da dies über eine Reihe von Funktionen zur Unterstützung im Microsoft.AspNetCore.JsonPatch-Namespace verfügt.

Auf der recht nützlichen Site jsonpatch.com sind auch einige weitere Optionen für Patch in .NET aufgeführt:

  • Asp.Net Core JsonPatch (offizielle Implementierung von Microsoft)
  • Ramone (ein Framework für die Verwendung von REST - Diensten, enthält eine JSON-Patch-Implementierung)
  • JsonPatch (fügt der ASP.NET-Web-API Unterstützung für JSON-Patch hinzu)
  • Starcounter (In-Memory Application Engine, verwendet JSON-Patch mit OT für die Client-Server-Synchronisierung)
  • Nancy.JsonPatch (fügt NancyFX JSON-Patch-Unterstützung hinzu)
  • Manatee.Json (JSON-alles einschließlich JSON-Patch)

Ich muss diese Funktionalität zu einem vorhandenen Web-API-2-Projekt hinzufügen, also werde ich diese Antwort aktualisieren, wenn ich etwas anderes finde, das nützlich ist.

0
tomRedox