Search Results for

    Show / Hide Table of Contents

    Using batching

    The PnP Core SDK has been built from the ground up with batching in mind, more precisely almost all requests (gets, posts, etc) are executed via the respective SharePoint REST and Microsoft Graph batch endpoints. If you for example load the Title property of an IWeb then that request is issued via the batch endpoint where the batch consists of a single request. When you write code you can however also add more requests to a batch, effectively improving performance as you're preventing unnecessary network calls.

    In the remainder of this article you'll see a lot of context use: in this case this is a PnPContext which was obtained via the PnPContextFactory as explained in the overview article and show below:

    using (var context = await pnpContextFactory.CreateAsync("SiteToWorkWith"))
    {
        // See next chapter on how to use the PnPContext for batching requests
    }
    

    A typical use case

    When you want to add items to a list you can do that one by one or you can group the items to add in a single batch and send that batch via one of the Execute methods, the latter approach is much faster and as easy to code as the first.

    using (var context = await pnpContextFactory.CreateAsync("SiteToWorkWith"))
    {
        var list = await web.Lists.AddAsync("Demo", ListTemplateType.GenericList);
    
        // Approach A: no batching
        for (int i = 0; i < 10; i++)
        {
            Dictionary<string, object> values = new Dictionary<string, object>
            {
                { "Title", $"Item {i}" }
            };
            
            // Add "list item add" to the server (= roundtrip)
            await list.Items.AddAsync(values);
        }
    
        // Approach B: batching
        for (int i = 0; i < 10; i++)
        {
            Dictionary<string, object> values = new Dictionary<string, object>
            {
                { "Title", $"Item {i}" }
            };
            
            // Add "list item add" to current batch (= NO roundtrip)
            await list.Items.AddBatchAsync(values);
        }
        
        // send the batch to server (= roundtrip)
        await context.ExecuteAsync();
    }
    

    For most of the methods in the PnP Core SDK you'll find equivalent batch methods: the followed convention is adding the "Batch" suffix e.g. we've a Get and GetAsync but also a GetBatch and GetBatchAsync. The "batch" methods have an override that allows you to specify a batch to add the request to, if you don't specify a batch the request will be added to the "current" batch. Creating a new batch can be done using the NewBatch method on PnPContext. Using a dedicated batch object you can be 100% no other code path in your application adds requests to your batch.

    using (var context = await pnpContextFactory.CreateAsync("SiteToWorkWith"))
    {
        var list = await web.Lists.AddAsync("Demo", ListTemplateType.GenericList);
    
        // Approach A: batching via the current batch
        for (int i = 0; i < 10; i++)
        {
            Dictionary<string, object> values = new Dictionary<string, object>
            {
                { "Title", $"Item {i}" }
            };
            
            // Add "list item add" to current batch (= NO roundtrip)
            await list.Items.AddBatchAsync(values);
        }
        
        // send the batch to server (= roundtrip)
        await context.ExecuteAsync();
    
        // Approach B: batching via the specific batch
        var newBatch = context.NewBatch();
        for (int i = 0; i < 10; i++)
        {
            Dictionary<string, object> values = new Dictionary<string, object>
            {
                { "Title", $"Item {i}" }
            };
            
            // Add "list item add" to current batch (= NO roundtrip)
            await list.Items.AddBatchAsync(newBatch, values);
        }
        
        // send the batch to server (= roundtrip)
        await context.ExecuteAsync(newBatch);
    }
    

    Loading data in batch

    While above example showed how to add items in a batch it's also possible to batch queries that load data. When you want to load data into the context you can use the LoadBatch or LoadBatchAsync methods, loading data into a variable can be done using the respective GetBatchAsync and GetBatch methods.

    // Create a single batch that loads SharePoint Site and Web data + Teams Members. As this involves REST and Graph the net
    // result will issue two batch requests going to Microsoft 365. One SharePoint batch request for the web + site data, one Microsoft
    // Graph batch request for the Teams data
    var batch = context.NewBatch();
    await context.Web.LoadBatchAsync(batch, p => p.Lists, p => p.Features);
    await context.Site.LoadBatchAsync(batch, p => p.Features, p => p.IsHubSite);
    await context.Team.LoadBatchAsync(batch, p => p.Members);
    await context.ExecuteAsync(batch);
    
    // Pick a list from the in batch loaded lists
    var myList = context.Web.Lists.AsRequested().FirstOrDefault(l => l.Title == "Site Pages");
    
    // Iterate over the in batch loaded Teams members
    foreach(var member in context.Team.Members.AsRequested())
    {
        // do something with the members
    }
    

    While the above approach loads data via a batch request it does load all data in a collection, all lists, features and members were loaded. What if you would want to perform a filtered batch load? For that you can use the AsBatchAsync and AsBatch methods. When you batch load data into a variable you can use the IsAvailable property of the variable to check if the batch was executed before consuming the data. In case of a collection the returned variable (e.g. siteAssets in below sample) simply allows you to query the results.

    // Do a two list gets with filter and custom properties specifying the data to load
    var siteAssets = await context.Web.Lists.QueryProperties(
            p => p.Title, p => p.TemplateType,
            p => p.ContentTypes.QueryProperties(
                p => p.Name, 
                p => p.FieldLinks.QueryProperties(p => p.Name)))
        .Where(p => p.Title == "Site Assets")
        .AsBatchAsync();
    
    var sitePages = await context.Web.Lists.QueryProperties(
            p => p.Title, p => p.TemplateType,
            p => p.ContentTypes.QueryProperties(
                p => p.Name, 
                p => p.FieldLinks.QueryProperties(p => p.Name)))
        .Where(p => p.Title == "Site Pages")
        .AsBatchAsync();
    
    // Batch results are not available right now
    Assert.IsFalse(siteAssets.IsAvailable);
    Assert.IsFalse(sitePages.IsAvailable);
    
    // Execute the batch
    await context.ExecuteAsync();
    
    // Batch results are now available
    Assert.IsTrue(siteAssets.IsAvailable);
    Assert.IsTrue(sitePages.IsAvailable);
    
    // Get the loaded data to use it
    var siteAssetsList = siteAssets.AsEnumerable().First();
    var sitePagesList = sitePages.AsEnumerable().First();
    

    In case of a collection the returned variable (e.g. siteAssets in above sample) simply allows you to query the results. When you're returning a simple type or a single model (e.g. web and site in below sample) you can access the batch loaded data via the Result property. Checking whether the batch was executed still happens via the IsAvailable property.

    var batch = context.NewBatch();
    var web = await context.Web.GetBatchAsync(batch, p => p.Lists, p => p.Features);
    var site = await context.Site.GetBatchAsync(batch, p => p.Features, p => p.IsHubSite);
    
    // Batch results are not available right now
    Assert.IsFalse(web.IsAvailable);
    
    // Execute the batch
    await context.ExecuteAsync(batch);
    
    // Batch results are now available
    Assert.IsTrue(web.IsAvailable);
    
    // Pick a list from the in batch loaded lists
    var myList = web.Result.Lists.AsRequested().FirstOrDefault(l => l.Title == "Site Pages");
    

    Batch limits

    PnP Core SDK is not imposing limits on the number of requests you can add to a single batch before executing the batch, but internally the SDK uses the official limits being maximum 20 requests for a Graph batch and 100 requests for a SharePoint REST batch. What that means is that you for example can add 1000 items into a single batch and execute that batch, during execution that batch will then be split into 10 batches of 100 items and each of these 10 batches will be executed sequentially resulting in 10 network calls to the respective batch endpoint.

    Handling batch failures

    The default behavior is that whenever a batch response is processed and a failing request inside the batch was detected an exception is thrown. This default mode is useful when you build small batches. However, when you perform bulk adds or bulk deletions then you don't want your batch processing being interrupted on a single failure. A sample case could be this: imagine you've created a batch to delete all list items from a list, but in parallel someone else already deleted an item you're also deleting via the submitted batch. With the default batch behavior you'll get an exception stating an item was not found, but you can turn off the ThrowOnError setting by providing it to the ExecuteAsync method and then the batch continues and you'll get a list of BatchResult (= the errors) which you then have to handle in your code.

    // Create a list
    var myList = await context.Web.Lists.AddAsync(listTitle, ListTemplateType.GenericList);
    
    // Add 150 items to the list
    for (int i = 1; i <= 150; i++)
    {
        Dictionary<string, object> values = new Dictionary<string, object>
            {
                { "Title", $"Item {i}" }
            };
    
        await myList.Items.AddBatchAsync(values);
    }
    await context.ExecuteAsync();
    
    // Delete some items from within both 1..100 and 101..150 sets
    await myList.Items.DeleteByIdBatchAsync(5);
    await myList.Items.DeleteByIdBatchAsync(50);
    await myList.Items.DeleteByIdBatchAsync(125);
    await context.ExecuteAsync();
    
    // Build a batch to delete all 150 items
    for (int i = 1; i <= 150; i++)
    {
        await myList.Items.DeleteByIdBatchAsync(i);
    }
    
    // Execute the batch without throwing an error, gets you a collection of errors back
    var batchResponse = await context.ExecuteAsync(false);
    
    foreach(var batchResponse in batchResponses)
    {
        // Do something with the failed request
    }
    
    

    It's also possible to track additional request information (e.g. the id of the deleted items) and then link the failing requests in a batch back to that tracked information. Below code snippet shows how this could be implemented.

    // Create a list
    var myList = await context.Web.Lists.AddAsync(listTitle, ListTemplateType.GenericList);
    
    // Add 150 items to the list
    for (int i = 1; i <= 150; i++)
    {
        Dictionary<string, object> values = new Dictionary<string, object>
            {
                { "Title", $"Item {i}" }
            };
    
        await myList.Items.AddBatchAsync(values);
    }
    await context.ExecuteAsync();
    
    // Delete some items from within both 1..100 and 101..150 sets
    await myList.Items.DeleteByIdBatchAsync(5);
    await myList.Items.DeleteByIdBatchAsync(50);
    await myList.Items.DeleteByIdBatchAsync(125);
    await context.ExecuteAsync();
    
    var batch = context.NewBatch();
    
    // Sample on how to track additional information for a batch request
    Dictionary<Guid, int> deletedListItemIds = new();
    
    for (int i = 1; i <= 150; i++)
    {
        await myList.Items.DeleteByIdBatchAsync(batch, i);
        deletedListItemIds.Add(batch.Requests.Last().Value.Id, i);
    }
    
    // Execute the batch without throwing an error, should get a result collection back
    var batchResponse = await context.ExecuteAsync(batch, false);                
    
    // Find the corresponding batch requests
    foreach (var errorResult in errorResults)
    {
        var failedListItemIdDelete = deletedListItemIds.FirstOrDefault(p=> p.Key == errorResult.BatchRequestId).Value;
    }
    
    Back to top PnP Core SDK
    Generated by DocFX with Material UI
    spacer