Utilisation de CSOM pour .NET Standard au lieu de CSOM pour .NET Framework
Vous pouvez utiliser le modèle objet client (CSOM) SharePoint pour extraire, mettre à jour et gérer des données dans SharePoint. SharePoint rend le modèle objet disponible sous plusieurs formes :
- Assemblys redistribuables .NET Framework
- Assemblies redistribuables .NET Standard
- Bibliothèque JavaScript (JSOM)
- Points de terminaison REST/OData
Dans cet article, nous allons nous concentrer sur l’explication des différences entre la version .NET Framework et la version .NET Standard redistribuable. Dans de nombreux cas, les deux versions sont identiques et si vous avez écrit du code à l’aide de la version .NET Framework, le code et tout ce que vous avez appris est toujours pertinent la plupart du temps lorsque vous utilisez la version Standard .NET.
Principales différences entre la version .NET Framework et la version .NET Standard
Le tableau ci-dessous décrit les différences entre les deux versions et fournit des instructions sur la façon de gérer les différences.
| Fonctionnalité CSOM | Version NET Framework | Version .NET Standard | Conseils |
|---|---|---|---|
| Capacité de prise en charge .NET | .NET Framework 4.5+ | .NET Framework 4.6.1 +, .NET Core 2.0 +, mono 5.4 + (.NET docs) | Nous vous recommandons d’utiliser CSOM pour la version Standard .NET pour tous vos développements de CSOM SharePoint Online |
| Multiplateforme | Non | Oui (peut être utilisé sur toute plateforme prenant en charge .NET Standard) | Pour les plateformes transversales, vous devez utiliser CSOM pour .NET Standard |
| Support SharePoint local | Oui | Non | Les versions CSOM .NET Framework sont tout de même entièrement prises en charge et sont mises à jour. Vous pouvez donc les utiliser pour le développement SharePoint local |
Prise en charge des flux d’authentification hérités (par « authentification basée sur les cookies » à l’aide de la classe SharePointOnlineCredentials) |
Oui | Non | Consultez le chapitre Utilisation de l’authentification moderne avec CSOM pour .NET Standard. L’utilisation des applications Azure AD pour configurer l’authentification pour SharePoint Online est l’approche recommandée. |
SaveBinaryDirect / OpenBinaryDirect API (basées sur WebDAV) |
Oui | Non | Utilisez les API de fichier normales dans CSOM. Il n’est pas recommandé d’utiliser l’API BinaryDirect, même si vous utilisez la version .NET Framework |
Classe Microsoft.SharePoint.Client.Utilities.HttpUtility |
Oui | Non | Basculer vers des classes similaires dans .NET telles que System.Web.HttpUtility |
Microsoft.SharePoint.Client.EventReceivers Espace de noms |
Oui | Non | Basculer vers les concepts d’événements modernes, tels que Raccordements web. |
Notes
La version .NET Standard des Assemblages CSOM est incluse dans le package NuGet existant appelé Microsoft.SharePointOnline.CSOM à partir de la version 16.1.20211.12000. L’exemple ci-dessous nécessite cette version ou une version supérieure pour fonctionner dans un projet ciblé .Net core/standard.
Utilisation de l’authentification moderne avec CSOM pour .NET Standard
L’authentification basée sur l’utilisateur/mot de passe, implémentée via la classe SharePointOnlineCredentials , est une approche courante pour les développeurs qui utilisent CSOM pour .NET Framework. Dans CSOM pour .NET Standard, cela n’est plus possible. Il appartient au développeur d’utiliser CSOM pour .NET Standard pour obtenir un jeton d’accès OAuth et de l’utiliser lors de l’appel de SharePoint Online. L’approche recommandée pour obtenir des jetons d’accès pour SharePoint Online consiste à configurer une application Azure AD. Pour CSOM pour .NET Standard, l’essentiel est que vous obteniez un jeton d’accès valide, ce qui peut être l’utilisation du flux d’informations d’identification de mot de passe de ressource, l’utilisation de la connexion de l’appareil à l’aide de l’authentification basée sur les certificats,...
Dans ce chapitre, nous allons utiliser un flux d’informations d’identification du mot de passe de la ressource propriétaire OAuth qui génère un jeton d’accès OAuth qui est ensuite utilisé par CSOM pour authentifier les demandes sur SharePoint Online, car il imite le comportement de la classe SharePointOnlineCredentials.
Configuration d’une application dans Azure Active Directory
Les étapes ci-dessous vous aideront à créer et configurer une application dans Azure Active Directory :
- Accéder au portail Azure AD via https://aad.portal.azure.com
- Sélection sur Azure Active Directory et sur Inscriptions des applications dans le volet de navigation gauche
- Sélectionnez Nouvelle inscription
- Entrez un nom pour votre application, puis sélectionnez Inscrire
- Accédez à Autorisations de l’API pour accorder des autorisations à votre application, sélectionnez Ajouter une autorisation, choisissez SharePoint, Autorisations déléguées, enfin sélectionnez par exemple AllSites.Manage
- Sélectionnez Accorder l’autorisation de l’administrateur pour accorder les autorisations demandées par l’application.
- Sélectionnez Authentification dans le volet de navigation gauche
- Modifier Autoriser les flux clients publics de Non à Oui
- Sélectionnez Vue d’ensemble, puis copiez l’ID de l’application dans le Presse-papiers (vous en aurez besoin plus tard)
Obtenir un jeton d’accès à partir d’Azure AD et l’utiliser dans votre application CSOM basée sur .NET Standard
Lors de l’utilisation de CSOM pour .NET Standard, il incombe au développeur d’obtenir un jeton d’accès pour SharePoint Online et de s’assurer qu’il est inséré dans chaque appel passé à SharePoint Online. Voici ci-dessous un modèle de code couramment utilisé :
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;
};
}
Le ClientContext obtenu via la méthode GetContext peut être utilisé comme n’importe quel autre ClientContext et fonctionne avec tout votre code existant. Les extraits de code ci-dessous illustrent une classe d’aide et une application de console utilisant la classe d’assistance. La réutilisation de ces classes facilite l’implémentation d’un équivalent pour la classe SharePointOnlineCredentials .
Notes
La bibliothèque principale de sites PnP possède une classe AuthenticationManager semblable qui prend en charge un grand nombre d’autres flux d’authentification Azure sur Active Directory.
Exemple d’application console
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}");
}
}
Classe d’exemple AuthenticationManager
Notes
Mettre à jour defaultAADAppId avec l’ID de l’application que vous avez enregistrée dans Azure AD
Notes
Si vous utilisez CSOM pour .NET Standard avec les fonctions Azure v3, vous pouvez rencontrer une erreur d’exécution liée à System.IdentityModel.Tokens.JWT. Pour résoudre ce problème, vous pouvez suivre cette solution de contournement.
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);
}
}
}