Batching w/ Microsoft Graph API – Part II


Batching w/ Microsoft Graph API – Part I used the Microsoft batch example as a guide, but their example created a folder in my [OneDrive for Business] account. Instead, I wanted to target a [SharePoint Online] site. And to specify which SPO site, the API requires the site ID…

FYI, the Graph API site search is useful to query a site ID. The REST response object will return the SPO site matches with their respective site IDs.


Process:

  • Search for SPO site target:
    • e.g., Modern
  • Return the ID of the site.
  • Search for document library target:
    • e.g., Documents
  • Return the ID of the document library.
  • Create folders in target library of target site.

Using the PowerShell ISE, construct a site search request. This query isn’t a wildcard, so literal matches are returned. The site ID is a part of the response body:

$GraphAPIRequest = Invoke-RestMethod `
        -Uri "https://graph.microsoft.com/v1.0/sites?search=Modern" `
        -Headers $Header `
        -Method "GET" `
        -ContentType "application/json"
$siteGuid = ($GraphAPIRequest.Value).id

Because the site ID is needed below, the function above stored it in a variable: $siteGuid. This variable is used to query the document libraries of the target SPO site, and that target library ID is stored in another variable: $librGuid.

$GraphAPIRequest = Invoke-RestMethod `
        -Uri "https://graph.microsoft.com/v1.0/sites/$($siteGuid)/drives/" `
        -Headers $Header `
        -Method "GET" `
        -ContentType "application/json"
$librGuid = ($GraphAPIRequest.value | ? { $_.name -eq "Documents" }).id

NOTE: SPO document libraries are drives in the API.


Rather than creating the folders one-by-one, batching allows multiple folders to be created at once.

  • E.g., create 20 folders with one request:
$listOfFolders = @(
    "Folder 001", "Folder 002", "Folder 003", "Folder 004",
    "Folder 005", "Folder 006", "Folder 007", "Folder 008", 
    "Folder 009", "Folder 010", "Folder 011", "Folder 012",
    "Folder 013", "Folder 014", "Folder 015", "Folder 016", 
    "Folder 017", "Folder 018", "Folder 019", "Folder 020" 
)

Note: There is a current limitation of 20 individual requests per batch.


The $siteID and $librID are stored, then passed as parameters, along with the array of folders, to format the JSON payload:

CreateFolderBatch `
     -SiteID $siteGuid `
     -LibrID $librGuid `
     -HeaderObj $Header `
     -ListOfFolders $listOfFolders
function CreateFolderBatch() {
    param(
        [System.String] $GraphAPI = "https://graph.microsoft.com/v1.0",
        [Parameter(Mandatory)] [System.String] $SiteID,
        [Parameter(Mandatory)] [System.String] $LibrID,
        [Parameter(Mandatory)] [System.Array] $ListOfFolders,
        [Parameter(Mandatory)] $HeaderObj
    )

    $thisRequest = @()
    $thisHeader = @{
        "Content-Type" = "application/json"
    }

    for($i = 0; $i -lt $ListOfFolders.Count; $i++) {
        $thisRequest += @{
            "url" = "/sites/$($SiteID)/drives/$($LibrID)/root/children"
            "method" = "POST"
            "id" = "$($i + 1)"
            "body" = @{
                "name" = "$($ListOfFolders[$i])"
                "folder" = @{}
            }
            "headers" = $thisHeader
        }
    }

    $jsonBody = @{
        "requests" = $thisRequest
    } | ConvertTo-Json -Depth 5
    $jsonBody | Out-Host

    $API = "$($GraphAPI)/`$batch"
    Invoke-RestMethod `
        -Uri $API `
        -Headers $HeaderObj `
        -Body $jsonBody `
        -Method POST `
        -ContentType "application/json" `
        -UseBasicParsing
}

Note: Set the -Depth parameter when converting to JSON and data structures are nested.


PowerShell Success!
SPO Success!

Conclusion:
Batching has a few limitations and a learning curve, but well worth the effort. Too many API calls can result in Microsoft throttling requests. But combining calls can help avoid the 429 error: Too many requests.

“I’m going to speak the truth when I’m asked about it. This isn’t for look. This isn’t for publicity or anything like that. This is for people that don’t have the voice.”

Colin Kaepernick

