0

I've got a function that determines a user's permissions to interact with certain objects based on a bunch of rules that require querying multiple parts of the database in different ways. One of these queries is particularly expensive (it's querying a massive view made up of unions and CTEs) and this query returns maybe 50 results or so for the current user. This particular query currently runs toward the start of this function and populates a list named userRoles. This is then followed by a series of 10 if...else if...else if... blocks, meaning that once one of these blocks matches the condition, it jumps to the end and returns the result without processing the rest of them. The userRoles list is used in 4 of these blocks, but there are 3 blocks that come before the first block that uses it. So if the conditions in any of those first 3 blocks match, then there's really no reason to query the expensive view in the first place.

In pseudo code, here's what it currently looks like:

var userRoles = await DAL.GetUserRolesAsync(userId); // Expensive query using EF

if (unrelatedConditions == 1) { doSomething1(); }
else if (unrelatedConditions == 2) { doSomething2(); }
else if (unrelatedConditions == 3) { doSomething3(); }
else if (userRoles.Any(x => x.role == "red")) { logicForRedRole(); }
else if (userRoles.Any(x => x.role == "blue")) { logicForBlueRole(); }
// more else if blocks...

What I want to have happen is that the query that populates the list would only get executed if the first three blocks have failed to match and it is now needing the list for the 4th block to test. I need to have the query defined at the top because it's actually more performant to just get all the results and then filter them down to the cases that are needed in each of the blocks than it is to run the query for each block. I've tried constructing a Task<T> object, passing in the asynchronous query (var userRolesTask = new Task<List<UserRole>>(async () => await DAL.GetUserRolesAsync(userId));), but that doesn't work because it actually creates a Task<Task<List<UserRole>>> object and has to be unwrapped to get the actual result, and I'm not sure that await userRolesTask would actually Start the query execution. I could change it to var userRolesTask = new Task<List<UserRole>>(() => DAL.GetUserRolesAsync(userId).Result);, but then it's just blocking the calling thread and I'm losing the benefits of async.

So what I need is something that functions as a Task, but that really just wraps an async method and doesn't execute until awaited. I can definitely make something clunky that would take a Func<Task<T>> and have a T Result property and a async Task<T> GetResult() method that could be awaited on that would only execute the query on the first call. I was just hoping to find something that's either built in or is used seamlessly without having to use some extra construct like await userRolesTask.GetResult().

I've seen some results in my searching that suggest using Rx Observables for doing something like this, but that is not an option on this project.

CptRobby
  • 1,441
  • 15
  • 19
  • 3
    Have you thought about simply refactoring the if elseif stack so you can move the db query down? – Fildor Apr 13 '23 at 20:40
  • Could you replace the `var` in the `var userRoles` with the actual type of the variable? – Theodor Zoulias Apr 13 '23 at 21:40
  • @TheodorZoulias What does it matter what the actual type is? It could be `List` or `IEnumerable`, it's just some dummy pseudo code for illustrative purposes. :) – CptRobby Apr 13 '23 at 22:10
  • Actually the difference between a `List` and an `IEnumerable` is huge. If you don't want to reveal the type, hoping to get answers more generally applicable, it's fine, but you might miss more targeted suggestions. – Theodor Zoulias Apr 13 '23 at 22:23

5 Answers5

3

How about extracting a method:

if (unrelatedConditions == 1) { doSomething1(); }
else if (unrelatedConditions == 2) { doSomething2(); }
else if (unrelatedConditions == 3) { doSomething3(); }
else if ( await IsUserRole() )
else if ...

//...

private async Task<bool> IsUserRole()
{
    var userRoles = await DAL.GetUserRolesAsync(userId); // Expensive query using EF

    if(userRoles.Any(x => x.role == "red")) 
    {
        logicForRedRole(); 
        return true;
    }
    if (userRoles.Any(x => x.role == "blue")) 
    {
        logicForBlueRole(); 
        return true;
    }
    return false;
}

And yet another possibility would be to maybe use Lazy<T>:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
                    
public class Program
{
    public static async Task Main()
    {
        // Just for example ...
        bool condition(int conditionNr) {
            Console.WriteLine("Testing condition #{0}", conditionNr);
            return false;
        }
        
        // The expensive Query
        var userId = 5;
        var expensiveDbAccess = new Lazy<Task<IEnumerable<int>>>(async () => {
            Console.WriteLine("Executing expensive query for User '{0}'", userId);
            await Task.Delay(1000); // "Doing some work"
            return new List<int>{1,2,3,4,5};
// This body would actually just be your
// return await DAL.GetUserRolesAsync(userId);
        });
        
        Console.WriteLine("Going down the rabbit hole ...");
        
        if(condition(1)){ Console.WriteLine("1"); }
        else if(condition(2)){ Console.WriteLine("2"); }
        else if(condition(3)){ Console.WriteLine("3"); }
        else if( (await expensiveDbAccess.Value).Contains(6) ){ Console.WriteLine("Contained 6"); }
        else if( (await expensiveDbAccess.Value).Contains(3) ){ Console.WriteLine("Contained 3"); }
        else if(condition(4)){ Console.WriteLine("4"); }
        else if(condition(5)){ Console.WriteLine("5"); }
    }
}

I tested this in a Fiddle.

Output:

Going down the rabbit hole ...
Testing condition #1
Testing condition #2
Testing condition #3
Executing expensive query for User '5'
Contained 3

If you need the example to be more complex, feel free to comment or add to your question.

Fildor
  • 14,510
  • 4
  • 35
  • 67
  • While that does solve the problem of else if blocks following an else block in the example, there's a lot more complexity to the actual function that would make this very difficult to actually implement because it's checking against properties of other objects and such. There's just too much going on with this function and it really isn't worth thinking about rearranging the blocks at all. lol – CptRobby Apr 13 '23 at 21:13
  • I am curious though about how you would use `Lazy` in the context of an async EF query. – CptRobby Apr 13 '23 at 21:15
  • @CptRobby Added example for Lazy. Mind that I added some Console.WriteLines just to showcase the flow and when the query is actually executed. – Fildor Apr 14 '23 at 06:30
  • 1
    OK. I see how it is. The Lazy basically is given the method for populating a value (which in this case is a Task) and that method only gets executed once. – CptRobby Apr 14 '23 at 13:44
  • Exactly. It takes away the boilerplate code for that common use case. – Fildor Apr 14 '23 at 16:15
  • Yeah, it's not perfect, but I think that's at least worth an up vote. I actually ended up doing something completely different today and was able to eliminate the expensive DB query altogether by analyzing what the view was doing and how each if block condition was using a different part of the massive view. I was able to make the three places that were using it just make simple queries against the actual tables that contain the relevant data and that seems to be working quite well. :) – CptRobby Apr 14 '23 at 21:30
  • I still feel like there should be a LazyTask that would take an async function and only execute it when you need to get the results. Something like `var userRolesTask = new LazyTask>(async () => await DAL.GetUserRolesAsync(userId));` and then later `else if ((await userRolesTask).Any(x => x.Role == "Red")) {...`. Having to wrap it in another construct just makes it more cumbersome to work with. Yaroslav's attempt was at least having it wait on a function call, but it suffered from having to define two things and wire them together to make it work. – CptRobby Apr 14 '23 at 21:43
  • 1
    I just posted an answer that pretty much covers my wish list and accepted that one since I think it would be the most helpful to others who are looking for what I was trying to find. But I do want to thank you for your help! I wish there was a way to give partial credit more than just an upvote. :) – CptRobby Apr 17 '23 at 17:22
