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

SharePoint クライアント オブジェクト モデル (CSOM) を使用して、SharePoint でデータを取得、更新、および管理できます。You can use the SharePoint client object model (CSOM) to retrieve, update, and manage data in SharePoint. SharePoint では、次のようなさまざまな形で CSOM を使用できます。SharePoint makes the CSOM available in several forms:

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

この記事では、.NET Framework バージョンと .NET Standard の再頒布可能バージョンとの違いについて説明することに重点を置いています。In this article, we'll focus on explaining what the differences are between the .NET Framework version and the .NET Standard version redistributable. 多くの点で両方のバージョンは同一であり、.NET Framework バージョンを使用してコードを記述している場合、そのコードと学習したほとんどすべてのことは、.NET Standard バージョンで作業するときにも関連性があります。In many ways both versions are identical and if you've been writing code using the .NET Framework version then that code and everything you've learned is, for the most part, still relevant when working with the .NET Standard version.

.NET Framework バージョンと .NET Standard バージョンとの主な相違点Key differences between the .NET Framework version and the .NET Standard version

次の表に、両方のバージョンの相違点と、その対処方法に関するガイドラインを示します。Below table outlines the differences between both versions and provides guidelines on how to handle the differences.

CSOM 機能CSOM feature .NET Framework のバージョン.NET Framework version .NET Standard のバージョン.NET Standard version ガイドラインGuidelines
.NET のサポート.NET supportability .NET Framework 4.5 以降.NET Framework 4.5+ .NET Framework 4.6.1 以降、.NET Core 2.0 以降、Mono 5.4 以降 (. NET ドキュメント).NET Framework 4.6.1+, .NET Core 2.0+, Mono 5.4+ (.NET docs) すべての SharePoint Online CSOM 開発に対して .NET Standard バージョンの CSOM を使用することをお勧めしますIt's recommended to use the CSOM for .NET Standard version for all your SharePoint Online CSOM developments
クロス プラットフォームCross platform いいえNo はい (.NET Standard をサポートする任意のプラットフォームで使用できます)Yes (can be used on any platform that support .NET Standard) クロス プラットフォームの場合には、CSOM for .NET Standard を使用する必要がありますFor cross platform, you have to use CSOM for .NET Standard
オンプレミスの SharePoint サポートOn-Premises SharePoint support はいYes いいえNo CSOM .NET Framework のバージョンは現在も完全にサポートされ、更新されているため、それらをオンプレミスの SharePoint 開発に使用しますThe CSOM .NET Framework versions are still fully supported and being updated, so use those for on-premises SharePoint development
(SharePointOnlineCredentialsクラスを使用した cookie ベースの認証と呼ばれる) 従来の認証フローのサポートSupport for legacy authentication flows (so called cookie based auth using the SharePointOnlineCredentials class) はいYes いいえNo CSOM for .NET Standard で先進認証を使用する」を参照してください。 See the Using modern authentication with CSOM for .NET Standard chapter. Azure AD アプリケーションを使用して SharePoint Online の認証を構成する方法をお勧めしますUsing Azure AD applications to configure authentication for SharePoint Online is the recommended approach
SaveBinaryDirect / OpenBinaryDirect API (webdav ベース)SaveBinaryDirect / OpenBinaryDirect APIs (webdav based) はいYes いいえNo .NET Framework バージョンを使用している場合でも BinaryDirect API の使用は推奨されていないため、CSOM の通常のファイル API を使用しますUse the regular file APIs in CSOM as it's not recommended to use the BinaryDirect APIs, even not when using the .NET Framework version
Microsoft.SharePoint.Client.Utilities.HttpUtility クラスMicrosoft.SharePoint.Client.Utilities.HttpUtility class はいYes いいえNo .NET で、System.Web.HttpUtility のような類似のクラスに切り替えますSwitch to similar classes in .NET such as System.Web.HttpUtility
Microsoft.SharePoint.Client.EventReceivers 名前空間Microsoft.SharePoint.Client.EventReceivers namespace はいYes いいえNo Web フックなどのモダン イベントの概念に切り替えます。Switch to modern eventing concepts such as Web Hooks.

CSOM for .NET Standard で先進認証を使用するUsing modern authentication with CSOM for .NET Standard

