Pages

Friday, August 23, 2024

How to return any REST coded response (including failures) from .NET Core Web API

Okay, look, if there's one thing I'm tired of, it's half-baked example code that doesn't anticipate changes needed to push it into production.

Like the good ole WeatherForecastController from the .NET Core WebAPI template.

using Microsoft.AspNetCore.Mvc;

namespace MyApp .Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", 
            "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet(Name = "GetWeatherForecast")]
        public IEnumerable<WeatherForecast> Get()
        {
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

I mean, Visual Studio immediately complains:

Remove this unread private field '_logger' or refactor the code to use its value.

Well, duh. We have an endpoint with no logging. When would we need to log? Probably when we're doing something more complicated than creating random 8-ball style forecasts.

So let's pretend it's more difficult, throw in a try... catch, and actually log the exception.

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
    try
    {
        return new ActionResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }));
    }
    catch (Exception e)
    {
        _logger.LogError(e, "Something failed");
        return BadRequest("that didn't work");
    }
}

Guess what? Now we got TWO errors! YAY!!

  1. CS0029 Cannot implicitly convert type 'Microsoft.AspNetCore.Mvc.ActionResult<System.Collections.Generic.IEnumerable<MyApp.WeatherForecast>>' to 'System.Collections.Generic.IEnumerable<MyApp.WeatherForecast>'
  2. CS0266 Cannot implicitly convert type 'Microsoft.AspNetCore.Mvc.BadRequestObjectResult' to 'System.Collections.Generic.IEnumerable<MyApp.WeatherForecast>'. An explicit conversion exists (are you missing a cast?)

Dare you to tell me what to do next. Heck, I don't know. I do know WebAPIs have been around so long there are tons of wrong answers on the net.

Let's just show one example that does work and call it a day.

[HttpGet(Name = "GetWeatherForecast")]
public ActionResult<IEnumerable<WeatherForecast>> Get()
{
    try
    {
        return new ActionResult<IEnumerable<WeatherForecast>>(Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }));
    }
    catch (Exception e)
    {
        _logger.LogError(e, "Something failed");
        return BadRequest("that didn't work");
    }
}

Why do I need to wrap the return type with ActionResult to be able to return BadRequest REST code? I don't know man. It should implicitly cast imo into any hard type return value. I mean it's not like we're literally returning IEnumerable<WeatherForecast> in the original code. We are doing some magic behind the scenes to make that a JSON return value for our consumers. Why not do the same for any REST code convenience type too?

Anyhow, I just want to remember this trick for the next time it happens so to the blog it goes.

:sigh: