次の方法で共有


CSOM for .NET Framework の代わりに CSOM for .NET Standard を使用する

SharePoint クライアント オブジェクト モデル (CSOM) を使用して、SharePoint でデータを取得、更新、および管理できます。 SharePoint では、次のようなさまざまな形で CSOM を使用できます。

  • .NET Framework 再頒布可能アセンブリ
  • .NET Standard 再頒布可能アセンブリ
  • JavaScript ライブラリ (JSOM)
  • REST/OData エンドポイント

この記事では、.NET Framework バージョンと .NET Standard の再頒布可能バージョンとの違いについて説明することに重点を置いています。 多くの点で両方のバージョンは同一であり、.NET Framework バージョンを使用してコードを記述している場合、そのコードと学習したほとんどすべてのことは、.NET Standard バージョンで作業するときにも関連性があります。

.NET Framework バージョンと .NET Standard バージョンとの主な相違点

次の表に、両方のバージョンの相違点と、その対処方法に関するガイドラインを示します。

CSOM 機能 .NET Framework のバージョン .NET Standard のバージョン ガイドライン
.NET のサポート .NET Framework 4.5 以降 .NET Framework 4.6.1 以降、.NET Core 2.0 以降、Mono 5.4 以降 (. NET ドキュメント) すべての SharePoint Online CSOM 開発に対して .NET Standard バージョンの CSOM を使用することをお勧めします
クロス プラットフォーム いいえ はい (.NET Standard をサポートする任意のプラットフォームで使用できます) クロス プラットフォームの場合には、CSOM for .NET Standard を使用する必要があります
オンプレミスの SharePoint サポート はい いいえ CSOM .NET Framework のバージョンは現在も完全にサポートされ、更新されているため、それらをオンプレミスの SharePoint 開発に使用します
(SharePointOnlineCredentialsクラスを使用した cookie ベースの認証と呼ばれる) 従来の認証フローのサポート はい いいえ CSOM for .NET Standard で先進認証を使用する」を参照してください。 Azure AD アプリケーションを使用して SharePoint Online の認証を構成する方法をお勧めします
SaveBinaryDirect / OpenBinaryDirect API (webdav ベース) はい いいえ .NET Framework バージョンを使用している場合でも BinaryDirect API の使用は推奨されていないため、CSOM の通常のファイル API を使用します
Microsoft.SharePoint.Client.Utilities.HttpUtility クラス はい いいえ .NET で、System.Web.HttpUtility のような類似のクラスに切り替えます
Microsoft.SharePoint.Client.EventReceivers 名前空間 はい いいえ Web フックなどのモダン イベントの概念に切り替えます。

注:

CSOM アセンブリの .NET 標準バージョンは、バージョン 16.1.20211.12000 以降の Microsoft.SharePointOnline.CSOM と呼ばれる既存の NuGet パッケージに含まれています。 以下のサンプルでは、.Net コア/標準のターゲット プロジェクトで機能するために、このバージョン以降が必要となります。

CSOM for .NET Standard で先進認証を使用する

SharePointOnlineCredentialsクラスを介して実装されたユーザー/パスワードベースの認証を使用することは、CSOM for .NET Framework を使用する開発者にとって一般的なアプローチです。 CSOM for .NET Standardでは、これを行うことはできなくなりました。これは、CSOM for .NET Standard を使用して OAuth アクセストークンを取得し、SharePoint Online を呼び出すときにそれを使用する開発者が決定します。 SharePoint Online のアクセストークンを取得するために推奨される方法は、Azure AD アプリケーションをセットアップすることです。 CSOM for .NET Standard では、重要なのは有効なアクセストークンを取得することのみで、これはつまり、リソース所有者のパスワード資格情報フロー、デバイスログイン、証明書ベースの認証など、様々な方法を使用できるということです。

この章では、OAuth リソースの所有者のパスワード資格情報フローを使用して OAuth アクセス トークンを生成し、それを CSOM が SharePoint Online に対するリクエストの認証に使用することによって、SharePointOnlineCredentials クラスの動作を模倣します。

