ASP.NET Web API 2 and partial updates

We are using ASP.NET Web API 2 and want to provide the ability to partially edit some objects as follows:

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

      

... set firstName

to "John"

and lastName

to null

.

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

      

... to just update firstName

to "John"

and not touch at all lastName

. Let's say we have a lot of properties that we want to update with this semantics.

This is a pretty handy behavior, for example, for OData .

The problem is that the default JSON serializer will just accept a value null

in both cases, so it cannot be distinguished.

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

+5


source to share


2 answers


I misunderstood the problem at first. When I was working with Xml I thought it was pretty easy. Just add a property attribute and leave the property blank. But as I found out, John doesn't work like that. Since I was looking for a solution that works for both xml and json, you will find xml links in this answer. Another thing, I wrote this with a C # client.

The first step is to create two classes for serialization.

public class ChangeType
{
    [JsonProperty("#text")]
    [XmlText]
    public string Text { get; set; }
}

public class GenericChangeType<T> : ChangeType
{
}

      

I chose for the generic and non-generic class because it is difficult to classify it as a generic type when it doesn't matter. Also, the xml implementation requires the XmlText to be a string.

XmlText is the actual value of the property. The advantage is that you can add attributes to this object and the fact that it is an object and not just a string. In Xml, it looks like this:<Firstname>John</Firstname>

It doesn't work for Json. Json doesn't know any attributes. So for Json it's just a class with properties. To implement the idea of ​​xml value (I'll get this later), I renamed the property to #text. It's just a convention.

Since the XmlText is a string (and we want to serialize the string), it's fine to store the value without regard to type. But in the case of serialization, I want to know the actual type.

The downside is that the viewmodel has to refer to these types, the advantage is that properties are strongly typed for serialization:

public class CustomerViewModel
{
    public GenericChangeType<int> Id { get; set; }
    public ChangeType Firstname { get; set; }
    public ChangeType Lastname { get; set; }
    public ChangeType Reference { get; set; }
}

      

Suppose I am setting values:

var customerViewModel = new CustomerViewModel
{
    // Where int needs to be saved as string.
    Id = new GenericeChangeType<int> { Text = "12" },
    Firstname = new ChangeType { Text = "John" },
    Lastname = new ChangeType { },
    Reference = null // May also be omitted.
}

      

In xml it will look like this:

<CustomerViewModel>
  <Id>12</Id>
  <Firstname>John</Firstname>
  <Lastname />
</CustomerViewModel>

      

This is enough for the server to detect the change. But with json it will generate the following:

{
    "id": { "#text": "12" },
    "firstname": { "#text": "John" },
    "lastname": { "#text": null }
}

      

It might work because in my implementation the receiving viewmodel has the same definition. But since you are only talking about serialization, and if you are using a different implementation, you need:



{
    "id": 12,
    "firstname": "John",
    "lastname": null
}

      

Here we need to add a custom json converter to generate this result. The relevant code is in WriteJson, assuming you only add this converter to your serializer settings. But for completeness, I have added the readJson code.

public class ChangeTypeConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        // This is important, we can use this converter for ChangeType only
        return typeof(ChangeType).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var value = JToken.Load(reader);

        // Types match, it can be deserialized without problems.
        if (value.Type == JTokenType.Object)
            return JsonConvert.DeserializeObject(value.ToString(), objectType);

        // Convert to ChangeType and set the value, if not null:
        var t = (ChangeType)Activator.CreateInstance(objectType);
        if (value.Type != JTokenType.Null)
            t.Text = value.ToString();
        return t;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var d = value.GetType();

        if (typeof(ChangeType).IsAssignableFrom(d))
        {
            var changeObject = (ChangeType)value;

            // e.g. GenericChangeType<int>
            if (value.GetType().IsGenericType)
            {
                try
                {
                    // type - int
                    var type = value.GetType().GetGenericArguments()[0];
                    var c = Convert.ChangeType(changeObject.Text, type);
                    // write the int value
                    writer.WriteValue(c);
                }
                catch
                {
                    // Ignore the exception, just write null.
                    writer.WriteNull();
                }
            }
            else
            {
                // ChangeType object. Write the inner string (like xmlText value)
                writer.WriteValue(changeObject.Text);
            }
            // Done writing.
            return;
        }
        // Another object that is derived from ChangeType.
        // Do not add the current converter here because this will result in a loop.
        var s = new JsonSerializer
        {
            NullValueHandling = serializer.NullValueHandling,
            DefaultValueHandling = serializer.DefaultValueHandling,
            ContractResolver = serializer.ContractResolver
        };
        JToken.FromObject(value, s).WriteTo(writer);
    }
}

      

At first I tried to add a converter class: [JsonConverter(ChangeTypeConverter)]

. But the problem is that the converter will always be used, which creates a reference loop (as also mentioned in the comment in the code above). Also you can use this converter for serialization only. This is why I only added it to the serializer:

var serializerSettings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore,
    DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
    Converters = new List<JsonConverter> { new ChangeTypeConverter() },
    ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
};
var s = JsonConvert.SerializeObject(customerViewModel, serializerSettings);

      

This will create the json I was looking for and should be sufficient for the server to detect the changes.

- update -

Since this answer focuses on serialization, the most important thing is that lastname is part of the serialization string. Then it is up to the host how to deserialize the string in the object again.

Serialization and deserialization use different settings. To deserialize again, you can use:

var deserializerSettings = new JsonSerializerSettings
{
    //NullValueHandling = NullValueHandling.Ignore,
    DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
    Converters = new List<JsonConverter> { new Converters.NoChangeTypeConverter() },
    ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
};
var obj = JsonConvert.DeserializeObject<CustomerViewModel>(s, deserializerSettings);

      

If you are using the same classes for deserialization, then Request.Lastname should be ChangeType, with text = null.

I am not sure why removing NullValueHandling from deserialization parameters is causing problems in your case. But you can overcome this by writing an empty object as a value instead of zero. In the transformer, the current ReadJson can already handle this. But there must be a modification in the WriteJson. Instead, writer.WriteValue(changeObject.Text);

you need something like:

if (changeObject.Text == null)
    JToken.FromObject(new ChangeType(), s).WriteTo(writer);
else
    writer.WriteValue(changeObject.Text);

      

This will lead to:

{
    "id": 12,
    "firstname": "John",
    "lastname": {}
}

      

+2


source


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 retrieving the request body from your PATCH based method, make sure the argument is accepted as a type such as JObject. JObject returns a KeyValuePair structure during iteration, which by its nature makes the modification process easier. This allows you to get the content of the request body without getting a deserialized result of the type you want.

This is useful because you don't need additional validation for invalidated properties. If you want your values ​​to be overridden, this also works because the method Patch<ObjectType>()

only Patch<ObjectType>()

looks at the properties set in the partial JSON document.

When used, Patch<ObjectType>()

you only need to pass in the source or target instance and a partial JSON document that will update your object. This method will use a camelCase-based contract handler to prevent the generation of inconsistent and imprecise property names. This method then serializes your passed instance of a specific type and turns into a JObject.

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

The method converts the current document that is currently modified and deserializes the modified JSON document to the required generic type.



If an exception is thrown, the method will simply throw it. Yes, that's pretty vague, but as a programmer, you need to know what to expect ...

All this can be done with a single, 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 = 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 updatedUser = 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).CurrentValues.SetValues(updatedUser);

      

This method will work well if you can match the two documents correctly using the camelCase contract recognizer.

Enjoy...

0


source







All Articles