MStodd
MStodd

Reputation: 4746

Make Async Calls inside a Controller Action

I have a controller action, which gets a list of document types, then makes a webservice call for each document type. I want to make these all at once, so that looping over them only takes as long as the longest one. I don't know if my code is correct, and I need to do something else, or if my code is simply incorrect.

Action:

public ActionResult GetPlan(MemberViewModel request)
{
    DocService ds = new DocService();

    List<DocType> docTypes = ds.GetDocTypesForPlan(request.PlanId);

    List<CoverageDocument> coverageDocuments = ds.GetDocumentsForDocTypes(docTypes);

    return View(coverageDocuments);
}

GetDocumentsForDocTypes:

public List<CoverageDocument> GetDocumentsForDocTypes(List<DocType> planDocTypes)
{
    List<CoverageDocument> planDocuments = new List<CoverageDocument>();

    DocumentUtility documentUtility = new DocumentUtility();
    int lastYear = DateTime.Now.Year - 1;

    planDocTypes.ForEach(async (docType) =>
    {
        DocumentUtility.SearchCriteria sc = new DocumentUtility.SearchCriteria();
        sc.documentType = docType;
        Dictionary<long, Tuple<string, string>> documentList = await documentUtility.FindDocuments(sc);

        documentList.ToList().ForEach((document) =>
            {
                CoverageDocument doc = this.coverageDocumentConstructor(document);
                planDocuments.Add(doc);
            });
    });

    return planDocuments;
}

Exception:

Additional information: An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%@ Page Async="true" %>. This exception may also indicate an attempt to call an "async void" method, which is generally unsupported within ASP.NET request processing. Instead, the asynchronous method should return a Task, and the caller should await it.

Upvotes: 1

Views: 2390

Answers (1)

i3arnon
i3arnon

Reputation: 116518

Your code is incorrect. By sending an async lambda to the ForEach extension method you're forcing it to be async void which is never a good idea outside UI event handlers.

To actually be asynchronous your calls need to be async all the way:

public async Task<ActionResult> GetPlan(MemberViewModel request)
{
    DocService ds = new DocService();

    List<DocType> docTypes = ds.GetDocTypesForPlan(request.PlanId);

    List<CoverageDocument> coverageDocuments = await ds.GetDocumentsForDocTypesAsync(docTypes);

    return View(coverageDocuments);
}

public async Task<List<CoverageDocument>> GetDocumentsForDocTypesAsync(List<DocType> planDocTypes)
{
    DocumentUtility documentUtility = new DocumentUtility();
    int lastYear = DateTime.Now.Year - 1;

    var planDocuments = await Task.WhenAll(planDocTypes.Select(async (docType) =>
    {
        DocumentUtility.SearchCriteria sc = new DocumentUtility.SearchCriteria();
        sc.documentType = docType;

        return await documentUtility.FindDocuments(sc).Select((document) => this.coverageDocumentConstructor(document))
    }));

    return planDocuments.SelectMany(doc => doc);;
}

Upvotes: 6

Related Questions