Web API 2 does not process PATCH requests for Inte

2019-06-24 00:39发布

问题:

I'm having a problem with Web API 2 (.net 4.5.1) in that it seems to ignore PATCH requests where the property is an integer, but processes other types without a problem (I've tested string and decimal).

I’ve setup an unsecured test API with a 'products' controller at http://playapi.azurewebsites.net/api/products. If you do a GET to that URL, you’ll get something like this product back:

{"Id": 1,"Name": "Xbox One","Category": "gaming","Price": 300,"Stock": 5}

‘Name’ and ‘Category’ are both strings, ‘Price’ is a Decimal and ‘Stock’ is an Integer.

If you send these requests, they both work (You’ll get a 200/OK with the updated entity):

  • PATCH, http://playapi.azurewebsites.net/api/products/1 with {"Price": 600.00}
  • PATCH, http://playapi.azurewebsites.net/api/products/1 with {"Category": "Electronics"}

However, if you send this, it returns 200/OK, but does not make the update and the stock remains at the original value

  • PATCH, http://playapi.azurewebsites.net/api/products/1 with {"Stock": 4}

My controller code is fairly standard boiler plate code (from the scaffolded ODATA controller but moved into a standard API controller):

// PATCH: api/Products/5
[AcceptVerbs("PATCH", "MERGE")]
public async Task<IHttpActionResult> PatchOrder(int id, Delta<Product> patch)
{
    Validate(patch.GetEntity());
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    var item = await db.Products.FindAsync(id);
    if (item == null)
    {
        return NotFound();
    }
    patch.Patch(item);
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Ok(item);
}

My model for 'Product' is as follows:

namespace PlayAPI.Models
{
    public class Product
    {
        [Key]
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public double Price { get; set; }
        public int Stock { get; set; }
    }
}

When I debug the controller, I see that the ‘patch’ object has a _changedProperties collection which has no items in it when I do an integer request, but when I do any other kind of request it has the key that I changed.

Should web API support PATCH requests for integer properties? If so, do I need to do anything special on the server or client to make it work?

回答1:

As a quick fix, Change the int to an Int64 on PlayAPI.Models.Product.

public Int64 Stock { get; set; }

It's my understanding that The Delta object used to patch the existing object doesn’t use JSON.net to convert and is silently throwing an Invalid cast exception when it parses JSON and then compares to the existing object from your database. You can read more about the bug over here: http://aspnetwebstack.codeplex.com/workitem/777



回答2:

If you can't actually change the data type successfully, there may be a decent hack fix that you can use. Just attached unreadable data into the query string.

Here's a function you can call from within your Patch functions. As long as you aren't using the query string parameters specifically named what it's looking for, you should be just fine.

/// <summary>
/// Tries to attach additional parameters from the query string onto the delta object. 
/// This uses the parameters extraInt32 and extraInt16, which can be used multiple times.
/// The parameter format is "PropertyName|Integer"
/// <para>Example: ?extraInt32=Prop1|123&extraInt16=Prop2|88&extraInt32=Prop3|null</para>
/// </summary>
[NonAction]
protected void SetAdditionalPatchIntegers<TEntity>(Delta<TEntity> deltaEntity, bool allowNull = true)
{
    var queryParameters = Request.GetQueryNameValuePairs();
    foreach (var param in queryParameters.Where(pair => 
                                pair.Key == "extraInt32" || 
                                pair.Key == "extraInt16"))
    {
        if (param.Value.Count(v => v == '|') != 1)
            continue;
        var splitParam = param.Value.Split('|');

        if (allowNull && 
                (String.IsNullOrWhiteSpace(splitParam[1]) || 
                splitParam[1].Equals("null", StringComparison.OrdinalIgnoreCase)))
        {
            deltaEntity.TrySetPropertyValue(splitParam[0], null);
            continue;
        }

        if (param.Key == "extraInt32")
        {
            int extraInt;
            if (Int32.TryParse(splitParam[1], out extraInt))
            {
                deltaEntity.TrySetPropertyValue(splitParam[0], extraInt);
            }
        }
        if (param.Key == "extraInt16")
        {
            short extraShort;
            if (Int16.TryParse(splitParam[1], out extraShort))
            {
                deltaEntity.TrySetPropertyValue(splitParam[0], extraShort);
            }
        }

    }
}

I really hate that there isn't a better answer, but at least something can be done about it.