Created
March 26, 2019 00:47
-
-
Save brendanmckenzie/a50f4eb7d5913372d01fef8e73c5dc9b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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