1

I am currently writing a method like this:

public async Task<UserModel> GetUserByUserIdAsync(string userId)
{
    IQueryable<UserEntity> usersQuery = BuildQueryable(userId);

    bool any = await usersQuery.ExecuteQuery().AnyAsync();
    
    if (!any) return null; // wanna do other logic in the future

    return (await usersQuery.ExecuteQuery().SingleAsync()).ToUserModel();
}

As you can see, I am calling the await usersQuery.ExecuteQuery() twice, and ExecuteQuery() is a method which iterates my database and could be considered an expensive operation. Is there any way I could save my IAsyncEnumerable<T> like I normally would with IEnumerable<T> and re-use it throughout my code?

I thought about using ToListAsync() on it, but I am unsure whether that is considered good practice or not. I've also read that I could return a Task<IAsyncEnumerable<T>> and do something with that maybe. What is the best way to handle this? I'd like to implement the most efficient solution.

Axajkamkat
  • 149
  • 1
  • 6
  • How about writing the foreach yourself, that way you can iterate once and handle all contingencies yourself? – Lasse V. Karlsen Jun 02 '21 at 12:45
  • 4
    "like I normally would with IEnumerable and re-use it throughout my code?" You know that the same applies to `IEnumerable` as well? Iterating the same `IEnumerable` again *will* of course fetch the underlying datastore again as well. You have to materialize the result somehow, I´d go with `ToListAsync`. – MakePeaceGreatAgain Jun 02 '21 at 12:51
  • @HimBromBeere Well, yes, but not in this context I believe. If my `ExecuteQuery()` returns a `Task>`, I could save the list in my other method, such as `var dbResponse = ExecuteQuery();` and then I could work with dbResponse throughout the method without making multiple trips to the database. – Axajkamkat Jun 02 '21 at 12:54
  • @LasseV.Karlsen What do you mean by writing the foreach myself? Do you mean the foreach inside the `ExecuteQuery()` method? If so, that one is supposed to read all elements for the given query. You are suggesting that I should handle these cases in that method there? – Axajkamkat Jun 02 '21 at 12:55
  • You would access DB, at least once, in any case, so just do `(await usersQuery.ExecuteQuery().FirstOrDefaultAsync()).ToUserModel()` and compare it with `null` for specific logic – ASpirin Jun 02 '21 at 12:55
  • You can use `SingleOrDefaultAsync`. Either it will return 1 or null record. With that you don't need to make two calls for `any` and `single` – user1672994 Jun 02 '21 at 12:57
  • No, inside the `GetUserByUserIdAsync` method, in the method you posted in the question. But as others have mentioned, since you're only going to use the first item anyway you should be using `FirstOrDefaultAsync` or a similar method. – Lasse V. Karlsen Jun 02 '21 at 13:03
  • @LasseV.Karlsen I did have a foreach loop initially, but I wasn't sure how to check whether there are any items in the list or not. `await foreach (var item in usersQuery.ExecuteQuery()) {}` It would enter inside the loop whether there are elements or not, which is why I started looking for another solution. – Axajkamkat Jun 02 '21 at 13:06

2 Answers2

4

Why not simply use SingleOrDefaultAsync? Assuming your entity is a reference type you can get your single item, check if it is null to handle the empty-case. Another alternative is always to convert the enumerable to a list. Then you can iterate over it however many times you want to.

JonasH
  • 28,608
  • 2
  • 10
  • 23
  • I think you are correct, I was overthinking it a little bit. I was thinking if I had to re-use the list multiple times, how would I handle it in the future, but if that's the case, I could just convert it to a normal List and work with it. Are there any downsides to converting `IAsyncEnumerable` to `List`? Also, I'm not using EF Core in my case. – Axajkamkat Jun 02 '21 at 13:07
  • @axajkamkat you would need to have all items in memory. If this is a problem or not depends on the use case. Often pagination is used to limit the amount of items returned. – JonasH Jun 02 '21 at 13:09
  • Fair enough. A very useful library for these things is https://www.nuget.org/packages/System.Linq.Async (fairly new and less documentation). I could use the `ToListAsync()` to make a `List` out of the `IAsyncEnumerable` with the extension methods from that library. – Axajkamkat Jun 02 '21 at 13:14
  • I would advice against the `ToListAsync`, because the OP is interested only for the first element of the asynchronous sequence. Awaiting for all elements could result to a waste of time and resources. – Theodor Zoulias Jun 02 '21 at 16:39
1

In case there is any possibility for the single returned UserEntity to be null, and you want to differentiate between no-entity and one-null-entity, you could install the System.Linq.Async package and do this:

public async Task<UserModel> GetUserByUserIdAsync(string userId)
{
    IQueryable<UserEntity> usersQuery = BuildQueryable(userId);

    var (userEntity, exists) = await usersQuery
        .AsAsyncEnumerable()
        .Select(x => (x, true))
        .FirstOrDefaultAsync();

    if (!exists) return null; // wanna do other logic in the future
    return userEntity.ToUserModel();
}

This query exploits the fact that the default value of a ValueTuple<UserEntity, bool> is (null, false).

Using the AsAsyncEnumerable may not be as efficient as using the SingleOrDefaultAsync method though, because the data provider may create a less optimized execution plan.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104