GraphAPIを使ってSharePointリストに大量のデータを投入する


# 概要
- PowerShellでSharePointリストに大量のデータを登録します。
- PnPやCSOMではなく、勉強を兼ねてMicrosoft Graphを使います。
- Graphのバッチ要求により、複数リクエストを一度に送信することで多数のデータを登録します。
- AzureADのアクセスの種類は要件的に「委任されたアクセス許可」によりユーザーとして登録します。

# 簡単な要件
- 管理者は、ユーザーにGraph APIの使用許可を与えSharePointリソースの操作権限を与える。
- ただし、全てのサイトを操作されては困るためユーザー自身にアクセス権があるサイトに制限したい。
- その他、ログインにMFAが設定されている場合でも利用でき、利便性を考えある程度の期間は再認証不要としたい。

# 準備
## 必要なリソース
**①AzureADのアプリ登録**
GraphAPIを実行できるよう、事前にAzureADのアプリ登録を行います。

今回の設定では以下の記事が詳しいです。

https://qiita.com/wataruf01/items/2659fde24977007cc771

**重要なポイント**
今回は「委任されたアクセス許可」により利用するユーザーの権限で接続します。
デスクトップアプリでSSOなどを実現するときに使う方式ですね。アプリにシークレットキーを埋め込む必要はありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/1b4238bb-e0e1-71eb-f56c-fdf4a7f0f254.png)

それから、SharePointサイトが操作できる権限を付与します。
今回の場合Sites.ReadWrite.AllだけでOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/11fe5a02-1ba4-26c2-fb75-b3e8e6922f2e.png)

リダイレクトURIは以下にチェックを入れておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/9f8b91db-feb9-bea8-424a-d012659cca46.png)


**②PowerShell**
AzureADの認証に必要なアクセストークンの取得を簡単にする、MSALライブラリをインストールします。
※管理者として起動が必要です。

```
Install-Module -Name MSAL.PS
```
https://github.com/AzureAD/MSAL.PS


**③SharePointリスト**
登録先となるSharePointリストです。
色々なデータ型を試したかったので、Person、DateTime、Choices、LookUp列を作成しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/347e6319-8854-8007-ac74-a5e1fbdcafcf.png)

## 必要なパラメータ
Graph APIを利用するにあたり必要なパラメータです。

以下はAzureADアプリ登録のページから取得します。

**①$clientID** = '{AzureADから取得したクライアントID}'
**②$tenantID** ='{テナントID}'
**③$redirectURI** =  ```'https://login.microsoftonline.com/common/oauth2/nativeclient'```

以下はGraph Explorerから取得した方が早いです。

**④$siteID** = {サイトID、Graph Explorerから取得します。}

```URL:サイト名で検索するクエリ
https://graph.microsoft.com/v1.0/sites?$filter=displayName eq '{your site name}'
```

⑤**\$listID** = {リストID、Graph Explorerから取得します。}

```HTML:サイトID/リスト名で検索するクエリ
https://graph.microsoft.com/v1.0/sites/{your site id}/lists?$filter=displayName eq '{your list name}'
```

## 事前知識
### Microsoft Graphのバッチリクエスト
公式ドキュメントを参照し、仕様や制約を確認しておきます。

https://docs.microsoft.com/ja-jp/graph/json-batching?view=graph-rest-1.0

- $batchリソースを使用すると、単一の要求で複数のリクエストを処理させることができる。
- 指定のJSONスキーマで複数リクエストをまとめる。
- 一バッチ20リクエスト以下。
- 順番はidで指定する。
- ただし前の要求が完了してから次の要求を処理させたい場合はdependsOnプロパティで順序指定する
    - SharePointリストに新規アイテムを登録する場合は、順序指定しないと競合か何かで失敗しました。

スロットリング制限はGraph API全体的なものと各サービス個別の制限があるようです。
ただしSharePointについては明確な閾値は開示できないようです。
要求するときに一定の間隔を開けるなどして配慮します。

https://docs.microsoft.com/ja-jp/graph/throttling


### フィールドへの値のセット方法
GraphAPIで新規アイテムをPOSTする場合の値の設定方法です。
まず一件手動でSharePointリストに登録し、値を確認してみます。

