SharePointOnlineへのファイルアップロード

SharePointOnlineの特定フォルダにC#でファイルをアップロードする手順です。
ここでは、ドキュメント配下にTESTフォルダを作成し、そこへアップロードすることにします。

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/17d379e3-1d73-4b65-b890-39b79bdd0fd5.png)

#環境
Visual Studio 2019
.NET Core 3.1
C#
SharePointOnline

#準備1
NuGetでMicrosoft.SharePointOnline.CSOMをインストール。
(当記事作成時点のバージョンは16.1.21909.12000)
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/e60ec5ad-d248-4222-c44c-d371ec8205a5.png)

#準備2
Azure ADでアプリの登録をする。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/2edff4ec-a8bb-a6f3-c6c1-8db75f198107.png)
<hr>
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/1564defd-8bd8-a4cf-8262-109063cdaf0e.png)
<hr>
任意の名前を付けて登録。
必要に応じてリダイレクトURI の設定を。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/b6d97530-0c86-a9ca-1abd-2a2fb370a699.png)
<hr>
**アプリケーション (クライアント) IDをメモしておく**
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/2ad86ae2-1b9f-9c23-4f9d-0cae73b844dc.png)


#準備3
登録したアプリにAPIアクセス許可を付与する。
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/8a2803e6-ffff-7d2e-ad53-c3afc4b3547e.png)
<hr>
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/847b439a-8366-235d-a785-d25caac1ae20.png)
<hr>
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/4f97d7f7-173b-392e-d814-5af5237b66d7.png)
<hr>
管理者の同意を与える
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/da398b1d-da0a-8299-cc46-ffe96ed4ce57.png)

#準備4
パブリック クライアント フローを許可する
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2240956/7aac0f4f-9db3-3b93-4b7b-991533f3fc0d.png)

#認証用クラスの作成
ここのを利用させてただきました。(https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/using-csom-for-dotnet-standard)
※パスワードの入力処理やnamespaceなどを一部変更してあります。

defaultAADAppIdを、準備2でメモした「アプリケーション (クライアント) ID」に置き換えてください。

