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