ASP.NET Core MVC中OData PUT端点异常:存在ID却创建新实体
问题描述
- 项目从Entity Framework迁移至Entity Framework Core后,原正常工作的PUT端点在更换特性为
[Microsoft.AspNetCore.Mvc.HttpPut](原使用[Microsoft.Http.Mvc.HttpPut])后出现异常:即使传入的ID已存在,仍会创建新实体,返回201 CREATED而非预期的204 NO CONTENT。 - 尝试过
[System.Web.Mvc.HttpPut]、[System.Http.Mvc.HttpPut]等特性,均未解决问题。 - 测试时(Postman/Swagger),参数始终通过URL传递,而非按预期使用
application/json请求体。
相关代码
UsersController.cs
using System.Net; using SyncSchools.WebServices.Models; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Deltas; using Microsoft.AspNetCore.OData.Formatter; using Microsoft.AspNetCore.OData.Routing.Controllers; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; using System.Diagnostics; using Microsoft.AspNetCore.OData.Routing.Template; namespace SyncSchools.WebServices.Controllers.Odata { public class UsersController : ODataController { SyncSchoolsContext _dbContext = new SyncSchoolsContext(); // GET odata/Users [EnableQuery] [EnableCors("MyPolicy")] public IQueryable<User> GetUsers() { return _dbContext.Users; } // GET odata/Users(5) [EnableQuery] [EnableCors("MyPolicy")] public Microsoft.AspNetCore.OData.Results.SingleResult<User> GetUser([FromODataUri] Guid key) { return Microsoft.AspNetCore.OData.Results.SingleResult.Create(_dbContext.Users.Where(User => User.Id == key)); } // PUT odata/User(5) [EnableCors("MyPolicy")] [Microsoft.AspNetCore.Mvc.HttpPut] public async Task<IActionResult> Put([FromODataUri] Guid key, User User) { if (!ModelState.IsValid) return (IActionResult)BadRequest(ModelState); return await SaveChanges(User); } // POST odata/User [HttpPost] [EnableCors("MyPolicy")] public async Task<IActionResult> Post(User User) { if (!ModelState.IsValid) return (IActionResult)BadRequest(ModelState); return await SaveChanges(User); } private async Task<IActionResult> SaveChanges(User data) { var existing = _dbContext.Users.Find(data.Id); var dbentry = _dbContext.Entry(data); if (existing == null) _dbContext.Users.Add(data); else _dbContext.Entry(existing).CurrentValues.SetValues(data); try { await _dbContext.SaveChangesAsync(); } catch { throw; } if (existing == null) return (IActionResult)Created(data); else return (IActionResult)Updated(data); } // PATCH odata/User(5) [Microsoft.AspNetCore.Mvc.AcceptVerbs("PATCH", "MERGE")] public async Task<IActionResult> Patch([FromODataUri] Guid key, Delta<User> patch) { if (!ModelState.IsValid) return (IActionResult)BadRequest(ModelState); User User = await _dbContext.Users.FindAsync(key); if (User == null) return (IActionResult)NotFound(); patch.Patch(User); try { await _dbContext.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!UserExists(key)) return (IActionResult)NotFound(); else throw; } return (IActionResult)Updated(User); } // DELETE odata/User(5) public async Task<IActionResult> Delete([FromODataUri] Guid key) { User User = await _dbContext.Users.FindAsync(key); if (User == null) return (IActionResult)NotFound(); _dbContext.Users.Remove(User); await _dbContext.SaveChangesAsync(); return (IActionResult)StatusCode((int)HttpStatusCode.NoContent); } private bool UserExists(Guid key) { return _dbContext.Users.Any(e => e.Id == key); } } }
Program.cs
var builder = WebApplication.CreateBuilder(args); builder.Services.AddCors(options => options.AddPolicy("MyPolicy", builder => { builder.AllowAnyMethod(); builder.AllowAnyHeader(); builder.AllowAnyOrigin(); })); builder.Services.AddControllers().AddJsonOptions(options => { options.JsonSerializerOptions.IncludeFields = true; options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; }); var app = builder.Build(); // Pour permettre les tests en local app.UseCors();
解决方案
1. 修正PUT端点的语义逻辑
PUT的核心语义是更新指定ID的实体,不存在则返回404,而非自动创建。当前代码复用了POST的SaveChanges方法,导致逻辑混淆,需拆分处理:
// 修改Put方法 [EnableCors("MyPolicy")] [HttpPut] public async Task<IActionResult> Put([FromODataUri] Guid key, User user) { if (!ModelState.IsValid) return BadRequest(ModelState); // 强制校验URL中的key与请求体ID一致 if (key != user.Id) return BadRequest("URL参数ID与请求体ID不匹配"); var existing = await _dbContext.Users.FindAsync(key); if (existing == null) return NotFound(); // 覆盖现有实体属性 _dbContext.Entry(existing).CurrentValues.SetValues(user); try { await _dbContext.SaveChangesAsync(); } catch { throw; } return Updated(existing); } // 简化Post方法,移除复用的SaveChanges [HttpPost] [EnableCors("MyPolicy")] public async Task<IActionResult> Post(User user) { if (!ModelState.IsValid) return BadRequest(ModelState); _dbContext.Users.Add(user); await _dbContext.SaveChangesAsync(); return Created(user); }
2. 清理无效的特性引用
在ASP.NET Core中,正确的PUT特性就是[Microsoft.AspNetCore.Mvc.HttpPut],可直接简写为[HttpPut](已引用using Microsoft.AspNetCore.Mvc;)。不要安装Microsoft.AspNet.Mvc或使用System.Web.Mvc下的特性,这些属于ASP.NET Framework,与Core不兼容。
3. 补充OData路由配置
当前Program.cs未注册OData路由,导致[FromODataUri]等特性无法正常工作,参数绑定异常。需添加OData配置:
using Microsoft.AspNetCore.OData; using Microsoft.OData.ModelBuilder; var builder = WebApplication.CreateBuilder(args); // 注册DbContext(替换为你的连接字符串) builder.Services.AddDbContext<SyncSchoolsContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddCors(options => options.AddPolicy("MyPolicy", builder => { builder.AllowAnyMethod(); builder.AllowAnyHeader(); builder.AllowAnyOrigin(); })); // 添加OData配置 builder.Services.AddControllers().AddOData(options => { options.Select().Filter().OrderBy().Expand().Count().SetMaxTop(100); options.AddRouteComponents("odata", GetEdmModel()); }).AddJsonOptions(options => { options.JsonSerializerOptions.IncludeFields = true; options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; }); var app = builder.Build(); app.UseCors("MyPolicy"); // 明确指定使用的Cors策略 app.UseRouting(); app.MapControllers(); app.Run(); // 定义OData实体模型 private static IEdmModel GetEdmModel() { ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder(); modelBuilder.EntitySet<User>("Users"); modelBuilder.EntityType<User>().HasKey(u => u.Id); // 明确指定主键 return modelBuilder.GetEdmModel(); }
4. 修复DbContext生命周期问题
当前直接在Controller中new SyncSchoolsContext会导致上下文管理混乱,需通过依赖注入获取:
// 修改UsersController的构造函数 private readonly SyncSchoolsContext _dbContext; public UsersController(SyncSchoolsContext dbContext) { _dbContext = dbContext; }
5. 测试时确保请求格式正确
- 请求头设置
Content-Type: application/json - 将User数据放在请求体(Body)中,而非URL参数
- 使用正确的OData PUT路由:
PUT odata/Users(GUID_VALUE)
内容的提问来源于stack exchange,提问作者Pierrick Martellière
相关产品推荐
相关产品推荐

