Search code examples
c#.netlinqselectfunc

Better way to Select to an async Func?


I've been refactoring a common pattern in my project and found it's not as simple as using a LINQ Select to an async function.

For context, here is how it is done currently.

async Task<ICollection<Group>> ExecuteQueryGroupsForDomain(DomainInfo domain, int batchSize, CancellationToken ct)
{
    try
    {
        return await BlahBlahActuallyGoGetGroupsForDomainHere(domain, batchSize, ct);
    }
    catch (Exception e)
    {
        return null;
    }
}

var executeQueries = new List<Func<CancellationToken, Task<ICollection<Group>>>>();

domains.ForEach(domain =>
    executeQueries.Add(async (ct) =>
        await ExecuteQueryGroupsForDomain(domain, 123, ct)));


Now if I try to replace the ForEach loop section using LINQ:

var executeQueries = domains.Select(domain =>
    async (ct) => await ExecuteQueryGroupsForDomain(domain, 123, ct));

It complains Type arguments cannot be inferred by the usage which leads me to believe I'm not returning anything from the Select, but I clearly am returning the Func I want.

Is there a better way of creating a list of Func's, ideally avoiding explicit casts? Also is there any explanation why the compiler is unable to infer the type when the Select'd async method is clearly telling it what the type should be?


To be clear, I do need to pass the CancellationToken to the Func's because it is a different token than the external one (specifically, it is a linked token tying the external one to another internal one).


Solution

  • The problem is in the returns of the select, for the compiler is not clear what the type of the return is. So, you need to explicitly the type of the return, here are 2 ways:

    executeQueries = domains.Select(domain => 
        new Func<CancellationToken, Task<ICollection<Group>>>(token => 
            this.ExecuteQueryGroupsForDomain(domain, 123, token))).ToList();
    
    executeQueries = domains
        .Select<DomainInfo, Func<CancellationToken, Task<ICollection<Group>>>>(domain =>
            ct => this.ExecuteQueryGroupsForDomain(domain, 123, ct)).ToList();
    

    ======================================================================

    EDIT 1: The compiler can't infer the type from a lambda expression because a lambda is just a shorthand for an anonymous method, not a type. So, you need to be explicitly and indicate the return type of the method, if the return is a base Delegate or other delegate type, like Action, Func, etc. Review this other answer, where explain the error compiler based on the C# 4 spec.

    If you need transform your original code in something more readable, I don't think there is another way more readable. Here are other ways the code can be written:

    foreach (var domain in domains) {
        executeQueries.Add(token => this.ExecuteQueryGroupsForDomain(domain, 123, token));
    }
    executeQueries.AddRange(domains
        .Select(domain => (Func<CancellationToken, Task<ICollection<Group>>>) (token => 
            this.ExecuteQueryGroupsForDomain(domain, 123, token))));
    executeQueries =
        (from domain in domains
        select new Func<CancellationToken, Task<ICollection<Group>>>(token => 
            this.ExecuteQueryGroupsForDomain(domain, 123, token))).ToList()