2

If you want to get some kind of "lazy" loading, you can do the following:

    public async Task YourMethod(Guid userId)
    {
        if (unrelatedConditions == 1) { doSomething1(); }
        else if (unrelatedConditions == 2) { doSomething2(); }
        else if (unrelatedConditions == 3) { doSomething3(); }
        else if ((await GetRolesAsync(userId)).Any(x => x.role == "red")) { logicForRedRole(); }
        else if ((await GetRolesAsync(userId)).Any(x => x.role == "blue")) { logicForBlueRole(); }
    }

    private List<Role> _roles;
    private async ValueTask<List<Role>> GetRolesAsync(Guild userId)
    {
        return _roles ??= await DAL.GetUserRolesAsync(userId)
    }

Note, if your framework version doesn't support ValueTask, you can replace it with Task

Yaroslav Bres
  • 1,029
  • 1
  • 9
  • 20
  • That is an interesting suggestion. I've never seen `??=` used before and I had to search to find what `ValueTask` is. This won't work the way you have it written because this function is in a shared provider, so it can't store this in a private field. But this does give me an idea that I'm going to try out where the async function is defined within the method itself so that multiple calls to the function for different users would each have their own copy of it. :) – CptRobby Apr 13 '23 at 21:36
  • That's basically a handrolled `Lazy` ... but still absolutely valid. – Fildor Apr 14 '23 at 06:26
  • @YaroslavBres I just posted an answer and accepted it since I think it would be the most helpful to others who are looking for what I was trying to find. But I do want to thank you for your help! It got me going down the right path and I wish there was a way to give partial credit more than just an upvote. :) – CptRobby Apr 17 '23 at 17:25
