Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebApi Put how to tell not specified properties from specified properties set to null?

Here is the scenario. There is an web api put call to change an object in sql server database. We want only to change the fields on the database object if they were explicitly specified on webapi call json. For example:

{ "Name":"newName", "Colour":null }

That should change the Name field to "newName" and "Colour" field to null. As opposed to this json:

{ "Name":"newName" }

that should only change the Name field, leaving the old Colour value intact.

What is a good way with WebApi to detect whether a property was passed or not?

If I define my method like this:

[HttpPut]
[Route("/item/{id}")]
public void ChangeItem(int id, Item item)
{
    ...
}

item.Colour will be null in either case. Note, that I'm working with a variety of data types here and property Colour in the example could be int, string, DateTime, Guid, etc.

I understand that I can get the raw json with [FromBody] attribute and then parse it myself, but it seems that the default binder is already doing most of the work (including validation), so I would be curious how I could reuse it, but also achieve what I want. What is the easiest way?

Update:

I'd like to clarify that mine is an "occasionally connected" scenario. That is, the devices that are using the API are out of network coverage most of the time, and they sync using the API from time to time.

Practically this means that most of the data that is needed to sync are aggregated into zero or one "push updates to server" call followed by "get latest state from server" call. With Sql Server and EF in the back-end that leads to several different (and sometimes unrelated) entities are contained within single json. Eg:

class TaskData
{ 
    public IList<User> AssignedUsers {get; set;} 
    public IList<Product> Products {get; set;} 
    public Task Task {get; set}
}

Also the model classes that are used to generate json for GET calls are separate from EF Entites, as the database schema does not exactly match the API object model.

like image 922
Andrew Savinykh Avatar asked Jun 16 '15 20:06

Andrew Savinykh


People also ask

Can an API property have a null value?

From API developer's point of view, there exist only two types of properties: optional (these MAY contain a value of their specific type but MAY also contain null. This makes it quite clear that when a property is mandatory, ie. required, it can never be null.

What does cannot set property of null mean?

The "Cannot set property of null" error occurs when trying to set a property on a null value. Variables that store a value of null are often returned from methods such as getElementById (), when the element does not exist in the DOM.

Can a mandatory property be null in JavaScript?

This makes it quite clear that when a property is mandatory, ie. required, it can never be null. On the other hand, should an optional property of an object not be set and left empty, I prefer to keep them in the response anyway with the null value.

How do I SET NULL values in OpenAPI?

You can get more details regarding Required and Optional Parameters in the official OAS document. OpenAPI supports values of data types to be null. To specify, one can use the “ nullable: true ” property while defining a data type in OAS version 3.0.0.


2 Answers

I ended up using dynamic proxy for the properties, so that I could mark the properties written by JsonMediaTypeFormatter as "dirty". I used slightly modified yappi (did not really have to modify it, just wanted to - mentioning this if the code below does not exactly match yappi samples/API). I'm guessing you can use your favourite dynamic proxy library. Just for fun I tried to port it to NProxy.Core but that did not work because for some reason json.net refused to write into proxies that NProxy.Core generated.

So it works like this. We have a base class along these lines:

public class DirtyPropertiesBase
{
    ...

    // most of these come from Yappi
    public static class Create<TConcept> where TConcept : DirtyPropertiesBase
    {
        public static readonly Type Type =PropertyProxy.ConstructType<TConcept, PropertyMap<TConcept>>(new Type[0], true);
        public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
    }

    private readonly List<string> _dirtyList = new List<string>();

    protected void OnPropertyChanged(string name)
    {
        if (!_dirtyList.Contains(name))
        {
            _dirtyList.Add(name);
        }
    }
    public bool IsPropertyDirty(string name)
    {
        return _dirtyList.Contains(name);
    }

    ...
    // some more Yappi specific code that calls OnPropertyChanged
    // when a property setter is called
}

Somewhere in the proxy implementation we call OnPropertyChanged so that we remember which properties were written to.

Then we have our custom JsonCreationConverter:

class MyJsonCreationConverter : JsonConverter
{
    private static readonly ConcurrentDictionary<Type, Func<DirtyPropertiesBase>> ContructorCache = new ConcurrentDictionary<Type, Func<DirtyPropertiesBase>>();
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException("MyJsonCreationConverter should only be used while deserializing.");
    }
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        Func<DirtyPropertiesBase> constructor = ContructorCache.GetOrAdd(objectType, x =>
            (Func<DirtyPropertiesBase>)typeof(DirtyPropertiesBase.Create<>).MakeGenericType(objectType).GetField("New").GetValue(null));

        DirtyPropertiesBase value = constructor();
        serializer.Populate(reader, value);
        return value;
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof (DirtyPropertiesBase).IsAssignableFrom(objectType);
    }
}

The idea here, is as JsonMediaTypeFormatter converts incoming json, we substitute the initial empty object to be the proxy that we defined earlier.

We register this converter in WebApiConfig.cs like this

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new MyJsonCreationConverter());

Now when our model is populated from json in place of each object derived from DirtyPropertiesBase there will be a proxy with properly populated _dirtyList collection. Now we only need to map each of these models back to EF entity. This is simple enough with AutoMapper. We register each model like this:

Mapper.CreateMap<Model, Entity>().ForAllMembers(x => x.Condition(z => ((Model)z.Parent.SourceValue).IsPropertyDirty(z.MemberName)));

And then you have your usual mapping code:

Entity current = _db.Entity.Single(x => x.Id == Id);
Mapper.Map(update, current);
_db.SaveChanges();

That will make sure that only Dirty properties are updated.

like image 97
Andrew Savinykh Avatar answered Oct 20 '22 07:10

Andrew Savinykh


Whilst introduced for OData services, you could try using System.Web.Http.OData.Delta<T>. This allows for partial updates of entities.

Take a look at this blog post for a good discussion on using Delta<T>. Essentially it boils down to defining Put and Patch methods such as:

public class MyController : ApiController
{
    // Other actions omitted…

    [AcceptVerbs("Patch")]
    public async Task<IHttpActionResult> Patch(int key, Delta<Item> model)
    {
        var entity = _items.FindAsync(o => o.Id == key);

        if (entity == null) {
            return NotFound();
        }

        model.Patch(entity);

        return StatusCode(HttpStatusCode.NoContent);
    }

    public async Task<IHttpActionResult> Put(int key, Delta<Item> model)
    {
        var entity = _items.FindAsync(o => o.Id == key);

        if (entity == null) {
            return NotFound();
        }

        model.Put(entity);

        return StatusCode(HttpStatusCode.NoContent);
    }
}

Here a request to Put will update the entire model, whereas a request to Patch will only partially update the model (using only the properties passed by the client).

like image 34
Chris Avatar answered Oct 20 '22 08:10

Chris