Azure AD でアプリケーションを構成する

次の手順では、Azure Active Directory でのアプリケーションの作成と構成について説明します。

  • https://aad.portal.azure.com を経由して、Azure AD ポータルに移動します
  • [Azure Active Directory] を選択し、左側のナビゲーションの [アプリの登録] を選択します
  • [新規登録] を選択します。
  • アプリケーションの名前を入力し、[登録] をクリックします
  • アプリケーションにアクセス許可を付与するために [API アクセス許可] に移動し、[アクセス許可の追加] を選択し、[SharePoint]、[委任されたアクセス許可] の順に選択して、たとえば [AllSites.Manage] を選択します
  • [管理者の同意の付与] を選択して、アプリケーションの要求するアクセス許可に同意します
  • 左側のナビゲーションで [認証] をクリックします
  • [パブリック クライアント フローを許可する] を [いいえ] から [はい] に変更
  • [概要] を選択して、アプリケーション ID をクリップボードにコピーします (後で必要になります)。

Azure AD からアクセス トークンを取得し、CSOM for .NET Standard ベースのアプリケーションでそのトークンを使用する

.NET Standard に対して CSOM を使用する場合、SharePoint Online のアクセストークンを取得し、SharePoint Online に対して行った各呼び出しにそれが挿入されていることを確認するのは開発者の責任です。 それを実現するための一般的なコードパターンを次に示します。

public ClientContext GetContext(Uri web, string userPrincipalName, SecureString userPassword)
{
    context.ExecutingWebRequest += (sender, e) =>
    {
        // Get an access token using your preferred approach
        string accessToken = MyCodeToGetAnAccessToken(new Uri($"{web.Scheme}://{web.DnsSafeHost}"), userPrincipalName, new System.Net.NetworkCredential(string.Empty, userPassword).Password);
        // Insert the access token in the request
        e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
    };
}

GetContext メソッドによって取得された ClientContext は他の ClientContext と同じように使用でき、既存のすべてのコードで動作します。 次のコード スニペットは、ヘルパー クラスを使用してヘルパー クラスとコンソール アプリを示しています。これらのクラスを再利用することで、SharePointOnlineCredentials クラスに相当するものを簡単に実装できます。

注:

PnP Sites コアライブラリには、多くの Azure AD ベースの認証フローをサポートする同様の AuthenticationManager クラスがあります。

コンソール アプリのサンプル

public static async Task Main(string[] args)
{
    Uri site = new Uri("https://contoso.sharepoint.com/sites/siteA");
    string user = "joe.doe@contoso.onmicrosoft.com";
    SecureString password = GetSecureString($"Password for {user}");

    // Note: The PnP Sites Core AuthenticationManager class also supports this
    using (var authenticationManager = new AuthenticationManager())
    using (var context = authenticationManager.GetContext(site, user, password))
    {
        context.Load(context.Web, p => p.Title);
        await context.ExecuteQueryAsync();
        Console.WriteLine($"Title: {context.Web.Title}");
    }
}

AuthenticationManager サンプル クラス

注:

AZURE AD に登録したアプリのアプリケーション ID を使用して defaultAADAppId を更新します。

注:

Azure 関数 v3 での .NET Standard に対して CSOM を使用している場合は、System.IdentityModel.Tokens.Jwt に関するランタイムエラーが発生することがあります。 これを解決するには、次の回避策を実行します。

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 CSOMDemo
{
    public class AuthenticationManager: IDisposable
    {
        private static readonly HttpClient httpClient = new HttpClient();
        private const string tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";

        private const string defaultAADAppId = "986002f6-c3f6-43ab-913e-78cca185c392";

        // 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, SecureString userPassword)
        {
            var context = new ClientContext(web);

            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 internalWaitToken = (TokenWaitInfo)state;
                                if (internalWaitToken.Handle != null)
                                {
                                    internalWaitToken.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);
        }
    }
}