2

After upgrading my API from ASP.Net Core 2.1 to 2.2, I experienced some InvalidOperationException exceptions at runtime.

Callstack

System.InvalidOperationException:
   at Microsoft.AspNetCore.Mvc.CreatedAtActionResult.OnFormatting (Microsoft.AspNetCore.Mvc.Core, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsync (Microsoft.AspNetCore.Mvc.Core, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.AspNetCore.Mvc.ObjectResult.ExecuteResultAsync (Microsoft.AspNetCore.Mvc.Core, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker+<InvokeResultAsync>d__20.MoveNext (Microsoft.AspNetCore.Mvc.Core, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)

Cause

This happened due to a missing parameter in my CreatedAtAction.

public async Task<IActionResult> GetFields([FromRoute]Guid productId)
{
    return Ok(await _prodRepo.GetProductFields(productId));
}

public async Task<IActionResult> CreateFields([FromRoute]Guid productId, [FromBody]ProductField[] fields)
{
    ...
    return CreatedAtAction(nameof(GetFields), fields);
}

Resolution

The last line should have included the productId route value as follows:

return CreatedAtAction(nameof(GetFields), { productId }, fields);

The same error occurs when route value names mismatch. It did not cause runtime exceptions in 2.1, but I guess it failed silently for anyone relying on the return URL.

The Bigger Problem

The same exception occurs at runtime if the route values are incorrectly named. The code compiles without error nor warning and fails during runtime.

My concern is that these mistakes are easy to introduce and easy to miss during development, and only surface at runtime. We have a team of developers actively working on this Web API project. It will happen again when a signature of an action method changes, referenced from another actions's CreatedAtAction result.

How can I unit test for this error case? I do not want to have to run integration tests on all the API endpoints to catch runtime errors before I deploy.

Attempt

I tried the following, but then found I need an UrlHelper and got stuck at this point.

[Fact]
public void Test_CreateFields_CreatedAtAction_Values()
{
    var fields = new ProductFieldRel { ProductId = _productId };
    var task = _controller.CreateFields(_productId, new[] { fields });
    var result = task.Result as CreatedAtActionResult;

    Assert.NotNull(result);

    result.OnFormatting(new ControllerContext
    {
        HttpContext = new DefaultHttpContext { User = _user }
    });
}
carlmon
  • 396
  • 6
  • 20
  • In memory integration tests (TestServer) should be enough to surface those run-time errors. The framework in its current incarnation wont allow for that to be easily surfaced via isolated unit tests because of all the plumbing (pipeline) needed as you have already experienced. – Nkosi Dec 05 '18 at 23:15
  • Reference [Integration tests in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-2.2) – Nkosi Dec 05 '18 at 23:17
  • 1
    Another possible option would be to create a generic extension using expressions that would allow for proper binding. That was something people used to do with the previous version of Asp.net MVC Web API 2, before core. The resulting expression would look like `return CreatedAtAction(() => GetFields(productId), fields);` and the expression would be parsed and the correct route generated. – Nkosi Dec 05 '18 at 23:23
  • Thanks Nkosi, I am interested in the extension method. It will be better than a unit test, because it fails even earlier at design time. Where can I find out how to build it and return the CreatedAtActionResult? – carlmon Dec 06 '18 at 11:32
  • Hold on let me check if I have an answer supporting the older version. Should just be a matter of porting it to core. – Nkosi Dec 06 '18 at 11:33
  • 1
    The concept is similar to what I proposed in the following answer https://stackoverflow.com/a/44334632/5233410 – Nkosi Dec 06 '18 at 11:36
  • Glad you got it to work. Now no more magic strings. Yay :) – Nkosi Dec 11 '18 at 08:07
  • You should add a self answer of what eventually worked. – Nkosi Dec 11 '18 at 08:08

0 Answers0