```AuthenticationManager.cs
using Microsoft.SharePoint.Client;  
using System;  
using System.Collections.Concurrent;  
using System.Net.Http;  
using System.Security;  
using System.Text;  
using System.Text.Json;  
using System.Threading;  
using System.Threading.Tasks;  
using System.Web;  
  
namespace HogehogeSystem
{
    public class AuthenticationManager : IDisposable
    {
        private static readonly HttpClient httpClient = new HttpClient();
        private const string tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";

        //メモしたアプリケーション (クライアント) IDを代入
        private const string defaultAADAppId = "●●●●●●●●●●●●●●●●";

        // Token cache handling  
        private static readonly SemaphoreSlim semaphoreSlimTokens = new SemaphoreSlim(1);
        private AutoResetEvent tokenResetEvent = null;
        private readonly ConcurrentDictionary<string, string> tokenCache = new ConcurrentDictionary<string, string>();
        private bool disposedValue;

        internal class TokenWaitInfo
        {
            public RegisteredWaitHandle Handle = null;
        }

        public ClientContext GetContext(Uri web, string userPrincipalName, string password)
        {
            var context = new ClientContext(web);

            var userPassword = new SecureString();
            foreach (var p in password)
                userPassword.AppendChar(p);


            context.ExecutingWebRequest += (sender, e) =>
            {
                string accessToken = EnsureAccessTokenAsync(new Uri($"{web.Scheme}://{web.DnsSafeHost}"), userPrincipalName, new System.Net.NetworkCredential(string.Empty, userPassword).Password).GetAwaiter().GetResult();
                e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
            };

            return context;
        }


        public async Task<string> EnsureAccessTokenAsync(Uri resourceUri, string userPrincipalName, string userPassword)
        {
            string accessTokenFromCache = TokenFromCache(resourceUri, tokenCache);
            if (accessTokenFromCache == null)
            {
                await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);
                try
                {
                    // No async methods are allowed in a lock section  
                    string accessToken = await AcquireTokenAsync(resourceUri, userPrincipalName, userPassword).ConfigureAwait(false);
                    Console.WriteLine($"Successfully requested new access token resource {resourceUri.DnsSafeHost} for user {userPrincipalName}");
                    AddTokenToCache(resourceUri, tokenCache, accessToken);

                    // Register a thread to invalidate the access token once's it's expired  
                    tokenResetEvent = new AutoResetEvent(false);
                    TokenWaitInfo wi = new TokenWaitInfo();
                    wi.Handle = ThreadPool.RegisterWaitForSingleObject(
                        tokenResetEvent,
                        async (state, timedOut) =>
                        {
                            if (!timedOut)
                            {
                                TokenWaitInfo wi1 = (TokenWaitInfo)state;
                                if (wi1.Handle != null)
                                {
                                    wi1.Handle.Unregister(null);
                                }
                            }
                            else
                            {
                                try
                                {
                                    // Take a lock to ensure no other threads are updating the SharePoint Access token at this time  
                                    await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);
                                    RemoveTokenFromCache(resourceUri, tokenCache);
                                    Console.WriteLine($"Cached token for resource {resourceUri.DnsSafeHost} and user {userPrincipalName} expired");
                                }
                                catch (Exception ex)
                                {
                                    Console.WriteLine($"Something went wrong during cache token invalidation: {ex.Message}");
                                    RemoveTokenFromCache(resourceUri, tokenCache);
                                }
                                finally
                                {
                                    semaphoreSlimTokens.Release();
                                }
                            }
                        },
                        wi,
                        (uint)CalculateThreadSleep(accessToken).TotalMilliseconds,
                        true
                    );

                    return accessToken;

                }
                finally
                {
                    semaphoreSlimTokens.Release();
                }
            }
            else
            {
                Console.WriteLine($"Returning token from cache for resource {resourceUri.DnsSafeHost} and user {userPrincipalName}");
                return accessTokenFromCache;
            }
        }

        private async Task<string> AcquireTokenAsync(Uri resourceUri, string username, string password)
        {
            string resource = $"{resourceUri.Scheme}://{resourceUri.DnsSafeHost}";

            var clientId = defaultAADAppId;
            var body = $"resource={resource}&client_id={clientId}&grant_type=password&username={HttpUtility.UrlEncode(username)}&password={HttpUtility.UrlEncode(password)}";
            using (var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"))
            {

                var result = await httpClient.PostAsync(tokenEndpoint, stringContent).ContinueWith((response) =>
                {
                    return response.Result.Content.ReadAsStringAsync().Result;
                }).ConfigureAwait(false);

                var tokenResult = JsonSerializer.Deserialize<JsonElement>(result);
                var token = tokenResult.GetProperty("access_token").GetString();
                return token;
            }
        }

        private static string TokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)
        {
            if (tokenCache.TryGetValue(web.DnsSafeHost, out string accessToken))
            {
                return accessToken;
            }

            return null;
        }

        private static void AddTokenToCache(Uri web, ConcurrentDictionary<string, string> tokenCache, string newAccessToken)
        {
            if (tokenCache.TryGetValue(web.DnsSafeHost, out string currentAccessToken))
            {
                tokenCache.TryUpdate(web.DnsSafeHost, newAccessToken, currentAccessToken);
            }
            else
            {
                tokenCache.TryAdd(web.DnsSafeHost, newAccessToken);
            }
        }

        private static void RemoveTokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)
        {
            tokenCache.TryRemove(web.DnsSafeHost, out string currentAccessToken);
        }

        private static TimeSpan CalculateThreadSleep(string accessToken)
        {
            var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(accessToken);
            var lease = GetAccessTokenLease(token.ValidTo);
            lease = TimeSpan.FromSeconds(lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds > 0 ? lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds : lease.TotalSeconds);
            return lease;
        }

        private static TimeSpan GetAccessTokenLease(DateTime expiresOn)
        {
            DateTime now = DateTime.UtcNow;
            DateTime expires = expiresOn.Kind == DateTimeKind.Utc ? expiresOn : TimeZoneInfo.ConvertTimeToUtc(expiresOn);
            TimeSpan lease = expires - now;
            return lease;
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    if (tokenResetEvent != null)
                    {
                        tokenResetEvent.Set();
                        tokenResetEvent.Dispose();
                    }
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method  
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}

```

#使い方

```
using System;
using Microsoft.SharePoint.Client;
using System.IO;

namespace HogehogeSystem
{
    public class Sample()
    {
        //●●●●●:ドメイン名
        private const string _serverUrl = @"https://●●●●●.sharepoint.com/";
        //▲▲▲▲▲:サイト名
        private const string _serverRelativeUrl = @"sites/▲▲▲▲▲";
        //対象フォルダまでのパス
        private const string _folderPath = @"Shared%20Documents/TEST";
        //フォルダにアクセスできるユーザID
        private const string _user = @"test-user@test.co.jp";
        //フォルダにアクセスできるユーザのパスワード
        private const string _password = @"test";
        //アップロード対象のファイルパス
        private const string _targetFilePath = @"C:\files\test.txt";

        public void Upload()
        {
            var site = new Uri(_serverUrl + _serverRelativeUrl);
            using (var authenticationManager = new AuthenticationManager())
            {
                using (var context = authenticationManager.GetContext(site, _user, _password))
                {                    
                    var folder = context.Web.GetFolderByServerRelativeUrl(_folderPath);
                    var files = folder.Files;
                    context.Load(files);
                    context.ExecuteQueryAsync();

                    using (var fs = new FileStream(_targetFilePath, FileMode.Open, FileAccess.ReadWrite))
                    {
                        var fileParames = new FileCreationInformation();
                        fileParames.ContentStream = fs;
                        fileParames.Url = _folderPath + @"/test.txt";

                        files.Add(fileParames);
                        context.ExecuteQueryAsync();
                    }
                }
            }
        }

```