5

I'm writing an API with .NET Core 3.1. This API has an asynchronous function called GetSomeProperty(), which I use in an endpoint (called Get).

When receiving the response from this endpoint, the results properties are "moved down" one layer, and wrapped in metadata from the asynchronous method, like this:

"results": [
    {
        "result": {//actual result here}
        "id": 1,
        "status": 5,
        "isCanceled": false,
        "isCompleted": true,
        "creationOptions": 0,
        "isFaulted": false
    },
    {
        "result": {//actual result here}
        "id": 2,
        "status": 5,
        "isCanceled": false,
        "isCompleted": true,
        "creationOptions": 0,
        "isFaulted": false
    }
]

I don't want these results to be wrapped in this "asynchronous" wrapper.

While keeping the method asynchronous, how can I return the task result, instead of an object containing the task result?

There are two reasons why I haven't used .Result:

  1. Using .Result is considered bad practice, as it can cause locks if the task hasn't yet completed.
  2. I couldn't figure out where to put it. It doesn't seem to fit anywhere, as far as I can tell.

Here is the code (please bear in mind this has been significantly diluted and simplified for example purposes):

[HttpGet]
public async Task<object> Get(string someParameter)
{   
    //Do stuff

    var things = BuildACollectionOfItems();
    var results = things.Select(x => x.IrrelevantThing).OrderBy(x => x.SomethingIrrelevant).Select(async x =>
    {
        return new
        {
            x.Id,
            SomeProperty = await GetSomeProperty(x.Id)
        };
    }).ToArray();

    return new
    {
        Results = ((IEnumerable<object>) results),
        SomeIrrelevantThing = someIrrelevantThing
    };
}

private async Task<bool> GetSomeProperty(int id)
{
    var somethingFromAServer = (await _thingProvider.GetThing()).TheProperty;

    //Do stuff here

    var thing = _context.Stuff.FirstOrDefault(x => x.Thing == somethingFromAServer);

    //Do some more stuff

    return thing.Stuff;
}
Jessica
  • 1,621
  • 2
  • 18
  • 34
  • 2
    A side note: consider a suffix `Async` for your `async` methods: `async Task GetSomePropertyAsync(int id)`. – dymanoid Jul 15 '20 at 14:02
  • 2
    Partly a guess, but is `things.Select()` awaitable? For example: `var results = await things.Select(...)` Structurally it just appears that `Get()` isn't itself awaiting anything, and results is getting a list of tasks. – David Jul 15 '20 at 14:02
  • Somewhere you're failing to unwrap a task - usually a missing `await`. Good places to check are places where you've forced the variable to be `object` - which can reference anything, including a `Task` - and check what actual types have been inferred for your `var` variables. – Damien_The_Unbeliever Jul 15 '20 at 14:05
  • @dymanoid good point, I'll keep that in mind from now on. – Jessica Jul 15 '20 at 14:06
  • @David, I just checked, it isn't awaitable apparently. Do you know what I can do to make it awaitable? (btw, I'm very new to async programming, this is my first project with it lol, so sorry about misunderstanding) – Jessica Jul 15 '20 at 14:08
  • Also, if it's any use... `things.Select(async x)` isn't the only LINQ method used... it's actually more like this: `things.Select(x).OrderBy.Select(async x)` – Jessica Jul 15 '20 at 14:09
  • 1
    @Jessica: Is `.Select()` in this case the standard LINQ method on an `IEnumerable`? If so then this looks very promising: https://stackoverflow.com/questions/14938467/calling-async-method-in-ienumerable-select Perhaps an extra couple steps, but the idea is to await all of the tasks within the results before returning the whole thing. I have no way to test it right now, but it *might* be as simple as changing your assignment at the end of that method to `Results = Task.WhenAll(results)` – David Jul 15 '20 at 14:10
  • @David It is the standard LINQ method on `IEnumerable<>`, yes. I'll look at that post now, thanks! – Jessica Jul 15 '20 at 14:12
  • @David you're a star, that works, thanks :) – Jessica Jul 15 '20 at 14:19

1 Answers1

5

Your Select returns IEnumerable<Task> (because it is passed an async function); you can use Task.WhenAll to wait for them all to complete, and unwrap the results:

[HttpGet]
public async Task<object> Get(string someParameter)
{   
    //Do stuff

    var things = BuildACollectionOfItems();
    var results = await Task.WhenAll(things
        .Select(x => x.IrrelevantThing)
        .OrderBy(x => x.SomethingIrrelevant)
        .Select(async x => new
        {
            x.Id,
            SomeProperty = await GetSomeProperty(x.Id)
        }));

    return new
    {
        Results = ((IEnumerable<object>) results),
        SomeIrrelevantThing = someIrrelevantThing
    };
}
Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35