2

Can someone explain me why I'm getting an error on the null-coalescing on the following method:

private readonly Product[] products = new Product[];

[HttpGet("{id}")]
public ActionResult<Product> GetById(int id)
{
    var product = products.FirstOrDefault(p => p.Id == id);
    if (product == null)
        return NotFound(); // No errors here
    return product; // No errors here

    //I want to replace the above code with this single line
    return products.FirstOrDefault(p => p.Id == id) ?? NotFound(); // Getting an error here: Operator '??' cannot be applied to operands of type 'Product' and 'NotFoundResult'
}  

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
}

What I don't understand is why the first returns are working without needs of any cast while the seconde single line null-coalescing doesn't works!

I'm targeting ASP.NET Core 2.1


Edit: Thanks @Hasan and @dcastro for the explanations, but I don't recommend to use the null-coalescing here as the NotFound() will not return the correct error code after the cast!

return (ActionResult<Product>)products?.FirstOrDefault(p => p.Id == id) ?? NotFound();

Wadjey
  • 145
  • 13
  • 1
    `return products.FirstOrDefault(p => p.Id == id) ?? (ActionResult)NotFound();` should do it. – Kirk Larkin Feb 04 '19 at 12:48
  • @KirkLarkin it will disappear the compiler warning but will it return the `NotFoundResult` properly? not sure about this. – TanvirArjel Feb 04 '19 at 13:10
  • 1
    I'm not sure you are doing yourself a big favor by trying to use this pattern, because it doesn't make your code more readable. It achieves the opposite, harder to read and maintable code – Tseng Feb 04 '19 at 13:12
  • @Tseng Keep aside readability, Actually will it work the way he wanting? This is the burning question. :) – TanvirArjel Feb 04 '19 at 13:14
  • 1
    @TanvirArjel: No it won't work the way he wants it and all other ways are just even more verbose than the first once and will end up in code duplication or unnecessary explicit wrapping around and `ActionResult`/`IActionResult`. All that to save 2 line breaks? He'd be better of with a helper method on the class `OkOrNotFound(result)`. An XY Problem imho – Tseng Feb 04 '19 at 13:21
  • @TanvirArjel Yes, the approach I've shown works, but I agree with Tseng re readability. – Kirk Larkin Feb 04 '19 at 13:22

3 Answers3

2

The OP's question can be split in two: 1) Why does the suggested null-coalescing expression not compile, and 2) is there another succinct ("single-line") way to return the result in ASP.NET Core 2.1?

As indicated in the second edit of @Hasan's answer, the result type of the null-coalescing operator is resolved according to the operand types, not the target type. Consequently, the OP's example fails, because there is no implicit conversion between Product and NotFoundResult:

products.FirstOrDefault(p => p.Id == id) ?? NotFound();

One way to fix it, while keeping succinct syntax, has been mentioned in a comment by @Kirk Larkin:

products.FirstOrDefault(p => p.Id == id) ?? (ActionResult<Product>)NotFound();

Starting with C# 8.0, you can also use a switch expression:

products.FirstOrDefault(p => p.Id == id) switch { null => NotFound(), var p => p };
Marc Sigrist
  • 3,964
  • 3
  • 22
  • 23
1

An error occurs because it is impossible to cast types.

Try this:

