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端点错误
FindAsync是异步方法,未加await,导致cashingRecord是Task<CashingRecord>而非实体对象,后续映射逻辑失效- 从
patchDoc.Operations提取id的方式错误,Where(...).ToString()无法获取实际id值,且JSON Patch的path应为/id而非id - 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
相关产品推荐
相关产品推荐