0
var userRoles;

if (unrelatedConditions == 1) { doSomething1(); }
else if (unrelatedConditions == 2) { doSomething2(); }
else if (unrelatedConditions == 3) { doSomething3(); }
else {
    userRoles = await DAL.GetUserRolesAsync(userId); // Expensive query using EF
    if (userRoles.Any(x => x.role == "red")) { logicForRedRole(); }
    else if (userRoles.Any(x => x.role == "blue")) { logicForBlueRole(); }
    else if { ... }
}
Sam Axe
  • 33,313
  • 9
  • 55
  • 89
  • 1
    This won't work because there are a bunch of other else if blocks afterwards. I was just posting a simplified version. – CptRobby Apr 13 '23 at 20:41
  • Maybe post a [mcve], so we don't have to guess what the problem is. – Fildor Apr 13 '23 at 20:43
  • 1
    I did. I guess I just assumed that "etc etc etc" would be understood to mean the other else if blocks. Thanks though for at least making an attempt. :) – CptRobby Apr 13 '23 at 20:45
  • I really fail to see why the answer's code should not work even with unrelated conditional cases afterwards... at least you could _make_ it work. Maybe by extracting the cases regarding db into a function... – Fildor Apr 13 '23 at 20:48
  • You can't have an else if following an else block. I mean, you could move all the subsequent else if blocks inside the else block, but in addition to being ugly to read, the actual code is far more complex than this little bit of pseudo code and it just wouldn't work right. The pseudo code is only meant as an illustrative example to help visualize the problem, not the problem itself. I am looking for something that can be used in multiple places. :) – CptRobby Apr 13 '23 at 20:55
0

You can implement and declare it as IQuerable. When you will need it then, you can call the ToList() method in that function. So it will be executed after the ToList() method is called.

Abdus Salam Azad
  • 5,087
  • 46
  • 35
  • I did make an attempt at that (the actual code doesn't use a DAL). But that actually runs the query against the database multiple times, which kills performance even more than running it one time even when it isn't necessary. Thanks for the suggestion though. :) – CptRobby Apr 13 '23 at 20:59
-1

As stated in my comment on Fildor's answer, I ended up taking a completely different route and was able to completely refactor out the expensive query into smaller parts that each only get executed when they are actually needed.

But I was still interested in solving the problem for the future and, taking inspiration from Yaroslav's answer, I was able to come up with something over the weekend that almost perfectly satisfies what I was looking for. It's a really simple generic function that you can just throw into a utility class for easy use. Here's the code:

public static Func<Task<T>> CreateLazyTask<T>(Func<Task<T>> asyncCall)
{
    Task<T> executedTask = null;
    return () => executedTask ??= asyncCall();
}

Using my initial sample code, this can be used like so:

var userRolesTask = CreateLazyTask(() => DAL.GetUserRolesAsync(userId)); // Expensive query using EF

