You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 08:40:59