Search code examples
c#asp.net-corejson-patch

SnakeCaseNamingStrategy and JsonPatch in ASP.NET Core


Is there a way to register/use a "global" ContractResolver when using the the ApsNetCore.JsonPatch (2.1.1) package?

I ran into an issue where the path was not resolved because the properties in my Models are in PascalCase but the path in the JsonPatch is in SnakeCase.

In this case I have to set the ContractResolver on the JsonPatchDocument to the Default/Globally registered ContractResolver in the Startup.cs file.

It works but I would have to do this for every Patch Route I am going to implement.

Startup configuration:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
  services
    .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver
    {
      NamingStrategy = new SnakeCaseNamingStrategy()
    })
}

Controller:

[HttpPatch("{id}"]
[Consumes(MediaTypeNames.Application.Json)]
public async Task<IActionResult> Patch(string id,
    [FromBody] JsonPatchDocument<Entity> patchEntity)
{
    ...
    patchEntity.ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new SnakeCaseNamingStrategy()
    };
    patchEntity.ApplyTo(entity);
    ...

Solution

  • It appears that there is no easy way to affect the ContractResolver that is used when creating an instance of JsonPatchDocument<T>. Instances of this class are created by a TypedJsonPatchDocumentConverter, like this code snippet shows:

    var container = Activator.CreateInstance(
        objectType,
        targetOperations,
        new DefaultContractResolver());
    

    It's clear here that DefaultContractResolver is hardcoded as the second argument when creating an instance of JsonPatchDocument<T>.

    One option for handling this when using ASP.NET Core MVC is to use an Action Filter, which allows for making changes to any arguments that are being passed into an action. Here's a basic example:

    public class ExampleActionFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext ctx)
        {
            // Find a single argument we can treat as IJsonPatchDocument.
            var jsonPatchDocumentActionArgument = ctx.ActionArguments.SingleOrDefault(
                x => typeof(IJsonPatchDocument).IsAssignableFrom(x.Value.GetType()));
    
            // Here, jsonPatchDocumentActionArgument.Value will be null if none was found.
            var jsonPatchDocument = jsonPatchDocumentActionArgument.Value as IJsonPatchDocument;
    
            if (jsonPatchDocument != null)
            {            
                jsonPatchDocument.ContractResolver = new DefaultContractResolver
                {
                    NamingStrategy = new SnakeCaseNamingStrategy()
                };
            }
        }
    }
    

    The ActionExecutingContext class passed in here includes an ActionArguments property, which is used in this example to attempt to find an argument of type IJsonPatchDocument. If one is found, we override the ContractResolver accordingly.

    In order to use this new action filter, you can add it to a controller, action or register it globally. Here's how to register it globally (there are many answers for the other options, so I won't dive deep into that here):

    services.AddMvc(options =>
    {
        options.Filters.Add(new ExampleActionFilterAttribute());
    });