SharePointOnlineCredentialsクラスを介して実装されたユーザー/パスワードベースの認証を使用することは、CSOM for .NET Framework を使用する開発者にとって一般的なアプローチです。Using user/password based authentication, implemented via the SharePointOnlineCredentials class, is a common approach for developers using CSOM for .NET Framework. CSOM for .NET Standardでは、これを行うことはできなくなりました。これは、CSOM for .NET Standard を使用して OAuth アクセストークンを取得し、SharePoint Online を呼び出すときにそれを使用する開発者が決定します。In CSOM for .NET Standard this isn't possible anymore, it's up to the developer using CSOM for .NET Standard to obtain an OAuth access token and use that when making calls to SharePoint Online. SharePoint Online のアクセストークンを取得するために推奨される方法は、Azure AD アプリケーションをセットアップすることです。The recommended approach for getting access tokens for SharePoint Online is by setting up an Azure AD application. CSOM for .NET Standard では、重要なのは有効なアクセストークンを取得することのみで、これはつまり、リソース所有者のパスワード資格情報フロー、デバイスログイン、証明書ベースの認証など、様々な方法を使用できるということです。For CSOM for .NET Standard the only thing that matters are that you obtain a valid access token, this can be using resource owner password credential flow, using device login, using certificate based auth,...

この章では、OAuth リソースの所有者のパスワード資格情報フローを使用して OAuth アクセス トークンを生成し、それを CSOM が SharePoint Online に対するリクエストの認証に使用することによって、SharePointOnlineCredentials クラスの動作を模倣します。In this chapter, we'll use an OAuth resource owner password credential flow resulting in an OAuth access token that then is used by CSOM for authenticating requests against SharePoint Online as that mimics the behavior of the SharePointOnlineCredentials class.

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

次の手順では、Azure Active Directory でのアプリケーションの作成と構成について説明します。Below steps will help you create and configure an application in Azure Active Directory:

  • https://aad.portal.azure.com を経由して、Azure AD ポータルに移動しますGo to Azure AD Portal via https://aad.portal.azure.com
  • 左側のナビゲーションで、[Azure Active Directory] と [アプリの登録] を選択しますSelect Azure Active Directory and on App registrations in the left navigation
  • [新規登録] を選択します。Select New registration
  • アプリケーションの名前を入力し、[登録] をクリックしますEnter a name for your application and select Register
  • アプリケーションにアクセス許可を付与するために [API アクセス許可] に移動し、[アクセス許可の追加] を選択し、[SharePoint]、[委任されたアクセス許可] の順に選択して、たとえば [AllSites.Manage] を選択しますGo to API permissions to grant permissions to your application, select Add a permission, choose SharePoint, Delegated permissions and select for example AllSites.Manage
  • [管理者の同意の付与] を選択して、アプリケーションの要求するアクセス許可に同意しますSelect Grant admin consent to consent the application's requested permissions
  • 左側のナビゲーションで [認証] をクリックしますSelect Authentication in the left navigation
  • [既定のクライアントの種類 - アプリケーションをパブリッククライアントとして扱う] を、[いいえ] から [はい] に変更しますChange Default client type - Treat application as public client from No to Yes
  • [概要] を選択して、アプリケーション ID をクリップボードにコピーします (後で必要になります)。Select Overview and copy the application ID to the clipboard (you'll need it later on)

Azure AD からアクセス トークンを取得し、CSOM for .NET Standard ベースのアプリケーションでそのトークンを使用するGetting an access token from Azure AD and using that in your CSOM for .NET Standard-based application

.NET Standard に対して CSOM を使用する場合、SharePoint Online のアクセストークンを取得し、SharePoint Online に対して行った各呼び出しにそれが挿入されていることを確認するのは開発者の責任です。 When using CSOM for .NET Standard it's the responsibility of the developer to obtain an access token for SharePoint Online and ensure, it's inserted into each call made to SharePoint Online. それを実現するための一般的なコードパターンを次に示します。A common code pattern to realize this is shown below:

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 と同じように使用でき、既存のすべてのコードで動作します。The ClientContext obtained via the GetContext method can be used like any other ClientContext and will work with all your existing code. 次のコード スニペットは、ヘルパー クラスを使用してヘルパー クラスとコンソール アプリを示しています。これらのクラスを再利用することで、SharePointOnlineCredentials クラスに相当するものを簡単に実装できます。Below code snippets show a helper class and console app using the helper class, reusing these classes will make it easy to implement an equivalent for the SharePointOnlineCredentials class.

注意

PnP Sites コアライブラリには、多くの Azure AD ベースの認証フローをサポートする同様の AuthenticationManager クラスがあります。The PnP Sites Core library has a similar AuthenticationManager class that supports many more Azure AD based authentication flows.

コンソール アプリのサンプルConsole app sample

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 サンプル クラスAuthenticationManager sample class

注意

AZURE AD に登録したアプリのアプリケーション ID を使用して defaultAADAppId を更新します。Update the defaultAADAppId with the application id of the app you've registered in Azure AD

注意

Azure 関数 v3 での .NET Standard に対して CSOM を使用している場合は、System.IdentityModel.Tokens.Jwt に関するランタイムエラーが発生することがあります。If you are using CSOM for .NET Standard with Azure Functions v3 you may encounter a runtime error related to System.IdentityModel.Tokens.Jwt. これを解決するには、次の回避策を実行します。This can be resolved by following this workaround.

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);
        }
    }
}