I recently passed from an Entity Framework app to an Entity Framework Core one, I'm developing an API as a web service.
I have some GET endpoints that are working properly, and the put endpoint did work yesterday before to end work with adding [Microsoft.AspNetCore.Mvc.HttpPut] before the endpoint definition instead of [Microsoft.Http.Mvc.HttpPut].
Now it's creating a new entity even if the passed Id exists.
I went above an interesting link and tried to install Microsoft.AspNet.Mvc as stated in this link, and modified [Microsoft.AspNetCore.Mvc.HttpPut] to [System.Web.Mvc.HttpPut] without success. I also tried [System.Http.Mvc.HttpPut] with no luck.
Every time I call the PUT endpoint, instead of answering a 204 NO CONTENT with the updated object, it answers a 201 CREATED.
I tried with Postman, same result;
The issue is that according to documentation, my Mvc Application should accept application/json Content-Type header by default. But even when I try with swagger, the parameters are passed in URL.
Here's my OData UsersController:
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();
//[EnableQuery]
//public IQueryable<User> GetUsers()
//{
// var q = _dbContext.Users.AsQueryable();
// return q;
//}
// 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);
}
/*protected override void Dispose(bool disposing)
{
if (disposing)
{
_dbContext.Dispose();
}
base.Dispose(disposing);
}*/
private bool UserExists(Guid key)
{
return _dbContext.Users.Any(e => e.Id == key);
}
}
}
Here's my Program.cs with the Cors policy definition:
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();
I'm a bit lost here, any help would be welcome.
This issue came from multiple causes.
I had to add
[FromBody]to theUserparameter to specify that I was waiting for JSON Javascript body.The JSON that I was sending was malformed, it was lacking some
UserClass defined attributes. This was causing the request to create an emptyUsercause theUserparameter wasn't linked to the JSON Body due to missing attributes, and theGuidwas null in theUserreceived by the endpoint, causing the Put endpoint to create another user with a new Guid, returning a201 CREATEDwithout raising any error.For information I used
HttpPostfromMicrosoft.AspNetCore.Mvc, so I don't think the download of Microsoft ASP.NET MVC package changed a thing.It was still working without defining JsonOptions in the
Program.cs.Here's the endpoint definition that works: