Skip to content

Instantly share code, notes, and snippets.

@brendanmckenzie
Created March 26, 2019 00:47
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brendanmckenzie/a50f4eb7d5913372d01fef8e73c5dc9b to your computer and use it in GitHub Desktop.
Save brendanmckenzie/a50f4eb7d5913372d01fef8e73c5dc9b to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json.Linq;
using MyApp.Models;
namespace MyApp.Controllers
{
[Authorize]
[Route("api/patch")]
public class PatchController : Controller
{
readonly DataContext _dataContext;
readonly ICollection<SupportedType> _supportedTypes;
public PatchController(DataContext dataContext)
{
_dataContext = dataContext;
_supportedTypes = new[]
{
SupportedType.FromT<Quote>(),
new SupportedType(typeof(Customer))
{
EntityLoader = (id, dc) =>
{
var entity = dc.Set<Customer>()
.Include(x => x.Quotes)
.SingleOrDefault(x => x.Id == id);
entity.Quotes = entity.Quotes.OrderBy(ent => ent.Order).ToList();
return entity;
},
EntityInitialiser = () =>
{
string GenerateKey(int length = 6)
{
var rand = new Random();
var alphabet = "234679CDFGHJKMNPRTWXYZ".ToCharArray();
return string.Join(string.Empty, Enumerable.Range(0, length).Select(_ => alphabet.ElementAt(rand.Next(0, alphabet.Length))));
}
return new Customer
{
Key = GenerateKey()
};
}
}
};
}
[HttpPatch]
[Route("{type}/{id}")]
public IActionResult PatchObject(string type, Guid id, [FromBody] JsonPatchDocument patch)
{
var typeDef = _supportedTypes.FirstOrDefault(ent => ent.Alias.Equals(type));
if (typeDef == null)
{
throw new InvalidOperationException("Unsupport type");
}
object LoadEntity()
{
if (typeDef.EntityLoader == null)
{
return _dataContext.Find(typeDef.Type, id);
}
return typeDef.EntityLoader.Invoke(id, _dataContext);
}
var entity = LoadEntity();
if (entity == null)
{
throw new NullReferenceException("Entity not found");
}
var normalisedPatch = NormaliseOperations(patch, typeDef.Type);
normalisedPatch.ApplyTo(entity);
if (entity is BaseModel baseModel)
{
baseModel.Modified = DateTime.UtcNow;
}
_dataContext.SaveChanges();
return NoContent();
}
[HttpPost]
[Route("{type}")]
public IActionResult CreateObject(string type, [FromBody] JsonPatchDocument patch)
{
var typeDef = _supportedTypes.FirstOrDefault(ent => ent.Alias.Equals(type));
if (typeDef == null)
{
throw new InvalidOperationException("Unsupport type");
}
object InitialiseEntity()
{
if (typeDef.EntityInitialiser == null)
{
return Activator.CreateInstance(typeDef.Type);
}
return typeDef.EntityInitialiser();
}
var entity = InitialiseEntity();
var id = Guid.NewGuid();
if (entity is BaseModel baseModel)
{
baseModel.Id = id;
baseModel.Created = DateTime.UtcNow;
baseModel.Modified = DateTime.UtcNow;
}
var normalisedPatch = NormaliseOperations(patch, typeDef.Type);
normalisedPatch.ApplyTo(entity);
_dataContext.Add(entity);
_dataContext.SaveChanges();
return Json(new { Id = id });
}
JsonPatchDocument NormaliseOperations(JsonPatchDocument input, Type targetType)
{
var sourceOperations = input.Operations;
List<Operation> HandleLinkedEntities(List<Operation> source)
{
// Update the operations so that when a linked entity is updated, the reference is updated, not the actual linked object
var suffix = "/id";
var linkedEntityOperations = sourceOperations.Where(op => op.path.EndsWith(suffix))
.Select(op => new
{
BasePath = op.path.Substring(0, op.path.Length - suffix.Length),
Op = op
});
var dest = new List<Operation>(sourceOperations);
dest.RemoveAll(op => linkedEntityOperations.Any(lop => op.path.StartsWith(lop.BasePath)));
dest.AddRange(linkedEntityOperations.Select(op =>
{
op.Op.path = $"{op.BasePath}Id";
return op.Op;
}));
return dest;
}
var destOperations = HandleLinkedEntities(sourceOperations);
foreach (var op in destOperations)
{
if (op.value is JObject jobj)
{
void NormaliseLinkedEntities(JToken tok)
{
foreach (var prop in tok.ToList())
{
if (prop is JProperty jprop)
{
if (jprop.Name.Equals("id"))
{
var id = jprop.Value.Value<string>();
if (tok.Parent != null)
{
tok.Parent.Parent[$"{tok.Parent.Path}Id"] = id;
tok.Parent.Remove();
}
else
{
op.path = $"{op.path}Id";
op.value = Guid.Parse(id);
}
}
if (jprop.Value is JObject obj)
{
NormaliseLinkedEntities(jprop.Value);
}
}
}
}
NormaliseLinkedEntities(jobj);
}
}
return new JsonPatchDocument(destOperations, input.ContractResolver);
}
class SupportedType
{
public string Alias { get; set; }
public Type Type { get; set; }
public Func<Guid, DataContext, object> EntityLoader { get; set; }
public Func<object> EntityInitialiser { get; set; }
public SupportedType()
{
}
public SupportedType(Type type)
{
Alias = type.Name;
Type = type;
}
public static SupportedType FromT<T>() => new SupportedType(typeof(T));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment