限制每个成员的 JsonPatchDocument 补丁操作

Restrict JsonPatchDocument patch operations on a per member basis

对于补丁操作需要一些成员immutable/read-only的情况,是否有任何建议?

通常我们会将上下文实体传递给 JsonPatchDocument,我们将整个对象暴露给修改禁止您可能拥有的任何实体验证规则。

我可以想象这样一种情况,一个实体的一个成员出于安全原因不应被编辑,或者可能是一个计算值。

我在网上看不到很多这方面的信息,我能想到的一种方法是将我的实体映射到一个 UpdateRequest/DTO class,您可以传递它来初始化 JsonPatchDocument 对象。然后 运行 针对它的补丁操作,最终将 updateRequest 映射回数据库实体并持久化。

我认为这可行,但感觉有点乱,如果有一个装饰器可以限制每个成员的补丁操作,例如 [NoPatch] 或相反。

这是 Dotnet API 补丁操作的一个很好的标准实现,符合我目前对该主题的理解 How to use JSONPatch in .net core

经过更多研究后,我发现了这个项目,它允许你传入一个不可变成员列表 Tingle.AspNetCore.JsonPatch.NewtonsoftJson

我想更进一步,用也可以处理基于角色的权限并能够完全禁用补丁操作的属性装饰我的实体,所以这就是我想出的。

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);
    }
    
}

具有自定义属性的实体

public class AquisitionChannel
{
    [PatchDisabled]
    public Guid Id { get; set; } = Guid.Empty;
    
    [PatchRestrictedToRoles(new [] { UserRoleConstants.SalesManager, UserRoleConstants.Administrator })]
    public string Description { get; set; } = string.Empty;
}

用法

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);