```
https://graph.microsoft.com/v1.0/sites/{your site id}/lists/{your list id}/items?expand=fields
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/7dd10d5c-9479-25d1-cd56-f4a578a890d9.png)

Bool列はtrue/false
Choices(選択肢)列は値をそのままセットすれば良いことが分かります。
奇妙なのはPerson(ユーザー)列やLookup(参照)列です。
自分で設定したPerson列などがなく、「列名+LookupId」の列になっています。標準列のEditor、Authorも同様です。
そしてその値は数字がセットされています。

調べてみると以下のようでした。
**Lookup列**
単純に「列名+LookupId」のフィールド名に対して、参照先テーブルのIDをセットするだけです。
```FieldNameLookupId : "1"```

**ユーザー列**
こちらも「列名+LookupId」のフィールド名に対してセットすれば良いのは同じです。
厄介なのがその値です。
上記画像では6や14といった数字がセットされており、ユーザーのClaimsでもオブジェクトIdでもありません。

この数字は、SharePointサイト内部で非表示のユーザー情報テーブルが作成されており、そこに登録されているユーザーのアイテムidとなるようです。
SharePointサイト内の以下のURLにアクセスすると表示されます。
```https://{tenant name}.sharepoint.com/sites/{site name}/_catalogs/users/detail.aspx```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/6c24af11-356a-6722-a738-25e362b07064.png)

Graph Explorerで以下のリソースを開くとアイテムが表示されます。
日本語環境では「ユーザー情報リスト」という名前でした。

```
https://graph.microsoft.com/v1.0/sites/{your site id}/lists?$filter=displayName eq 'ユーザー情報リスト'
```

参考サイト

https://mmsharepoint.wordpress.com/2021/02/10/use-microsoft-graph-to-create-sharepoint-items/


#データ登録方法

## PowerShellのコード
前置きが長くなりましたが、今回のコードです。
登録するデータは今回はランダムな値などにします。

```PowerShell

$ErrorActionPreference = "stop"

function main(){

    # 必要なパラメータを設定
    $clientID= '{Your clientID}'
    $tenantID ='{Your tenantID}'
    $redirectURI =  'https://login.microsoftonline.com/common/oauth2/nativeclient'
    $loginHint = '{User email}'
    $siteID = '{Your siteID}'
    $listID = '{Your listID}'

    # MSAL.psでアクセストークンを取得(TokenCache保存付き)
    $MsalClientApplication = New-MsalClientApplication -ClientId $clientID -TenantId $tenantID -RedirectUri $redirectURI
    Enable-MsalTokenCacheOnDisk $MsalClientApplication
    $Token = Get-MsalToken -PublicClientApplication $MsalClientApplication -LoginHint $loginHint

    # アクセストークンを付加
    $headers = @{
        Authorization = 'Bearer ' + $Token.AccessToken
    }

    # バッチリクエストを格納する配列
    $requests = @()
    $itemHeader = @{ "Content-Type" = "application/json" }
     
    # バッチ処理回数の設定
    $itemLimit = 100
    $batchCount = 0
    $batchSize = 20

    for ($i =1; $i -lt $itemLimit+1; $i++) {
        
        $batchCount++

        # 各リクエストのBodyを定義
        $itemBody = @{
            fields = @{
                'Title' = 'ItemTitle' + $i
                'Number' = Get-Random -Maximum 100 -Minimum 0
                'Bool' = Get-Random $true,$false
                'Choices' = Get-Random "High","Normal","Low"
                'DateTime' = Get-Date -Format "yyyy-MM-dd"
                'PersonLookupId' = Get-Random "14","6","13"
                'LookUpLookupId' = Get-Random "1","2","3","4","5","6","7"
            }
        }        
        

        # 各リクエストのスキーマを定義
        $request = @{
            id      = "$batchCount"
            method  = "POST"
            url     = "/sites/${siteID}/lists/${listID}/items"
            body    = $itemBody
            headers = $itemHeader
        }
        # 要求順序指定
        if($batchCount -ne 1) {$request["dependsOn"] = @("$($batchCount-1)")}

        # バッチ配列に追加
        $requests += $request     

        # バッチサイズごとにリクエスト
        if ($batchCount -eq $batchSize -or $i -eq $itemLimit) {
         
            # jsonスキーマに変換
            $batchRequests = @{
                requests = $requests
            }
            $batchBody = $batchRequests | ConvertTo-Json -Depth 4

            # Graph APIにbatchリクエスト
            $response = Invoke-RestMethod -Method Post -Uri 'https://graph.microsoft.com/v1.0/$batch' -Headers $headers -ContentType "application/json" -Body $batchBody
            $countSuccess = @($response.responses | Where-Object { $_.status -eq 201 }).Count
            \Write-Host "{$i} Try:$($response.responses.Count), Success: $countSuccess"

            # リセット
            $batchCount = 0
            $requests = @()

            # 500ms待機
            Start-Sleep -Milliseconds 500
        }

    }     
}

main


```

