KevinDockx / JsonPatch

JSON Patch (JsonPatchDocument) RFC 6902 implementation for .NET
MIT License
174 stars 28 forks source link

Is it possible to ApplyTo() from a viewModel to a model? #98

Closed MikeAlhayek closed 4 years ago

MikeAlhayek commented 4 years ago

First of all, thank you for this great package!

It seems that JsonPatchDocument<> expect an entity-model as a generic type in order to call the ApplyTo<> method to patch the entity-model.

This works great for the most part. However, I personally don't like to add validation rules to my entity-model. I rather add validation rules to a view-model/request-model. This way I have one entity-model representing the data being stored, and one entity-model per request representing incoming data from a request. The request-model would contain data annotation validation rules which are specific to the user's request.

Let's take the following code as example. In this example, I pass EntityModel to the JsonPatchDocument and the code works perfectly. However, in order for the request to work, you must pass EntityModel as a generic to the JsonPatchDocument<> method. However, the request must be validated before the data is persisted. This setup requires the user to add validation rules to the entity-model which will mean the same rules for every request!

[HttpPatch("{id:int}")]
public virtual async Task<IActionResult> Patch(int id, JsonPatchDocument<EntityModel> patchDoc)
{
    if(patchDoc == null || patchDoc.Operations == null || patchDoc.Operations.Count == 0)
    {
        return BadRequest(ModelState);
    }

    EntityModel model = await GetAsync(id);

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

    patchDoc.ApplyTo(model, ModelState); // this works with no problem since model is the same type as the generic given to JsonPatchDocument<>
    if (!ModelState.IsValid)
    {
        return new BadRequestObjectResult(ModelState);
    }

    // Perform the update on the model
    DisplayModel result = Mapper.Map<DisplayModel>(model);

    return Ok(result);
}

Since I like to keep the validation rules in a request-model, not in entity-model, it would be important to have an .ApplyTo() method that would accept an object (as source) not as the generic type found in the JsonPatchDocument<> and ModelState to track errors.

So the above example will look like this instead

[HttpPatch("{id:int}")]
public virtual async Task<IActionResult> Patch(int id, JsonPatchDocument<RequestModel> patchDoc)
{
    if(patchDoc == null || patchDoc.Operations == null || patchDoc.Operations.Count == 0)
    {
        return BadRequest(ModelState);
    }

    EntityModel model = await GetAsync(id);

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

    patchDoc.ApplyTo(model, ModelState); // This currently does not work, note here I am passing entity-model aka EntityModel and the generic in pathDoc is RequestModel !! important different types

    if (!ModelState.IsValid)
    {
        return new BadRequestObjectResult(ModelState);
    }

    // Perform the update on the model
    DisplayModel result = Mapper.Map<DisplayModel>(model);

    return Ok(result);
}

Will it be possible to add .ApplyTo<TType>(TType requestModel, ModelState) or is there a better approach for my request?

Here is an example if EntityModel

public class User
{
    public int Id { get; set; }

    public string Username { get; set; }

    public string FirstName { get; set; }

    public string MiddleName { get; set; }

    public string LastName { get; set; }

    public bool IsActive { get; set; }
}

Here is few examples of different request-models, in the above example PatchUser would be an example of RequestModel that I would pass to JsonPatchDocument<PatchUser>

public class PatchUser
{
    [MaxLength(50)]
    public string FirstName { get; set; }

    [MaxLength(50)]
    public string MiddleName { get; set; }

    [MaxLength(50)]
    public string LastName { get; set; }
}

public class CreateUser
{
    [Required, MinLength(5), MaxLength(50)]
    public string Username { get; set; }

    [Required, MaxLength(50)]
    public string FirstName { get; set; }

    [MaxLength(50)]
    public string MiddleName { get; set; }

    [MaxLength(50)]
    public string LastName { get; set; }
}

public class UpdateUser
{
    [Required, MaxLength(50)]
    public string FirstName { get; set; }

    [MaxLength(50)]
    public string MiddleName { get; set; }

    [MaxLength(50)]
    public string LastName { get; set; }
}
KevinDockx commented 4 years ago

Glad you like JsonPatch :) There's really no need for the object you want to apply the patch to to be an entity model. Any type of object can be patched. But you'll have to do the mapping yourself - you cannot apply a patch document made for one type of object to another object type.

Another approach you could take is installing the marvin.jsonpatch.dynamic extension package (https://github.com/KevinDockx/JsonPatch.Dynamic). That includes support for non-typed patch docs, which sounds like what you need :)

Or, preferably: switch to .NET Core ;-) The implementation there is based on Marvin.JsonPatch & Marvin.JsonPatch.Dynamic, so it already includes all you need.

Hope this helps! :)