.NET 7中JsonPatchDocument标识符转ID的动态数据处理方案
在.NET 7中动态处理JsonPatchDocument的标识符替换问题
问题背景
我们在.NET 7中使用JsonPatchDocument实现Patch功能,为任务添加关联产品。前端不传递数据库ID,而是使用标识符:
- 理想的Patch操作数据(直接用数据库ID):
{ "op": "add", "path": "/products", "value": [{ "taskId": 1002, "productId": 2, "companyId": 2, "count": 30 }] }
执行代码:
request.Request.JsonDocument.ApplyTo(task, request.Controller.ModelState);
- 实际前端传递的数据:
{ "op": "add", "path": "/products", "value": [{ "taskId": 0, "productId": 0, "companyId": 0, "count": 30 }] }
同时会单独传入taskIdentifier: "802D5980-4D33-4091-89E3-DD6FCB57C1B1",需要将task、product、company的标识符替换为真实数据库ID后,再执行ApplyTo操作。
注:仅任务关联的列表(如/products)存在该问题,修改任务标题等字段时,可直接通过taskIdentifier获取任务对象后调用ApplyTo,无需修改数据。
动态处理方案
1. 解析JsonPatchDocument并修改操作内容
先将JsonPatchDocument反序列化为可修改的操作列表,遍历匹配目标路径的操作,替换value中的标识符为数据库ID:
// 1. 反序列化Patch文档为可操作的列表 var patchOperations = JsonSerializer.Deserialize<List<JsonPatchOperation>>(request.Request.JsonDocument.RootElement.GetRawText()); // 2. 筛选出/products路径的add操作 foreach (var op in patchOperations.Where(o => o.Op == "add" && o.Path.Equals("/products", StringComparison.OrdinalIgnoreCase))) { // 3. 解析value中的产品列表 var products = JsonSerializer.Deserialize<List<ProductDto>>(op.Value.ToString()); if (products == null) continue; // 4. 根据taskIdentifier获取任务真实ID var task = await _taskRepository.GetByIdentifierAsync(taskIdentifier); if (task == null) { request.Controller.ModelState.AddModelError("taskIdentifier", "任务不存在"); return; } // 5. 替换产品中的ID字段 foreach (var product in products) { // 替换taskId为真实数据库ID product.TaskId = task.Id; // 替换productId:根据前端传递的product标识符查询真实ID var realProduct = await _productRepository.GetByIdentifierAsync(product.ProductIdentifier); product.ProductId = realProduct?.Id ?? throw new InvalidOperationException("产品不存在"); // 替换companyId:同理查询真实ID var realCompany = await _companyRepository.GetByIdentifierAsync(product.CompanyIdentifier); product.CompanyId = realCompany?.Id ?? throw new InvalidOperationException("公司不存在"); } // 6. 将修改后的产品列表重新序列化为JsonElement,替换原操作的Value op.Value = JsonSerializer.SerializeToElement(products); } // 7. 将修改后的操作列表重新转为JsonPatchDocument并应用到任务对象 var modifiedPatchDocument = JsonSerializer.SerializeToElement(patchOperations); modifiedPatchDocument.ApplyTo(task, request.Controller.ModelState);
2. 封装自定义过滤器统一处理
如果多个接口有类似需求,可封装成ActionFilter,统一处理标识符替换逻辑:
public class PatchIdentifierConversionFilter : IActionFilter { private readonly ITaskRepository _taskRepository; private readonly IProductRepository _productRepository; private readonly ICompanyRepository _companyRepository; public PatchIdentifierConversionFilter(ITaskRepository taskRepository, IProductRepository productRepository, ICompanyRepository companyRepository) { _taskRepository = taskRepository; _productRepository = productRepository; _companyRepository = companyRepository; } public async void OnActionExecuting(ActionExecutingContext context) { if (!context.HttpContext.Request.Method.Equals("PATCH", StringComparison.OrdinalIgnoreCase)) return; // 从路由或查询参数获取taskIdentifier if (!context.HttpContext.Request.RouteValues.TryGetValue("taskIdentifier", out var identifierObj) || string.IsNullOrEmpty(identifierObj?.ToString())) return; var taskIdentifier = identifierObj.ToString(); var task = await _taskRepository.GetByIdentifierAsync(taskIdentifier); if (task == null) { context.ModelState.AddModelError("taskIdentifier", "任务不存在"); context.Result = new BadRequestObjectResult(context.ModelState); return; } // 读取并修改Patch文档 using var reader = new StreamReader(context.HttpContext.Request.Body); var patchJson = await reader.ReadToEndAsync(); var patchOperations = JsonSerializer.Deserialize<List<JsonPatchOperation>>(patchJson); foreach (var op in patchOperations.Where(o => o.Op == "add" && o.Path.Equals("/products", StringComparison.OrdinalIgnoreCase))) { var products = JsonSerializer.Deserialize<List<ProductDto>>(op.Value.ToString()); if (products == null) continue; foreach (var product in products) { product.TaskId = task.Id; var realProduct = await _productRepository.GetByIdentifierAsync(product.ProductIdentifier); product.ProductId = realProduct?.Id ?? throw new InvalidOperationException("产品不存在"); var realCompany = await _companyRepository.GetByIdentifierAsync(product.CompanyIdentifier); product.CompanyId = realCompany?.Id ?? throw new InvalidOperationException("公司不存在"); } op.Value = JsonSerializer.SerializeToElement(products); } // 重新设置请求体 var modifiedPatchJson = JsonSerializer.Serialize(patchOperations); var modifiedStream = new MemoryStream(Encoding.UTF8.GetBytes(modifiedPatchJson)); context.HttpContext.Request.Body = modifiedStream; context.HttpContext.Request.ContentLength = modifiedStream.Length; } public void OnActionExecuted(ActionExecutedContext context) { } }
在Program.cs中注册过滤器:
builder.Services.AddScoped<PatchIdentifierConversionFilter>(); builder.Services.AddControllers(options => { options.Filters.Add<PatchIdentifierConversionFilter>(); });
3. 关键注意事项
- 处理ID替换时要校验数据存在性,及时将错误信息写入
ModelState - 如果前端传递的标识符是独立字段(如
productIdentifier而非productId=0),需调整ProductDto结构,序列化时忽略ID字段,只处理标识符字段 - 针对复杂嵌套路径的Patch操作,可扩展路径匹配逻辑,覆盖更多业务场景
内容的提问来源于stack exchange,提问作者Michael Winther
相关产品推荐
相关产品推荐

