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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 17:34:54