REST Patch in dotnet/meditr best practices
Asked Answered
S

2

7

I have used PUT/POST to update the applications I'm working with, which has been straightforward until now. Now I'm working on an API that should expose a way to update data in a database, and my initial thought was that I could replace the row (or replace it partially). Since different applications with different knowledge about the data need to update, I think it could be a good idea with a PATCH action instead of a PUT.

I can find some toy examples that implement the update in the controller, which I'm not interested in.

I use the same pattern(CQRS w. Mediatr) as in the Clean architecture project by jasontaylordev , so i will use that in my example.

I have two questions.

  1. I don't like that the JsonPatchDocument object should reach my Handlers, but I don't know how I can avoid that. Access to the database from the controllers is not an option. So are there other options? (n.b. Automapper or similar dynamic mapping libraries is neither an option in the projects I'm working on)

  2. How can I build up the JsonPatch objects from the client applications (all dotnet core projects). I think it could be neat if I could have a DTO in application one that has the two properties Title and Note and if Title="test" and Note=null, then it should replace the Title and delete the Note. Can I map to the (weird) patch object in an easy way?

     [HttpPatch("{id}")]
     public async Task<ActionResult> Update(int id, UpdateTodoItemCommand command)
     {
      if (id != command.Id)
       {
      return BadRequest();
      }
    
     await Mediator.Send(command);
    
     return NoContent();
     }
    
     public class UpdateTodoItemCommand : IRequest
     {
       public int Id { get; set; }
    
       public JsonPatchDocument<TodoItemDto> Todo { get; set; }
     }
    
     public class TodoItemDto
     {
       public string Title { get; set; }
    
       public string Note { get; set; }
     }
    
    
     public class UpdateTodoItemCommandHandler : IRequestHandler<UpdateTodoItemCommand>
     {
       private readonly IApplicationDbContext _context;
    
       public UpdateTodoItemCommandHandler(IApplicationDbContext context)
       {
        _context = context;
       }
    
     public async Task<Unit> Handle(UpdateTodoItemCommand request, CancellationToken cancellationToken)
     {
         var entity = await _context.TodoItems.FindAsync(request.Id);
    
         if (entity == null)
         {
             throw new NotFoundException(nameof(TodoItem), request.Id);
         }
    
         var todoItemDto = new TodoItemDto();
         todoItemDto.Title = entity.Title;
         todoItemDto.Note = entity.Note: 
    
         todoItemDto.Todo.ApplyTo(request.Todo);
    
         entity.Title = todoItemDto.Title;
         entity.Note = todoItemDto.Note;
    
         await _context.SaveChangesAsync(cancellationToken);
    
         return Unit.Value;
     }
    }
    
Spectatress answered 18/6, 2021 at 21:31 Comment(3)
By JsonPatch object what do you mean? Are you referring to JsonPatchDocument<T> In ASP.NET Core?Capitulation
Yes, I mean JsonPatchDocumentSpectatress
Hi @XRaycat, have you found a solution to this problem?Brannan
D
3

I had the same problem and I came up with a solution that worked for me.

In the Handle method of you UpdateTodoItemCommandHandler class you would create one more JsonPatchDocument, but of type TodoItem (not Dto). And then transfer the operations and ContractResolver to your new JsonPatchDocument object, which you can patch and save to the database. Something like this:

var entity = await _context.TodoItems.FindAsync(request.Id);

if (entity == null)
{
    throw new NotFoundException(nameof(TodoItem), request.Id);
}    

var todoItemPatch = new JsonPatchDocument<TodoItem>();

foreach (var opr in request.Todo.Operations)
{
    todoItemPatch.Operations.Add(new Operation<TodoItem> 
    { 
        op = opr.op, 
        path = opr.path, 
        value = opr.value 
    });
}

todoItemPatch.ContractResolver = 
request.Todo.ContractResolver;

todoItemPatch.ApplyTo(entity);

await _context.SaveChangesAsync(cancellationToken);
Dona answered 14/8, 2023 at 12:57 Comment(0)
C
1

`JsonPatchDocument is a very simple object that represents the JSON Patch RFC. It’s essentially a set of instructions. If you don’t want to use it because you don’t want the built-in Microsoft implementation to surface in your handlers, then you’d have to

  • create your own class (e.g UpdateCommand that itself is very similar to a patch document; taking “replace”, “delete”, and so on instructions.
  • map the patch document to an instance of this class and route that via Mediatr
  • recreate the “patching machinery” already provided out of the asp.net toolbox

That seems like a lot to do to avoid just taking that reference. If you want you can perhaps create a command that holds the document, but that doesn’t do much.

An implementation I created just takes the JsonPatchDocument and passes it to a strategy class that applies the patch (this strategy class played the same role as Mediatr so is effectively the same thing. I then called the Apply() method against the resource.

Capitulation answered 18/6, 2021 at 22:20 Comment(4)
I think I will use the JsonPatchDocument in my handler. But is it standard practice to first transfer the fields from the domain entity to a DTO, then apply the operations from the JsonPatchDocument to the DTO, and then finally map the DTO a domain object, before I save it to the db?Spectatress
Furthermore, I'm also using Fluentvlidation that is a part of the mediatr pipeline. So that's another thing I don't know how to solve in an elegant way.Spectatress
I don’t know if it’s standard practice or not. Is your system a system of record or reference? That makes a difference.Capitulation
The data that is accessible from the API is the "truth", although parts of the records found in the system are replicated (in another context) in other systems to avoid too chatty services that not work independently. The API will be used for reads when it's important to have the most up-to-date information and to add and update records.Spectatress

© 2022 - 2025 — McMap. All rights reserved.