After more research I came across this project which allowed you to pass in a list of immutable members
Tingle.AspNetCore.JsonPatch.NewtonsoftJson
I wanted to take that further and decorate my entities with attributes that could also handle role based permissions and have the ability to disable patch operations completely, so this is what I came up with.
ApplyPatchWrapper
public static class JsonPatchDocumentExtensions
{
public static void CheckAttributesThenApply<T>(this JsonPatchDocument<T> patchDoc,
T objectToApplyTo,
Action<JsonPatchError> logErrorAction,
List<string>? currentUserRoles)
where T : class
{
if (patchDoc == null) throw new ArgumentNullException(nameof(patchDoc));
if (objectToApplyTo == null) throw new ArgumentNullException(nameof(objectToApplyTo));
foreach (var op in patchDoc.Operations)
{
if (!string.IsNullOrWhiteSpace(op.path))
{
var pathToPatch = op.path.Trim('/').ToLowerInvariant();
var objectToPatch = objectToApplyTo.GetType().Name;
var attributesFilter = BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance;
var propertyToPatch = typeof(T).GetProperties(attributesFilter).FirstOrDefault(p => p.Name.Equals(pathToPatch, StringComparison.InvariantCultureIgnoreCase));
var patchRestrictedToRolesAttribute = propertyToPatch?
.GetCustomAttributes(typeof(PatchRestrictedToRolesAttribute),false)
.Cast<PatchRestrictedToRolesAttribute>()
.SingleOrDefault();
if (patchRestrictedToRolesAttribute != null)
{
var userCanUpdateProperty = patchRestrictedToRolesAttribute.GetUserRoles().Any(r =>
currentUserRoles != null && currentUserRoles.Any(c => c.Equals(r, StringComparison.InvariantCultureIgnoreCase)));
if(!userCanUpdateProperty) logErrorAction(new JsonPatchError(objectToApplyTo, op,
$"Current user role is not permitted to patch {objectToPatch}.{propertyToPatch!.Name}"));
}
var patchDisabledForProperty = propertyToPatch?
.GetCustomAttributes(typeof(PatchDisabledAttribute),false)
.SingleOrDefault();
if (patchDisabledForProperty != null)
{
logErrorAction(new JsonPatchError(objectToApplyTo, op,
$"Patch operations on {objectToPatch}.{propertyToPatch!.Name} have been disabled"));
}
}
}
patchDoc.ApplyTo(objectToApplyTo: objectToApplyTo, logErrorAction: logErrorAction);
}
}
Entity with custom attribute
public class AquisitionChannel
{
[PatchDisabled]
public Guid Id { get; set; } = Guid.Empty;
[PatchRestrictedToRoles(new [] { UserRoleConstants.SalesManager, UserRoleConstants.Administrator })]
public string Description { get; set; } = string.Empty;
}
Usage
var currentUserRoles = _contextAccessor.HttpContext.User.Claims.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value).ToList();
patchDocument.CheckAttributesThenApply(acquisitionChannelToUpdate,
error => throw new JsonPatchException(error),currentUserRoles);