The Startup
Published in

The Startup

C# .NET Core and TypeScript: Using Generics and LINQ to Secure and Filter Operations on Your JSONPatchDocuments

Full stack: React with TypeScript frontend, .NET backend!

Photo by AltumCode on Unsplash

Show Me the Code!

Motivation

using System;
using System.ComponentModel.DataAnnotations;
namespace JsonPatchFilterExample.Models
{
public class WidgetModel
{
[Required]
public Guid Id { get; set; }
[Required]
[StringLength(128, MinimumLength = 2)]
public string Title { get; set; }
[Required]
[StringLength(1000, MinimumLength = 2)]
public string Description { get; set; }
[Required]
public DateTime Updated { get; set; }
[Required]
public DateTime Created { get; set; }
}
}

Using the Correct HTTP Method for Editing

Enter JSON Patch and JSONPatchDocument<T>

services.AddControllersWithViews().AddNewtonsoftJson();

Finally: Our Problem

Enter: Generics

  1. any model of type T

The PatchFiltererService

using System;
using System.Linq;
using Microsoft.AspNetCore.JsonPatch;
namespace JsonPatchFilterExample.Services
{
public static class PatchFiltererService
{
public static JsonPatchDocument<T> ApplyAttributeFilterToPatch<T, TU>(JsonPatchDocument<T> patch)
where T : class
where TU : Attribute
{
// Get path for all attributes of type TU that are in type T
var allowedPaths = typeof(T)
.GetProperties()
.Where(x => x.GetCustomAttributes(false).OfType<TU>().Any())
.Select(x => x.Name);
// Now build a new JSONPatchDocument based on properties in T that were found above
var filteredPatch = new JsonPatchDocument<T>();
patch.Operations.ForEach(x =>
{
if (allowedPaths.Contains(x.path))
{
filteredPatch.Operations.Add(x);
}
});
return filteredPatch;
}
}
}

Using the PatchFiltererService

patch = PatchFiltererService.ApplyAttributeFilterToPatch<WidgetModel, StringLength>(patch);
[HttpPatch("{id}")]
public ActionResult Patch(Guid id, [FromBody] JsonPatchDocument<WidgetModel> patch)
{
try
{
// For now, load the widget from the json file - ideally this would be retrieved via a repository from a database
var physicalProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory());
var jsonFilePath = Path.Combine(physicalProvider.Root, "App_Data", "ExampleWidget.json");
var item = new WidgetModel();
using (var reader = new StreamReader(jsonFilePath))
{
var content = reader.ReadToEnd();
item = JsonConvert.DeserializeObject<WidgetModel>(content);
}
if (item.Id != id || patch == null)
{
return NotFound();
}
// Create a new patch to match only the type and attributes passed
patch = PatchFiltererService.ApplyAttributeFilterToPatch<WidgetModel, StringLengthAttribute>(patch);
// Apply the patch!
patch.ApplyTo(item);
// Update updated time - normally would be handled in a repository
item.Updated = DateTime.Now;
// Update the item - ideally this would also be done with a repository via an 'Update' method
// write JSON directly to a file
var json = JsonConvert.SerializeObject(item);
//write string to file
System.IO.File.WriteAllText(jsonFilePath, json);
return Ok();
}
catch
{
return UnprocessableEntity();
}
}

Bonus #1: The Editable Attribute

namespace JsonPatchFilterExample.Models
{
using System;
using System.ComponentModel.DataAnnotations;
public class WidgetModel
{
[Required]
[Editable(false)]
public Guid Id { get; set; }
[Required]
[Editable(true)]
[StringLength(128, MinimumLength = 2)]
public string Title { get; set; }
[Required]
[Editable(true)]
[StringLength(1000, MinimumLength = 2)]
public string Description { get; set; }
[Required]
[Editable(false)]
public DateTime Updated { get; set; }
[Required]
[Editable(false)]
public DateTime Created { get; set; }
}
}
patch = PatchFilterer.ApplyAttributeFilterToPatch<WidgetModel, Editable(true)>(patch);

Bonus #2: Frontend Stuff

/**
* @description RFC 6902 compliant enum for allowed JSON Patch operations. See http://jsonpatch.com/ for details.
*/
enum JSONPatchOperationType {
Add = "add",
Remove = "remove",
Replace = "replace",
Copy = "copy",
Move = "move",
Test = "test"
}
export default JSONPatchOperationType;
import JSONPatchOperationType from "./JSONPatchOperationType";/**
* @description RFC 6902 compliant interface for a JSON Patch Operation. See http://jsonpatch.com/ for details.
*/
export default interface JSONPatchOperation {
op: JSONPatchOperationType;
path: string;
value: string;
}
let requestObject: JSONPatchOperation[] = [{
op: JSONPatchOperationType.Replace,
path: propertyName,
value: debouncedValue
}];
await apiService.patch(
requestObject,
() => {
setEditState(EditStatus.Saved);
setTimeout(() => setEditState(EditStatus.Idle), 1500)
},
(error) => {
setEditState(EditStatus.Error);
}
);

Thanks!

--

--

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +768K followers.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store