Search Results for

    Show / Hide Table of Contents

    IQueryable performance considerations

    In the Requesting model collections section you saw quite a lot of different examples of how to query collections. Almost all collections inside PnP Core SDK implement an IQueryable interface.

    Thanks to this you can use LINQ expressions to dynamically filter or asynchronously load collection elements on demand. All your LINQ expressions will be accurately translated to the REST OData query operations (like $filter, $select, $expand, etc).

    Having below code:

    var lists = await context.Web.Lists
      .Where(l => l.Hidden == false && l.TemplateType == ListTemplateType.DocumentLibrary)
      .QueryProperties(p => p.Title, p => p.TemplateType, p => p.ContentTypes.QueryProperties(p => p.Name)).ToListAsync();
    

    upon execution will be translated to the below REST OData query:

    _api/web/lists?$select=Id,Title,BaseTemplate,ContentTypes/Name,ContentTypes/StringId&$expand=ContentTypes&$filter=(BaseTemplate+eq+101)
    

    It's a very powerful feature, however let's take a closer look at this technique to avoid some common performance issues.

    Important

    The most important rule of IQueryable is that an IQueryable doesn't fire a request when it's declared, but only when it's enumerated over (inside foreach cycle or when calling ToList()/ToListAsync()).

    Loading collections into the PnPContext

    Let's have a sample query to get a web's lists:

    ❌ not efficient:

    // All lists loaded into the context
    await context.Web.LoadAsync(p => p.Lists);
    
    foreach (var list in context.Web.Lists)
    {
      // do something with the list here
    }
    

    What's wrong with this code? It works just fine, however it sends two identical HTTP requests to the SharePoint server to get lists (one in LoadAsync(p => p.Lists) and the second one in the foreach cycle). Why does it happen? Because Lists property implements IQueryable, inside foreach cycle you effectively enumerate the IQueryable, as a result, it sends an HTTP request to get data.

    How to fix the code? Use AsRequested():

    ✅ better:

    // All lists loaded into the context
    await context.Web.LoadAsync(p => p.Lists);
    
    foreach (var list in context.Web.Lists.AsRequested())
    {
      // do something with the list here
    }
    

    As mentioned earlier, AsRequested() method returns an already loaded collection of items, you should use this method to avoid multiple unnecessary HTTP requests. In this case, we enumerate a collection loaded in memory before.

    Alternatively, you can also use just one cycle without LoadAsync(p => p.Lists):

    ✅ better:

    await foreach (var list in context.Web.Lists)
    {
      // do something with list here
    }
    

    In this case, list collection will be requested at the beginning of the foreach cycle. Do remember though, that if you iterate over collection again somewhere in your code path, an additional request will be sent.

    Load related properties

    The below code has a similar problem with the query efficiency:

    ❌ not efficient:

    var list = await context.Web.Lists.GetByTitleAsync("Documents", l => l.Fields);
    var fields = await list.Fields.Where(l => l.InternalName.StartsWith("tax")).ToListAsync();
    

    The first line loads a list by title and also loads related property - all list fields. On the second line we again send HTTP request to further filter fields. But what we need instead is to filter already loaded fields:

    var fields = list.Fields.AsRequested().Where(l => l.InternalName.StartsWith("tax")).ToList();
    

    To make it even more efficient, you should change it like this:

    ✅ better:

    var list = await context.Web.Lists.GetByTitleAsync("Documents");
    var fields = await list.Fields.Where(l => l.InternalName.StartsWith("tax")).ToListAsync();
    

    It doesn't make sense to load all related fields with the list request. Thus we simply send a separate request with a filter (will be translated to the $filter=startswith OData query) to get desired fields.

    Cycles and/or method calls

    Could you guess what's the problem with the below code:

    ❌ not efficient:

    var filteredList = context.Web.Lists.Where(l => l.TemplateType == ListTemplateType.DocumentLibrary);
    
    for (int i = 0; i < 10; i++)
    {
        DoSmth(filteredList);
    }
    
    private bool DoSmth(IEnumerable<IList> lists)
    {
        foreach (var list in lists)
        {
            // do smth with list
        }
    }
    

    It also works just fine, however has an issue, that the above code sends 10 HTTP requests to get lists data. The filteredList is an instance of IQueryable<IList>, that's why it doesn't execute the request immediately, but only inside the foreach cycle in the Check function. Every time we visit the function, we send an HTTP request to get lists.

    How to fix it? Change the filter query so that it executes immediately using ToList() or ToListAsync() methods:

    ✅ better:

    var filteredList = await context.Web.Lists.Where(l => l.TemplateType == ListTemplateType.DocumentLibrary).ToListAsync();
    

    The code above executes the request instantly and loads all items into the memory, thus we don't have the issue with multiple HTTP queries. The type of filteredList will be IList, not IQueryable.

    Back to top PnP Core SDK
    Generated by DocFX with Material UI
    spacer