参考サイト
https://michalsacewicz.com/add-items-to-a-sharepoint-list-with-powershell-pnp-vs-batch-requests-to-graph/

## 解説

**①必要なパラメータを設定**
clientIDなどの値を事前に設定しておきます。
$loginHintは設定しなくても問題ありません。複数アカウント管理している方は優先するログインメールアドレスを設定します。
コードを実行すると認証ダイアログが開きますのでログインします。
MSAL.psの機能でリフレッシュトークンを含むトークン情報をストレージに格納するため、次回からは入力不要です。

**②バッチ処理回数の設定**
$itemLimitに登録数を設定します。

**③各リクエストのBodyを定義**
ここにHashTableでフィールドと値をセットします。

**④要求順序指定**
dependsOn属性で、前のリクエストが終わってから処理されるようにします。
設定しないと登録順がバラバラになり、また競合などで登録に失敗します。

**⑤Graph APIにbatchリクエスト**
20回に1回GraphAPIにbatchリクエストします。
batchRequests をJSONに変換したものをPOSTします。


## MSAL.psとアクセストークン

MSAL.NETを使って簡単にアクセストークンを取得するモジュールです。
取得されたトークンは、更新トークンを含むトークンキャッシュとしてメモリ内に保存されますが、
アプリやセッションが終了すると消えてしまい、次回起動時にダイアログにて再認証が必要になります。

トークンキャッシュをストレージに保存しておき、再利用したい場合は以下のように呼び出します。

```
    $MsalClientApplication = New-MsalClientApplication -ClientId $clientID -TenantId $tenantID -RedirectUri $redirectURI
    Enable-MsalTokenCacheOnDisk $MsalClientApplication
    $Token = Get-MsalToken -PublicClientApplication $MsalClientApplication -LoginHint $loginHint
```

一行目でPublicClientApplicationインスタンスを初期化します。
二行目で以下のイベントを挿入してストレージキャッシュを有効化します。
・SetBeforeAccess：tokenCacheを参照しようとする前、ストレージからトークン情報を読み込みます。
・SetAfterAccess：tokenCacheに値がセットされた後、tokenCacheをストレージに保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/ca523d6a-bdbd-83d3-ea3a-205888077586.png)

MSAL.PSの場合、トークン情報は以下の場所に保存されます。
```~\AppData\Local\MSAL.PS\```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/6d14fc24-d97b-81e4-4e98-4a175619f5d6.png)

三行目でアクセストークンを取得します。
-SilentなどのSwitchを指定しなければ、
まず.AcquireTokenSilentでトークンキャッシュ利用がトライされ、無ければ.AcquireTokenInteractiveなどで認証画面を出してアクセストークンを取得するといった流れです。

また、tokenCacheをストレージに保存する必要がなければ以下の一行だけでOKです。
-PublicClientApplicationを渡さなければ、内部で勝手にNew-MsalClientApplicationを実行してPublicClientApplicationを生成してくれます。

```
$Token = Get-MsalToken -ClientId $clientID -TenantId $tenantID -RedirectUri $redirectURI
```

素晴らしいモジュールです。
https://github.com/AzureAD/MSAL.PS/tree/master/


C#であればdocsのサンプルコードにより自分で実装する必要がありましたね・・
https://docs.microsoft.com/ja-jp/azure/active-directory/develop/msal-net-acquire-token-silently
https://github.com/Azure-Samples/active-directory-dotnet-desktop-msgraph-v2/tree/msal3x/active-directory-wpf-msgraph-v2


# その他
>一般に、Microsoft Graph は、同じ機能を実現するために CSOM や REST よりも消費するリソースが少なくて済みます。 そのため、Microsoft Graph を採用すると、アプリケーションのパフォーマンスが向上し、調整が減る可能性があります。

https://docs.microsoft.com/ja-jp/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online