Adds JSON Patch functionality to Nancy
C#
Permalink
Failed to load latest commit information.
src Update nuget package in sample project Apr 21, 2016
.gitignore Update assembly info for v0.2 Apr 21, 2016
LICENSE Initial commit Feb 24, 2016
README.md Update README.md Mar 4, 2016

README.md

Nancy.JsonPatch NuGet Version

Adds JSON Patch functionality to Nancy

Patch["/customer/{customerId}"] = _ =>
{
    Customer customer = _repository.Get(_.customerId);

    if (this.JsonPatch(customer))
        _repository.Save(customer);

    return HttpStatusCode.NoContent;
};

This is an example of a JSON Patch document:

[
  { "op": "test", "path": "/a/b/c", "value": "foo" },
  { "op": "remove", "path": "/a/b/c" },
]

For a good introduction to JSON Patch, checkout jsonpatch.com or RFC6902.

Installation

Install via NuGet:

PM > Install-Package Nancy.JsonPatch

Getting Started

Nancy.JsonPatch adds an extension method to NancyModule, so usage is as simple as calling:

this.JsonPatch(myThing);

from anywhere inside your route action.

Nancy.JsonPatch will then deserialize the request body and apply the patch operations to your object.

RFC6902 states that if one operation in the document fails, the entire document should not be considered successful. In other words, you only want to save the changes to your object if all operations in the document succeed.

The .JsonPatch() extension method returns a JsonPatchResult indicating the result of the patch. This can be implicitly converted to a bool, allowing you to do this:

  if (this.JsonPatch(myThing))
      _repository.Save(myThing);

Advanced Error Handling

RFC5789 describes various error codes that should be returned depending on the result of the JSON Patch operation. If you wish to handle failure in this way, the JsonPatchResult object returned from .JsonPatch() should give you the information you need:

public class JsonPatchResult
{
    // True if the operation succeeds, otherwise false
    public bool Succeeded { get; set; }

    // In the event that an operation fails, this will contain a message describing the failure
    public string Message { get; set; }

    // Can be either:
    //  TestFailed - A 'test' operation failed
    //  CouldNotParseJson - The JSON Patch document in the request body is invalid
    //  CouldNotParsePath - A 'path' could not be mapped to a property on the object being patched
    //  CouldNotParseFrom - As above, but for the 'from' path used for copy/move operations
    //  OperationFailed - An operation in the document could not be completed (see the Message for details)
    public JsonPatchFailureReason FailureReason { get; set; }
}

For example:

var patchResult = this.JsonPatch(myThing);
if (!patchResult)
{
    if (patchResult.FailureReason == JsonPatchFailureReason.CouldNotParsePath)
        return HttpStatusCode.Conflict;
    else
        return HttpStatusCode.UnprocessableEntity;
}

_repository.Save(myThing);
return HttpStatusCode.NoContent;

JSON Patch operations in details

Since C# is strongly typed, our implementation has to be slightly different from that described in RFC6902.

This is how Nancy.JsonPatch implements each JsonPatch operation:

Add

[
  { "op": "add", "path": "/Name", "value": "Nancy" }
]

If the 'path' refers to a specific item in a collection (e.g. '/SomeCollection/2'), the item is added before that item.

If the 'path' uses '-', to refer to the end of the collection (e.g '/SomeCollection/-') the item is added to the end of the existing collection.

We can't add properties to objects in .NET. Therefore if the 'path' refers to a property that is not a collection (e.g. '/FirstName'), Add is treated as Replace.

Remove

[
  { "op": "remove", "path": "/Name" }
]

For collections, removes the item from the collection.

Since we can't remove properties from strongly-typed objects, if the 'path' does not refer to a collection then the target property will be set to it's default value (e.g. null for reference types)

Replace

[
  { "op": "replace", "path": "/SomeCollection/1", "value": { "FirstName" : "Nancy"  } }
]

Replaces the property referred to by the 'path' with the new 'value'.

Copy

[
  { "op": "copy", "path": "/Customer/1", "from": "/TopCustomer" }
]

Copies the value of one property to another.

Performs an add operation, taking the value of the item in the 'from' path and adding to the object in referred to by 'path'.

Move

[
  { "op": "move", "path": "/Customer/1", "from": "/TopCustomer" }
]

Moves the value of one property to another.

The same as copy, but performs a remove operation on the original 'path' first.

Test

[
  { "op": "test", "path": "/Customer/1/FirstName", "value": "Nancy" }
]

Tests that the value of the property referred to by the 'path' matches that specified in 'value'. If the test fails, execution of the JSON Patch document stops and an error is returned from the .JsonPatch() method.

So is this fine to use right now?

Yeah! If you do find any issues though, please let me know (or, even better, submit a pull request 😄 ).