Web API 2 - Implementing a PATCH

37,969

Solution 1

PATCH operations aren't usually defined using the same model as the POST or PUT operations exactly for that reason: How do you differentiate between a null, and a don't change. From the IETF:

With PATCH, however, the enclosed entity contains a set of instructions describing how a resource currently residing on the origin server should be modified to produce a new version.

You can look here for their PATCH suggestion, but sumarilly is:

[
    { "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" }
]

Solution 2

I hope this helps using Microsoft JsonPatchDocument:

.Net Core 2.1 Patch Action into a 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);
    }
}

Node Model class:

[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; }
}

A valid Patch JSon to update just the "full_name" and the "node_id" properties will be an array of operations like:

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

As you can see "op" is the operation you would like to perform, the most common one is "replace" which will just set the existing value of that property for the new one, but there are others:

[
  { "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" }
]

Here is an extensions method I built based on the Patch ("replace") specification in C# using reflection that you can use to serialize any object to perform a Patch ("replace") operation, you can also pass the desired Encoding and it will return the HttpContent (StringContent) ready to be sent to httpClient.PatchAsync(endPoint, httpContent):

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;
}

}

Noticed that tt also uses this class I created to serialize the PatchObject using DataContractJsonSerializer:

[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; }
}

A C# example of how to use the extension method and invoking the Patch request using 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;

Thanks

Solution 3

@Tipx's answer re using PATCH is spot on, but as you've probably already found, actually achieving that in a statically typed language like C# is a non-trivial exercise.

In the case where you're using a PATCH to represent a set of partial updates for a single domain entity (e.g. to update the first name and last name only for a contact with many more properties) you need to do something along the lines of looping each instruction in the 'PATCH' request and then applying that instruction to an instance of your class.

Applying an individual instruction will then comprise of

  • Finding the property of the instance that matches the name in the instruction, or handling property names you weren't expecting
  • For an update: Trying to parse the value submitted in the patch into the instance property and handling the error if e.g. the instance property is a bool but the patch instruction contains a date
  • Deciding what to do with Add instructions as you can't add new properties to a statically typed C# class. One approach is to say that Add means "set the value of the instance's property only if property's existing value is null"

For Web API 2 on the full .NET Framework the JSONPatch github project looks to make a stab at providing this code, although it doesn't look like there's been a lot of development on that repo recently and the readme does state:

This is still very much an early project, don't use it in production yet unless you understand the source and don't mind fixing a few bugs ;)

Things are simpler on .NET Core as that has a set of functionality to support this in the Microsoft.AspNetCore.JsonPatch namespace.

The rather useful jsonpatch.com site also lists out a few more options for Patch in .NET:

  • Asp.Net Core JsonPatch (Microsoft official implementation)
  • Ramone (a framework for consuming REST services, includes a JSON Patch implementation)
  • JsonPatch (Adds JSON Patch support to ASP.NET Web API)
  • Starcounter (In-memory Application Engine, uses JSON Patch with OT for client-server sync)
  • Nancy.JsonPatch (Adds JSON Patch support to NancyFX)
  • Manatee.Json (JSON-everything, including JSON Patch)

I need to add this functionality to an existing Web API 2 project of ours, so I'll update this answer if I find anything else that's useful while doing that.

Solution 4

I wanted to achieve exactly the same thing, but used a different method to others described here. I've created a working repo using this if you want to check it out:

https://github.com/emab/patch-example

If you have the following two models:

Database model

public class WeatherDBModel
    {
        [Key]
        public int Id { get; set; }
        public string City { get; set; }
        public string Country { get; set; }
        public double Temperature { get; set; }
        public double WindSpeed { get; set; }
        public double Rain { get; set; }

        public Weather(int id, string city, string country, double temperature, double windSpeed, double rain)
        {
            Id = id;
            City = city;
            Country = country;
            Temperature = temperature;
            WindSpeed = windSpeed;
            Rain = rain;
        }
    }

Update model

Containing exact names of database model properties. Includes properties which can be updated

public class WeatherUpdateModel
{
      public string? City { get; set; }
      public string? Country { get; set; }
      public double Temperature { get; set; }
      public double WindSpeed { get; set; }
      public double Rain { get; set; }
}

This update model is sent to the service layer along with the id of the object you'd like to update.

You can then implement the following method in your repository layer which maps any non-null values from the updateModel into an existing entity if it has been found:

public Weather Update(int id, WeatherUpdate updateObject)
{
    // find existing entity
    var existingEntity = _context.Weather.Find(id);
    
    // handle not found
    if (existingEntity == null)
    {
        throw new EntityNotFoundException(id);
    }

    // iterate through all of the properties of the update object
    // in this example it includes all properties apart from `id`
    foreach (PropertyInfo prop in updateObject.GetType().GetProperties())
    {
        // check if the property has been set in the updateObject
        // if it is null we ignore it. If you want to allow null values to be set, you could add a flag to the update object to allow specific nulls
        if (prop.GetValue(updateObject) != null)
        {
            // if it has been set update the existing entity value
            existingEntity.GetType().GetProperty(prop.Name)?.SetValue(existingEntity, prop.GetValue(updateObject));               
        }
    }
    _context.SaveChanges();
    return existingEntity;
}

Using this method you can change your models without worrying about the update logic, as long as you ensure that the UpdateModel is kept up-to-date with the database model.

Share:
37,969

Related videos on Youtube

Icemanind
Author by

Icemanind

I am a .NET developer. I am proficient in C# and I use ASP.Net Core, Winforms and WPF. I also dabble in React and Xamarin.

Updated on March 15, 2022

Comments

  • Icemanind
    Icemanind about 2 years

    I currently have a Web API that implements a RESTFul API. The model for my API looks like this:

    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; }
    }
    

    I've implemented a PUT method for updating a row similar to this (for brevity, I've omitted some non-relevant stuff):

    [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();
    }
    

    Using PostMan, I can send the following JSON and everything works fine:

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

    If I send this as my body to http://localhost:8311/api/v1/Member/12 as a PUT request, the record in my data with ID of 12 gets updated to what you see in the JSON.

    What I would like to do though is implement a PATCH verb where I can do partial updates. If Sara gets married, I would like to be able to send this JSON:

    {
        lastName: "Jones"
    }
    

    I would like to be able to send just that JSON and update JUST the LastName field and leave all the other fields alone.

    I tried this:

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

    My problem is that this returns all the fields in the model object (all of them are nulls except the LastName field), which makes sense since I am saying I want a Models.Member object. What I would like to know is if there is a way to detect which properties have actually been sent in the JSON request so I can update just those fields?

    • mexanichp
      mexanichp almost 5 years
      If for some reason you're not going to use JsonPatchDocument and your client wants to send a direct API call with specifying only a few properties, as you have in the example - take a look at the patcharp library github.com/mexanichp/patcharp This can give you a direction or once it's released you may use it as a NuGet package.
  • jramm
    jramm almost 4 years
    Downvoted because of the misleading implication that the IETF has suggested this format. The "here" site you link to doesn't seem to have any affiliation to IETF after reviewing their About and Contact Us pages.
  • Andrew Stevens
    Andrew Stevens over 3 years
    @jramm - agree with the downvote; I had the same reaction. Doing some further research though, the IETF does actually have a separate RFC which describes the same format as in the link Tipx gave - RFC 6902, tools.ietf.org/html/rfc6902. 6902 is a subset of 5987, so not mandatory - perhaps not even recommended - that it's used. Tipx maybe update your answer accordingly?
  • Tipx
    Tipx over 3 years
    Nah, I'll leave it as is. The sample I gave is exactly the one on the RFC you linked, in the "document structure" section. While it's not a 100% complete answer, I stand by my original idea that it's a good piece of information to guide in the proper direction. The main idea behind my answer is "be careful, a patch is not just an SQL upsert". If you want to edit the answer, be my guess!
  • jimebe
    jimebe about 3 years
    What if you are trying to set the value on the db model back to null?
  • emab
    emab about 3 years
    @jimebe I suppose that is a limitation of this method. Maybe you could instead have a list of properties that you'd like to update, and if the property is included in that list it is updated, otherwise it is not. That way you could use any property value you wanted, including null, and as long as it is included in the list it would be applied as an update.
  • hormberg
    hormberg over 2 years
    @emab: I think some of your variables are renamed for clarity, but you missed a few of them. Could you please update for future readers?
  • emab
    emab over 2 years
    @hormberg I've updated the code example above, and included a link to a working solution using that code on GitHub. Hope it helps!