4 thoughts on “Batching w/ Microsoft Graph API – Part II

  1. oops, ignore previous post. This was a great post, but I am getting a ‘bad request’ error [does not contain Content-Type header or body] and I cannot, for the life of me, see why it thinks there is no Content-Type header or body. I can run a single item creation to a list, but when I try the batch syntax for two items, I get the ‘bad request’ message

    So this syntax works for the single item creation:
    $actionURL = ‘https://graph.microsoft.com/v1.0/sites/’ + $siteID + ‘/lists/’ + $listID + ‘/items’

    $body = @{
    fields = @{
    “Title” = “Event Viewer”;
    “EventDate” = “4/30/2021”;
    “Log” = “Security”;
    “EventID” = 4624;
    “Computer” = “1PH64LL”;
    “Action” = “logon”;
    “Account” = “bob”;
    “IP” = “”;
    “msg” = “An account was successfuly logged on again”;
    }
    }

    $bodyJSON = $body | ConvertTo-Json -Depth 4
    $request = Invoke-RestMethod -Method Post -Headers $authHeader -Uri $actionURL -ContentType ‘application/json’ -Body $bodyJSON
    ====================================================================

    However, the corresponding batch syntax does not:
    $contentTypeHeader=@{“ContentType” = “application/json”}
    $trunActionUrl = “/sites/” + $siteID + “/lists/” + $listID + “/items”

    $requests=@()

    # ITEM 1
    $body = @{
    fields = @{
    “Title” = “Event Viewer”;
    “EventDate” = “3/12/2021”;
    “Log” = “Security”;
    “EventID” = 4624;
    “Computer” = “ABCDEF”;
    “Action” = “logon”;
    “Account” = “ophelia”;
    “IP” = “”;
    “msg” = “An account was logged off”;
    }
    }

    #build POST request for the current item and add to $requests array
    $request = @{
    “headers” = $contentTypeHeader
    “body” = $body
    “id” = “1”
    “method” = “POST”
    “url” = $trunActionUrl
    }

    $requests += $request

    # ITEM 2
    $body = @{
    fields = @{
    “Title” = “Event Viewer”;
    “EventDate” = “5/16/2021”;
    “Log” = “Security”;
    “EventID” = 4624;
    “Computer” = “wksta3”;
    “Action” = “logon”;
    “Account” = “pierre”;
    “IP” = “”;
    “msg” = “An account was logged off”;
    }
    }

    #build POST request for the current item and add to $requests array
    $request = @{
    “headers” = $contentTypeHeader
    “body” = $body
    “id” = “2”
    “method” = “POST”
    “url” = $trunActionUrl
    }

    $requests += $request

    $batchRequests = @{
    “requests” = $requests
    }

    $batchBody = $batchRequests | ConvertTo-Json -depth 5

    try {
    Invoke-RestMethod “https://graph.microsoft.com/v1.0/`$batch” -Headers $authHeader -Body $batchBody -Method Post -ContentType “application/json”
    }
    catch {
    $streamReader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream())
    $ErrResp = $streamReader.ReadToEnd() | ConvertFrom-Json
    $streamReader.Close()
    }

    $ErrResp

    error
    —–
    @{code=BadRequest; message=Write request id : 1 does not contain Content-Type header or body.; innerError=}

    $batchbody looks like this:

    > $batchBody
    {
    “requests”: [
    {
    “url”: “/sites/12345678-1234-1234-1234-1234567890ab/lists/abcdefgh-abcd-abcd-abcd-abcdefghijkl/items”,

    “method”: “POST”,
    “body”: {
    “fields”: {
    “Account”: “ophelia”,
    “EventDate”: “3/12/2021”,
    “IP”: “”,
    “Computer”: “ABCDEF”,
    “Log”: “Security”,
    “Action”: “logon”,
    “Title”: “Event Viewer”,
    “EventID”: 4624,
    “msg”: “An account was logged off”
    }
    },
    “headers”: {
    “ContentType”: “application/json”
    },
    “id”: “1”
    },
    {
    “url”: “/sites/12345678-1234-1234-1234-1234567890ab/lists/abcdefgh-abcd-abcd-abcd-abcdefghijkl/items”,

    “method”: “POST”,
    “body”: {
    “fields”: {
    “Account”: “pierre”,
    “EventDate”: “5/16/2021”,
    “IP”: “”,
    “Computer”: “wksta3”,
    “Log”: “Security”,
    “Action”: “logon”,
    “Title”: “Event Viewer”,
    “EventID”: 4624,
    “msg”: “An account was logged off”
    }
    },
    “headers”: {
    “ContentType”: “application/json”
    },
    “id”: “2”
    }
    ]
    }

    Like

  2. Bless you!!! Embarrassed but relieved! Thank you so much for getting back to me so quickly. Sorry for taking up your time, but I’ve been looking at this for 2 days and could not see that missing dash (eyes rolling).

    Liked by 1 person

Leave a comment