[HttpGet("{id}")]
public ActionResult<Product> GetById(int id)
{
    var result = products?.FirstOrDefault(p => p.Id == id);
    return result != null ? new ActionResult<Product>(result) : NotFound();
}
1
[HttpGet("{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public ActionResult<Product> GetById(int id)
{
    if (!_repository.TryGetProduct(id, out var product))
    {
        return NotFound();
    }

    return product;
}

In the preceding code, a 404 status code is returned when the product doesn't exist in the database. If the product does exist, the corresponding Product object is returned. Before ASP.NET Core 2.1, the return product; line would have been return Ok(product);.

As you can see from above code and explanation from microsoft's related page, after .NET Core 2.1 you do not need to return the exact type in the controller (ActionResult<T>) like before. To use that feature, you need to add attributes to indicate possible response types such as [ProducesResponseType(200)] and so on.

In your case, what you need to do is basically adding appropiate response type attributes to your controller method like shown below (since you develop with .NET Core 2.1).

[HttpGet("{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public ActionResult<Product> GetById(int id)

Edit:

The reason why you can not compile the program (use null-coalescing operator) is that the return types are not competible. In one case it returns product class, otherwise it returns ActionResult<T>. After updating the code as I suggested I suppose you will be able to use null-coalescing operator.

2. Edit (Answered here)

After digging the issue more deeply, I have found out that when using ternary if statements or null coalescing operator we need to explicitly specify what type we expect to be produced from that statement when multiple types possibly might be returned. As asked before here, compiler doesn’t decide which type it returns without implicitly casting it. So casting return type to ActionResult solves the problem.

return (ActionResult<Product>) products.FirstOrDefault(p=>p.id ==id) ?? NotFound();

But it’s better to add response type attributes as shown above.

Hasan
  • 1,243
  • 12
  • 27
  • Question not about this! He is actually wanting why the null coalescing operator is not compiling here? – TanvirArjel Feb 04 '19 at 13:12
  • Kinda odd to use the try-get pattern for a repository. Try/Get pattern should be only used where you typically have methods which throw exception and you want (for sake of performance and readabiliy) avoid try/catch, such as `int.Parse(...)` vs `int.TryParse(...)` – Tseng Feb 04 '19 at 13:14
  • @TanvirArjel It is so clear he is not able to compile beacuse returning product class is not deriving from ActionResult. So, types are not matched. – Hasan Feb 04 '19 at 13:17
  • Thanks for the help, but even after adding these attributes I'm still getting an error with the null-coalescing. – Wadjey Feb 04 '19 at 14:59
  • what I don't understand is what's the difference between this code: if (product == null) return NotFound(); return product; and this null-coalescing one: return products.FirstOrDefault(p => p.Id == id) ?? NotFound(); Why the first one works without casting but the seconde one doesn't works? – Wadjey Feb 04 '19 at 15:00
  • Can you also make sure NotFound() is derived from Mvc.ControllerBase, not from System.Web.Http? – Hasan Feb 04 '19 at 15:52
  • @Hasan I verified that it's well derived from ControllerBase. – Wadjey Feb 04 '19 at 16:01
  • @Wadjey can you also try with return products?.FirstOrDefault(p => p.Id == id); – Hasan Feb 04 '19 at 16:15
  • 1
    I guess I have found the correct answer. As specified here https://stackoverflow.com/a/27822029/5198054 compiler gets confused to select which type to return when using ternary if statement or null coalescing operator. So update your code as return (ActionResult) products.FirstOrDefault(p=>p.id ==id) ?? NotFound(); – Hasan Feb 04 '19 at 16:32
  • @Hasan Thanks, it's working now and I have an explanation for this, please to edit your response to mark it as resolved :) – Wadjey Feb 04 '19 at 16:49
  • @Wadjey Finally we did it and more importantly understood what is going on there! Thank you for the great question it’s been challenging but useful for me as well. I’ll update my answer. – Hasan Feb 04 '19 at 16:51
  • "[...] when using ternary if statements [...] we need to explicitly specify what type we expect to be produced [...]" More precisely: Until **C# 8.0**, the result type of the ternary conditional operator `?:` was resolved based on the operands's types, which had to be equal, or one operand had to be implicitly convertible to the other. However, starting with **C# 9.0**, resolution occurs based on the target type; it is now sufficient that both operands's types are implicitly convertible to the target type. (However, these new rules do _not_ apply to the null-coalescing `??` operator). – Marc Sigrist Nov 15 '21 at 17:48