JSON Patch (2) In ASP.NET Core web API For Database

This article is a continuation of Json Patch (1) In ASP.NET Core web API, beginning with part C.

  • A: Introduction
    • Full Update vs. Patial Update
    • What is JSON Patch?
    • How Json Patch works?
  • B: Implementation in ASP.NET Core Web API, in Memory
    • Configure the App
    • Add PATCH related files (Entities and Controller)
    • Discussions:
      • Add Operation
      • Remove Operation
      • Replace Operation
      • Move Operation
      • Copy Operation
      • Test Operation
  • C: Implementation in ASP.NET Core Web API, for Database
    • PreSetup a new App with CRUD
    • Config the App for JsonPatch
      • Enable Json PATCH support
      • Add HttpPatch action
      • Add Initial Data
    • Discussions:
      • Add Operation
      • Remove Operation
      • Replace Operation
      • Move Operation
      • Copy Operation
      • Test Operation

C: Implementation in ASP.NET Core Web API, for Database

In the previous article,  we discussed the concept of Json PATCH, and running PATCH for View Models/DTOs from memory,  now we need to apply the patches back onto a database. Years ago, people used  Automapper to do the job, and now with .NET Core 5.0 (at least), we can do it just by the Microsoft code. We will demo a short sample here.

1, PreSetup a new App with CRUD

Follow the instruction from my previous article Entity Framework With .Net Core MVC (4), Code-First, step by step, we create an ASP.NET Core Web API app with CRUD,  by given:

Project Name as JsonPatch

Entity class name as VideoGame in /models/VideoGame.cs:

using System;  
using System.ComponentModel.DataAnnotations;  
  
namespace JsonPatch.Models  
{  
    // VideoGame.cs  
    public partial class VideoGame  
    {  
        [Key]  
        public virtual int Id { get; set; }  
  
        public virtual string Title { get; set; }  
  
        public virtual string Publisher { get; set; }  
  
        public virtual DateTime? ReleaseDate { get; set; }  
  
        public VideoGame(int id, string title, string publisher, DateTime? releaseDate)  
        {  
            Id = id;  
            Title = title;  
            Publisher = publisher;  
            ReleaseDate = releaseDate;  
        }  
    }  
}

DbContext class name as JsonPatchContext in /modes/JsonPatchContext.cs

using Microsoft.EntityFrameworkCore;  
  
namespace JsonPatch.Models  
{  
    public partial class JsonPatchContext : DbContext  
    {  
        public JsonPatchContext()  
        {  
        }  
  
        public JsonPatchContext(DbContextOptions<JsonPatchContext> options)  
            : base(options)  
        {  
        }  
  
        public virtual DbSet<VideoGame> VideoGames { get; set; }  
  
    }  
}

Connection string in the appsettings.json file

"ConnectionStrings": {  
  "JsonPatchContext": "Data Source=localhost;Initial Catalog=JsonPatch;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"  
}, 

Create a VideoGamesController with CRUD:

using System.Collections.Generic;  
using System.Linq;  
using System.Threading.Tasks;  
using Microsoft.AspNetCore.Mvc;  
using Microsoft.EntityFrameworkCore;  
using JsonPatch.Models;  
using AutoMapper;  
using Microsoft.AspNetCore.JsonPatch;  
  
namespace JsonPatch.Controllers  
{  
    [Route("api/[controller]")]  
    [ApiController]  
    public class VideoGamesController : ControllerBase  
    {  
        private readonly JsonPatchContext _context;  
        private readonly IMapper _mapper;  
  
        public VideoGamesController(JsonPatchContext context)  
        {  
            _context = context;  
        }  
  
        // GET: api/VideoGames  
        [HttpGet]  
        public async Task<ActionResult<IEnumerable<VideoGame>>> GetVideoGames()  
        {  
            return await _context.VideoGames.ToListAsync();  
        }  
  
        // GET: api/VideoGames/5  
        [HttpGet("{id}")]  
        public async Task<ActionResult<VideoGame>> GetVideoGame(int id)  
        {  
            var videoGame = await _context.VideoGames.FindAsync(id);  
  
            if (videoGame == null)  
            {  
                return NotFound();  
            }  
  
            return videoGame;  
        }  
  
        // PUT: api/VideoGames/5  
        [HttpPut("{id}")]  
        public async Task<IActionResult> PutVideoGame(int id, VideoGame videoGame)  
        {  
            if (id != videoGame.Id)  
            {  
                return BadRequest();  
            }  
  
            _context.Entry(videoGame).State = EntityState.Modified;  
  
            try  
            {  
                await _context.SaveChangesAsync();  
            }  
            catch (DbUpdateConcurrencyException)  
            {  
                if (!VideoGameExists(id))  
                {  
                    return NotFound();  
                }  
                else  
                {  
                    throw;  
                }  
            }  
  
            return NoContent();  
        }  
  
        // POST: api/VideoGames  
        [HttpPost]  
        public async Task<ActionResult<VideoGame>> PostVideoGame(VideoGame videoGame)  
        {  
            _context.VideoGames.Add(videoGame);  
            await _context.SaveChangesAsync();  
  
            return CreatedAtAction("GetVideoGame", new { id = videoGame.Id }, videoGame);  
        }  
  
        // DELETE: api/VideoGames/5  
        [HttpDelete("{id}")]  
        public async Task<IActionResult> DeleteVideoGame(int id)  
        {  
            var videoGame = await _context.VideoGames.FindAsync(id);  
            if (videoGame == null)  
            {  
                return NotFound();  
            }  
  
            _context.VideoGames.Remove(videoGame);  
            await _context.SaveChangesAsync();  
  
            return NoContent();  
        }  
  
        private bool VideoGameExists(int id)  
        {  
            return _context.VideoGames.Any(e => e.Id == id);  
        }  
    }  
}

Run the app, the result will be

2. Config the App for JsonPatch

Step 1: Enable Json PATCH support:

Install the Microsoft.AspNetCore.Mvc.NewtonsoftJson from NuGet package to anable Json PATCH support

Step 2: Add HttpPatch action

Add HttpPatch action in VideoGamesController class:

[HttpPatch("{id:int}")]  
public IActionResult Patch(int id, [FromBody] JsonPatchDocument<VideoGame> patchEntity)  
{  
    var entity = _context.VideoGames.FirstOrDefault(videoGame => videoGame.Id == id);  
  
    if (entity == null)  
    {  
        return NotFound();  
    }  
  
    patchEntity.ApplyTo(entity, ModelState); // Must have Microsoft.AspNetCore.Mvc.NewtonsoftJson installed  
  
    if (!ModelState.IsValid)  
    {  
        return BadRequest(ModelState);  
    }  

    _context.Update(entity); //Update in the database.   
  
    try  
    {  
        _context.SaveChangesAsync();  
    }  
    catch (DbUpdateConcurrencyException)  
    {  
        if (!VideoGameExists(id))  
        {  
            return NotFound();  
        }  
        else  
        {  
            throw;  
        }  
    }  
    return Ok(entity);  
}

and Remove all other Verbs except GET.

Step 3: Add Initial Data:

Add initial data in the constructor of VideoGamesController class:

public VideoGamesController(JsonPatchContext context)  
{  
    _context = context;  
  
    if (_context.VideoGames.Count() == 0)  
    {
        _context.VideoGames.Add(new VideoGame(0, "Call of Duty: Warzone", "Activision", new System.DateTime(2020, 3, 10)));  
        _context.VideoGames.Add(new VideoGame(0, "Friday the 13th: The Game", "Gun Media", new System.DateTime(2017, 5, 26)));  
        _context.VideoGames.Add(new VideoGame(0, "DOOM Eternal", "Bethesda", new System.DateTime(2020, 3, 20)));  
        _context.SaveChanges();  
    }  
}

3, Discussions

Run the App:

Run the App, we have only two action methods: GET and PATCH because we have removed all other actions. Run the GET, we will get the Original input data:

Run PATCH,

We will run the PATCH operations: “add”, “remove”, “replace”, “move”, “copy” and “test”, one by one.  Because all results are saved into database, the following step will be against the updated one instead of the original one. We can check this by run GET to see the result for each step.

The add operation

with id = 2:

[  
    {  
        "value": "Tuesday : the Game",  
        "path": "/title",  
        "op": "add"  
    }  
]

Shown in app like this,

if we run GET, we will see the data saved into database:

The remove operation

with id = 2: 

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

Then we got the result:

The replace operation

with id = 2:

[  
    {  
        "value": "Top Gun",  
        "path": "/publisher",  
        "op": "replace"  
    }  
] 

Then we got the result:

The move operation

with id = 2:

[  
    {  
        "from": "/title",  
        "path": "/publisher",  
        "op": "move"  
    }  
]

Then we got the result:

The copy operation

with id = 2: 

[  
    {  
        "from": "/releaseDate",  
        "path": "/publisher",  
        "op": "copy"  
    }  
] 

Then we got the result:

The test operation

with id = 2:

[  
    {  
        "value": "Tuesday : the Game",  
        "path": "/title",  
        "op": "test"  
    }  
]

Then we got the result:

Summary

Thia article described the PATCH Implementation in ASP.NET Core Web API, for Database.

Reference


Similar Articles