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

Angular Mock API的PATCH方法迁移至ASP.NET Core API遇400错误排查

问题:Angular迁移PATCH请求至ASP.NET Core时出现400错误

确认Angular Mock API的PATCH端点可正常运行后,迁移至ASP.NET Core真实API时出现400错误,使用Postman测试同样提示:"The JSON patch document was malformed and could not be parsed."


Angular客户端服务请求代码

updateCashingRecord(
  id: string,
  cashingRecord: CashingRecord
): Observable<CashingRecord> {
  return this.cashingRecords$.pipe(
    take(1),
    switchMap((cashingRecords) =>
      this._httpClient
        .patch<CashingRecord>(this.apiUrl + 'cashingrecords', {
          id,
          cashingRecord,
        })
        .pipe(
          map((updatedCashingRecord) => {
            // Find the index of the updated cashingRecord
            const index = cashingRecords.findIndex((item) => item.id === id);

            // Update the cashingRecord
            cashingRecords[index] = updatedCashingRecord;

            // Update the cashingRecords
            this._cashingRecords.next(cashingRecords);

            // Return the updated cashingRecord
            return updatedCashingRecord;
          }),
          switchMap((updatedCashingRecord) =>
            this.cashingRecord$.pipe(
              take(1),
              filter((item) => item && item.id === id),
              tap(() => {
                // Update the cashingRecord if it's selected
                this._cashingRecord.next(updatedCashingRecord);

                // Return the updated cashingRecord
                return updatedCashingRecord;
              })
            )
          )
        )
    )
  );
}

原Angular Mock API端点代码

this._mockApiService
    .onPatch('api/apps/cashing-records/cashing-record')
    .reply(({request}) => {

        // Get the id and item
        const id = request.body.id;
        const cashingRecord = cloneDeep(request.body.cashingRecord);

        // Prepare the updated item
        let updatedCashingRecord = null;

        // Find the item and update it
        // eslint-disable-next-line @typescript-eslint/no-shadow
        this._cashingRecords.forEach((item, index, cashingRecords) => {

            if ( item.id === id )
            {
                // Update the item
                cashingRecords[index] = assign({}, cashingRecords[index], cashingRecord);

                // Store the updated item
                updatedCashingRecord = cashingRecords[index];
            }
        });

        // Return the response
        return [200, updatedCashingRecord];
    });

新创建的ASP.NET Core API端点代码

[HttpPatch]
public async Task<IActionResult> PartiallyUpdateCashingRecord([FromBody] JsonPatchDocument<CashingRecordDto> patchDoc)
{
    if(patchDoc == null)
    {
        _logger.LogError("patchDoc object sent from client is null.");
        return BadRequest("patchDoc object is null");
    }

    string id = patchDoc.Operations.Where(o => o.path.Equals("id")).ToString();

    var cashingRecord = _context.pm_CashingRecords.FindAsync(id);
    if (cashingRecord == null)
    {
        _logger.LogInformation($"CashingRecord with id: {id} doesn't exist.");
        return NotFound();
    }
    var cashingRecordToPatch = _mapper.Map<CashingRecordDto>(cashingRecord);
    patchDoc.ApplyTo(cashingRecordToPatch);
    await _mapper.Map(cashingRecordToPatch, cashingRecord);
    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!CashingRecordExists(id))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }

    return NoContent();
}

问题根源与修复方案

核心问题:请求格式不兼容

Angular客户端发送的是普通JSON对象,但ASP.NET Core的JsonPatchDocument要求的是符合RFC 6902标准的JSON Patch数组格式,两者完全不匹配。原Mock API是自定义解析请求体的id和cashingRecord字段,而真实API期望的是类似这样的Patch结构:

[
  { "op": "replace", "path": "/amount", "value": 100 },
  { "op": "replace", "path": "/status", "value": "Completed" }
]

额外的ASP.NET端点错误

  1. FindAsync是异步方法,未加await,导致cashingRecord是Task<CashingRecord>而非实体对象,后续映射逻辑失效
  2. 从patchDoc.Operations提取id的方式错误,Where(...).ToString()无法获取实际id值,且JSON Patch的path应为/id而非id
  3. AutoMapper的Map方法是同步的,无需加await

修复方案二选一

方案一:修改ASP.NET端点适配现有请求(快速兼容)

如果不想改动前端代码,可自定义请求模型替代JsonPatchDocument:

// 定义请求模型
public class UpdateCashingRecordRequest
{
    public string Id { get; set; }
    public CashingRecordDto CashingRecord { get; set; }
}

// 修改端点逻辑
[HttpPatch]
public async Task<IActionResult> PartiallyUpdateCashingRecord([FromBody] UpdateCashingRecordRequest request)
{
    if (request == null || string.IsNullOrEmpty(request.Id))
    {
        _logger.LogError("Invalid request data.");
        return BadRequest("Invalid request");
    }

    var cashingRecord = await _context.pm_CashingRecords.FindAsync(request.Id);
    if (cashingRecord == null)
    {
        _logger.LogInformation($"CashingRecord with id: {request.Id} doesn't exist.");
        return NotFound();
    }

    // 用传入的DTO更新实体
    _mapper.Map(request.CashingRecord, cashingRecord);
    
    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!CashingRecordExists(request.Id))
        {
            return NotFound();
        }
        throw;
    }

    var updatedDto = _mapper.Map<CashingRecordDto>(cashingRecord);
    return Ok(updatedDto);
}

方案二:修改Angular发送标准JSON Patch请求(规范做法)

遵循RESTful规范,前端构造标准Patch请求:

updateCashingRecord(id: string, cashingRecord: CashingRecord): Observable<CashingRecord> {
    // 构造JSON Patch操作数组
    const patchOperations = Object.keys(cashingRecord).map(key => ({
        op: 'replace',
        path: `/${key}`,
        value: cashingRecord[key]
    }));

    // 将ID放入URL,符合REST规范
    return this._httpClient.patch<CashingRecord>(`${this.apiUrl}cashingrecords/${id}`, patchOperations).pipe(
        take(1),
        switchMap(updated => {
            // 更新本地状态
            this._cashingRecords.next(this._cashingRecords.value.map(item => item.id === id ? updated : item));
            this._cashingRecord.next(updated);
            return of(updated);
        })
    );
}

同时修改ASP.NET端点,从URL获取ID:

[HttpPatch("{id}")]
public async Task<IActionResult> PartiallyUpdateCashingRecord(string id, [FromBody] JsonPatchDocument<CashingRecordDto> patchDoc)
{
    if (patchDoc == null)
    {
        _logger.LogError("patchDoc object sent from client is null.");
        return BadRequest("patchDoc object is null");
    }

    var cashingRecord = await _context.pm_CashingRecords.FindAsync(id);
    if (cashingRecord == null)
    {
        _logger.LogInformation($"CashingRecord with id: {id} doesn't exist.");
        return NotFound();
    }

    var cashingRecordToPatch = _mapper.Map<CashingRecordDto>(cashingRecord);
    patchDoc.ApplyTo(cashingRecordToPatch);
    
    // 可选:验证模型有效性
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    _mapper.Map(cashingRecordToPatch, cashingRecord);
    
    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!CashingRecordExists(id))
        {
            return NotFound();
        }
        throw;
    }

    return Ok(_mapper.Map<CashingRecordDto>(cashingRecord));
}

内容的提问来源于stack exchange,提问作者Sami-L

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 18:46:04