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 the User
parameter to specify that I was waiting for JSON Javascript body.
The JSON that I was sending was malformed, it was lacking some User
Class defined attributes. This was causing the request to create an empty User
cause the User
parameter wasn't linked to the JSON Body due to missing attributes, and the Guid
was null in the User
received by the endpoint, causing the Put endpoint to create another user with a new Guid, returning a 201 CREATED
without raising any error.
For information I used HttpPost
from Microsoft.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:
[HttpPut]
[EnableCors("MyPolicy")]
public async Task<IActionResult> Put([FromODataUri] Guid key, [FromBody] User User) {}