Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.Net Core CreatedAtAction in HttpPost action returns 201 but entire request ends with 500

I'm following[1] for setting up my first .NET Core WebAPI (moving from "old" WebAPI). When I do HttpPost action, as in tutorial:

[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
    if (item == null)
    {
        return BadRequest();
    }
    TodoItems.Add(item);
    return CreatedAtAction("GetTodo", new { id = item.Key }, item);
}

I get a HttpError 500 after exiting my Controller. CreatedAtAction returns proper object with Status Code 201 as I would expect, and after my method exits the Server somehow turns it into 500. I don't seem to be able to step into the rest of the pipeline. Trace gives me following error:

81.  -GENERAL_REQUEST_ENTITY 
82.  -NOTIFY_MODULE_COMPLETION 
83.  -MODULE_SET_RESPONSE_ERROR_STATUS [Warning] 171ms

      ModuleName          AspNetCoreModule 
      Notification        EXECUTE_REQUEST_HANDLER 
      HttpStatus          500 
      HttpReason          Internal Server Error 
      HttpSubStatus       0 
      ErrorCode           The operation completed successfully. (0x0) 
      ConfigExceptionInfo

Everything (Program.cs, Startup.cs) are set up exactly as in[1]. Behaves exactly the same in IISExpress (VS2015 Update 3) and IIS7.5 (Windows 7). Other return types, 200 (with new ObjectResult(item) for GET, 404 for NotFound() or 204 NoContentResult() for PUT work ok. So it seems like it's a problem with 201 somehow. Any ideas?

[1] https://docs.asp.net/en/latest/tutorials/first-web-api.html

update Posting additional details per request:

Startup.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FirstWebApi.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace FirstWebApi
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddLogging();
            services.AddSingleton<ITodoRepository, TodoRepository>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();

            app.UseMvc();   
        }
    }
}

TodoController.cs:

using System.Collections.Generic;
using FirstWebApi.Models;
using Microsoft.AspNetCore.Mvc;

namespace FirstWebApi.Controllers
{
    [Route("api/[controller]")]
    public class TodoController : Controller
    {
        public TodoController(ITodoRepository todoItems)
        {
            TodoItems = todoItems;
        }
        public ITodoRepository TodoItems { get; set; }

        public IEnumerable<TodoItem> GetAll()
        {
            return TodoItems.GetAll();
        }

        [HttpGet("{id}", Name = "GetTodo")]
        public IActionResult GetById(string id)
        {
            var item = TodoItems.Find(id);
            if (item == null)
            {
                return NotFound();
            }
            return new ObjectResult(item);
        }

        [HttpPost]
        public IActionResult Create([FromBody] TodoItem item)
        {
            if (item == null)
            {
                return BadRequest();
            }
            TodoItems.Add(item);
            return CreatedAtAction("GetTodo", new { id = item.Key }, item);
        }

        [HttpPut("{id}")]
        public IActionResult Update(string id, [FromBody] TodoItem item)
        {
            if (item == null || item.Key != id)
            {
                return BadRequest();
            }

            var todo = TodoItems.Find(id);
            if (todo == null)
            {
                return NotFound();
            }

            TodoItems.Update(item);
            return new NoContentResult();
        }

        [HttpDelete("{id}")]
        public void Delete(string id)
        {
            TodoItems.Remove(id);
        }
    }
}

update 2

turns out, CreateAtAction with 201 tries to redirect to unhandled route:

System.InvalidOperationException: No route matches the supplied values.
   at Microsoft.AspNetCore.Mvc.CreatedAtActionResult.OnFormatting(ActionContext context)

Still not sure why tho, as the method GetById should be mapped to GetTodo per Controller settings.

like image 339
Andrzej Lichnerowicz Avatar asked Jul 14 '16 09:07

Andrzej Lichnerowicz


1 Answers

So, after a bit of debugging I found what caused it. It turned out that this.Url.Action("Get", "GetTodo", new { id=item.Key }) would return null and the 500 came from the fact that IIS could not match route attached to 201.

As it turns out, You either need to setup:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller}/{action}");
});

or use CreateAtRoute.

like image 133
Andrzej Lichnerowicz Avatar answered Oct 13 '22 10:10

Andrzej Lichnerowicz