if (unrelatedConditions == 1) { doSomething1(); }
else if (unrelatedConditions == 2) { doSomething2(); }
else if (unrelatedConditions == 3) { doSomething3(); }
else if ((await userRolesTask()).Any(x => x.role == "red")) { logicForRedRole(); }
else if ((await userRolesTask()).Any(x => x.role == "blue")) { logicForBlueRole(); }
// more else if blocks...

There are a couple things to point out with this implementation. First off is that there's no async/await used and it's just returning the Task<T> from the asyncCall, so it's not trying to create a state machine or anything for this, nor is it going to have to rewrap the results into a completed Task<T> in subsequent calls (which is what Yaroslav's code does), there's only one Task<T> created by asyncCall and that is returned for every call to the function returned by CreateLazyTask (which is similar to Fildor's Lazy<T> code).

Also, I'm pretty sure this should be thread safe (in the traditional sense). So it should be possible to create a function with this and pass it to a collection of worker threads and it would get executed the first time it was needed and if other threads are awaiting it before completion, it will just handle it correctly and resume all the threads when the task completes and subsequent calls would just get the result directly and continue on. I don't think Yaroslav's method would be thread safe (I think concurrent calls would trigger multiple executions of the query until the result is returned) and I'm not entirely sure how the Lazy<T> example would behave in that scenario.

--Thinking about it a little more, there might actually be a very small time slice where multiple calls could trigger execution multiple times (between when asyncCall is executed and the first await is hit so that it can assign the Task<T> back to executedTask), but that is typically going to be a much smaller window than waiting for it to actually complete. There are also a bunch of other complexities regarding what type of data is being returned and how it would be used that would need to be considered before actually using something like this in a scenario that would allow for parallel execution.--

Next is that there's no need to specify the type of value that is returned, since the compiler is able to determine that directly from the passed in asyncCall. Also, there's no need to use await on something other than what is created (like the Value property on the Lazy<T> example), it's just awaiting the results of the created function. So this is much more easily readable.

So this is basically the best parts of Fildor's and Yaroslav's answers all wrapped into a very neatly contained generic function that can be used anywhere for any type of async call. I hope this helps out anyone who was having similar difficulties to what I was having!

--- Edit in response to comment about thread safety ---

So Stephen Cleary apparently wrote an article about making an async version of Lazy<T> called AsyncLazy<T>. In there, he stated that "It will not be executed more than once, even when multiple threads attempt to start it simultaneously (this is guaranteed by the Lazy type)." So since he said that, I believe that the Lazy<T> method would be thread safe. (Though looking at the reference source for Lazy<T>, I'm seeing comments that make me think that it's still not 100%...) There are some other oddities in that article that I don't understand (like his use of Task.Run, which he specifically discourages in numerous other articles), but overall it's pretty good and it looks like he's changed it a bit since then in his AsyncEx project (including the removal of Task.Run).

But for anyone who needs to use what I wrote where it needs to be thread safe, here's a version that relies on Lazy<T> to prevent race conditions:

public static Func<Task<T>> CreateLazyTask<T>(Func<Task<T>> asyncCall)
{
    var lazy = new Lazy<Task<T>>(asyncCall);
    return () => lazy.Value;
}
CptRobby
  • 1,441
  • 15
  • 19
  • The question makers no mention about concurrency, so IMHO this answer is off-topic. It could be on-topic as an answer on [this](https://stackoverflow.com/questions/28340177/enforce-an-async-method-to-be-called-once "Enforce an async method to be called once") question, but it would be a flawed answer because the proposed solution is not thread-safe. – Theodor Zoulias Apr 17 '23 at 17:40
  • @TheodorZoulias But damn, that link you provided nearly has what I was looking for to begin with: `AsyncLazy` from Stephen Cleary himself! If you had just posted that last week as an answer instead of asking me to clarify what the return type of the fictional DAL.GetUserRolesAsync function was, I would have probably accepted it. LOL – CptRobby Apr 17 '23 at 18:48