Search code examples
c#entity-framework-coreasp.net-core-mvcodata

Odata Put endpoint doesn't work as expected on ASP.NET Core MVC web service


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.


Solution

  • This issue came from multiple causes.

    1. I had to add [FromBody] to the User parameter to specify that I was waiting for JSON Javascript body.

    2. 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.

    3. 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) {}