ASP.NET Web API 2 and partial updates

Viewed 5291

We are using ASP.NET Web API 2 and want to expose ability to partially edit some object in the following fashion:

HTTP PATCH /customers/1
{
  "firstName": "John",
  "lastName": null
}

... to set firstName to "John" and lastName to null.

HTTP PATCH /customers/1
{
  "firstName": "John"
}

... in order just to update firstName to "John" and do not touch lastName at all. Suppose we have a lot of properties that we want to update with such semantic.

This is quite convenient behavior that is exercised by OData for instance.

The problem is that default JSON serializer will just come up with null in both cases, so it's impossible to distinguish.

I'm looking for some way to annotate model with some kind of wrappers (with value and flag set/unset inside) that would allow to see this difference. Any existing solutions for this?

4 Answers

I know that answers which are already given cover all aspects already, but just want to share concise summary of what we ended up doing and what seems to work for us pretty well.

Created a generic data contract

[DataContract]
public class RQFieldPatch<T>
{
    [DataMember(Name = "value")]
    public T Value { get; set; }
}

Created ad-hoc data cotnracts for patch requests

Sample is below.

[DataContract]
public class PatchSomethingRequest
{
    [DataMember(Name = "prop1")]
    public RQFieldPatch<EnumTypeHere> Prop1 { get; set; }

    [DataMember(Name = "prop2")]
    public RQFieldPatch<ComplexTypeContractHere> Prop2 { get; set; }

    [DataMember(Name = "prop3")]
    public RQFieldPatch<string> Prop3 { get; set; }

    [DataMember(Name = "prop4")]
    public RQFieldPatch<int> Prop4 { get; set; }

    [DataMember(Name = "prop5")]
    public RQFieldPatch<int?> Prop5 { get; set; }
}

Business Logic

Simple.

if (request.Prop1 != null)
{
    // update code for Prop1, the value is stored in request.Prop1.Value
}

Json format

Simple. Not that extensive as "JSON Patch" standard, but covers all our needs.

{
  "prop1": null, // will be skipped
  // "prop2": null // skipped props also skipped as they will get default (null) value
  "prop3": { "value": "test" } // value update requested
}

Properties

  • Simple contracts, simple logic
  • No serialization customization
  • Support for null values assignment
  • Covers any types: value, reference, complex custom types, whatever

Here's my quick and inexpensive solution...

public static ObjectType Patch<ObjectType>(ObjectType source, JObject document)
    where ObjectType : class
{
    JsonSerializerSettings settings = new JsonSerializerSettings
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver()
    };

    try
    {
        String currentEntry = JsonConvert.SerializeObject(source, settings);

        JObject currentObj = JObject.Parse(currentEntry);

        foreach (KeyValuePair<String, JToken> property in document)
        {    
            currentObj[property.Key] = property.Value;
        }

        String updatedObj = currentObj.ToString();

        return JsonConvert.DeserializeObject<ObjectType>(updatedObj);
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

When fetching the request body from your PATCH based method, make sure to take the argument as a type such as JObject. JObject during iteration returns a KeyValuePair struct which inherently simplifies the modification process. This allows you to get your request body content without receiving a deserialized result of your desired type.

This is beneficial due to the fact that you don't need any additional verification for nullified properties. If you want your values to be nullified that also works because the Patch<ObjectType>() method only loops through properties given in the partial JSON document.

With the Patch<ObjectType>() method, you only need to pass your source or target instance, and the partial JSON document that will update your object. This method will apply camelCase based contract resolver to prevent incompatible and inaccurate property names from being made. This method will then serialize your passed instance of a certain type and turned into a JObject.

The method then replaces all properties from the new JSON document to the current and serialized document without any unnecessary if statements.

The method stringifies the current document which now is modified, and deserializes the modified JSON document to your desired generic type.

If an exception occur, the method will simply throw it. Yes, it is rather unspecific but you are the programmer, you need to know what to expect...

This can all be done on a single and simple syntax with the following:

Entity entity = AtomicModifier.Patch<Entity>(entity, partialDocument);

This is what the operation would normally look like:

// Partial JSON document (originates from controller).
JObject newData = new { role = 9001 };

// Current entity from EF persistence medium.
User user = await context.Users.FindAsync(id);

// Output:
//
//     Username : engineer-186f
//     Role     : 1
//
Debug.WriteLine($"Username : {0}", user.Username);
Debug.WriteLine($"Role     : {0}", user.Role);

// Partially updated entity.
user = AtomicModifier.Patch<User>(user, newData);

// Output:
//
//     Username : engineer-186f
//     Role     : 9001
//
Debug.WriteLine($"Username : {0}", user.Username);
Debug.WriteLine($"Role     : {0}", user.Role);

// Setting the new values to the context.
context.Entry(user).State = EntityState.Modified;

This method will work well if you can correctly map your two documents with the camelCase contract resolver.

Enjoy...

Update

I updated the Patch<T>() method with the following code...

public static T PatchObject<T>(T source, JObject document) where T : class
{
    Type type = typeof(T);

    IDictionary<String, Object> dict = 
        type
            .GetProperties()
            .ToDictionary(e => e.Name, e => e.GetValue(source));

    string json = document.ToString();

    var patchedObject = JsonConvert.DeserializeObject<T>(json);

    foreach (KeyValuePair<String, Object> pair in dict)
    {
        foreach (KeyValuePair<String, JToken> node in document)
        {
            string propertyName =   char.ToUpper(node.Key[0]) + 
                                    node.Key.Substring(1);

            if (propertyName == pair.Key)
            {
                PropertyInfo property = type.GetProperty(propertyName);

                property.SetValue(source, property.GetValue(patchedObject));

                break;
            }
        }
    }

    return source;
}

I know I'm a little bit late on this answer, but I think I have a solution that doesn't require having to change serialization and also doesn't include reflection (This article refers you to a JsonPatch library that someone wrote that uses reflection).

Basically create a generic class representing a property that could be patched

    public class PatchProperty<T> where T : class
    {
        public bool Include { get; set; }
        public T Value { get; set; }
    }

And then create models representing the objects that you want to patch where each of the properties is a PatchProperty

    public class CustomerPatchModel
    {
        public PatchProperty<string> FirstName { get; set; }
        public PatchProperty<string> LastName { get; set; }
        public PatchProperty<int> IntProperty { get; set; }
    }

Then your WebApi method would look like

    public void PatchCustomer(CustomerPatchModel customerPatchModel)
    {
        if (customerPatchModel.FirstName?.Include == true)
        {
            // update first name 
            string firstName = customerPatchModel.FirstName.Value;
        }
        if (customerPatchModel.LastName?.Include == true)
        {
            // update last name
            string lastName = customerPatchModel.LastName.Value;
        }
        if (customerPatchModel.IntProperty?.Include == true)
        {
            // update int property
            int intProperty = customerPatchModel.IntProperty.Value;
        }
    }

And you could send a request with some Json that looks like

{
    "LastName": { "Include": true, "Value": null },
    "OtherProperty": { "Include": true, "Value": 7 }
}

Then we would know to ignore FirstName but still set the other properties to null and 7 respectively.

Note that I haven't tested this and I'm not 100% sure it would work. It would basically rely on .NET's ability to serialize the generic PatchProperty. But since the properties on the model specify the type of the generic T, I would think it would be able to. Also since we have "where T : class" on the PatchProperty declaration, the Value should be nullable. I'd be interested to know if this actually works though. Worst case you could implement a StringPatchProperty, IntPatchProperty, etc. for all your property types.

Related