Verwenden von CSOM für den .NET Standard anstelle von CSOM für das .NET Framework
Sie können das SharePoint-Clientobjektmodell (CSOM) zum Abrufen, Aktualisieren und Verwalten von Daten in SharePoint verwenden. SharePoint stellt das CSOM in verschiedenen Formen zur Verfügung:
- Weitervertreibbare Assemblys für .NET Framework
- Weitervertreibbare Assemblys für .NET Standard
- JavaScript-Bibliothek (JSOM)
- REST-/OData-Endpunkte
In diesem Artikel konzentrieren wir uns auf die Erläuterung der Unterschiede zwischen der .NET Framework-Version und der weitervertreibbaren .NET Standard-Version. Auf vielerlei Hinsicht sind beide Versionen identisch. Wenn Sie Code mit der .NET Framework-Version geschrieben haben, ist dieser Code und alles, was Sie gelernt haben, zum größten Teil weiterhin relevant, wenn Sie mit der .NET Standard-Version arbeiten.
Wichtigste Unterschiede zwischen der .NET Framework-Version und der .NET Standard-Version
Die folgende Tabelle zeigt die Unterschiede zwischen beiden Versionen und enthält Richtlinien zum Umgang mit den Unterschieden.
CSOM-Feature | .NET Framework-Version | .NET Standard-Version | Richtlinien |
---|---|---|---|
.Net-Unterstützung | .NET Framework 4.5+ | .NET Framework 4.6.1+, .NET Core 2.0+, Mono 5.4+ (.NET Dokumentation) | Es wird empfohlen, für alle SharePoint Online-CSOM-Entwicklungen die CMOS-Version für .NET Standard zu verwenden. |
Plattformübergreifend | Nein | Ja (kann auf jeder beliebigen Plattform verwendet werden, die .NET Standard unterstützt) | Für die plattformübergreifende Nutzung müssen Sie CSOM für .NET Standard einsetzen |
Lokaler SharePoint-Support | Ja | Nein | Die CSOM .NET Framework-Versionen werden weiterhin vollständig unterstützt und aktualisiert und können damit für lokale SharePoint-Entwicklungen verwendet werden. |
Unterstützung für Legacy-Authentifizierungsströme (so genannte Cookie-basierte Authentifizierungen mithilfe der SharePointOnlineCredentials -Klasse) |
Ja | Nein | Lesen Sie das Kapitel Verwendung moderner Authentifizierung mit CSOM für .NET Standard. Die Verwendung von Azure AD-Anwendungen zum Konfigurieren der Authentifizierung für SharePoint Online ist die empfohlene Vorgehensweise. |
SaveBinaryDirect / OpenBinaryDirect -APIs (WebDAV-basierend) |
Ja | Nein | Verwenden Sie die regulären Datei-APIs in CSOM, da die Verwendung der BinaryDirect-APIs nicht empfohlen wird, auch nicht bei Verwendung der .NET Framework-Version |
Microsoft.SharePoint.Client.Utilities.HttpUtility -Klasse |
Ja | Nein | Wechseln zu ähnlichen Klassen in .NET wie etwa System.Web.HttpUtility |
Microsoft.SharePoint.Client.EventReceivers Namespace |
Ja | Nein | Wechseln Sie zu modernen Ereignis-Konzepten wie etwa WebHooks. |
Hinweis
Die .NET Standard-Version der CSOM-Assemblys ist im vorhandenen NuGet-Paket „Microsoft.SharePointOnline.CSOM“ ab Version 16.1.20211.12000 enthalten. Das folgende Beispiel erfordert, dass diese Version oder höher in einem .Net Core-/Standardzielprojekt funktioniert.
Verwendung moderner Authentifizierung mit CSOM für .NET Standard
Die Verwendung von Benutzer / Kennwort-basierten Authentifizierungen, die über die SharePointOnlineCredentials
-Klasse implementiert werden, ist ein gängiger Ansatz für Entwickler, die CSOM für .NET Framework verwenden. In CSOM für .NET Standard ist das nicht mehr möglich, es ist Sache des Entwicklers, CSOM für .NET Standard zu verwenden, um ein OAuth-Zugriffstoken zu erhalten und dieses beim Aufrufen von SharePoint Online zu verwenden. Der empfohlene Ansatz für das Abrufen von Zugriffstoken für SharePoint Online ist das Einrichten einer Azure AD-Anwendung. Für CSOM für .NET Standard ist nur wichtig, dass Sie ein gültiges Zugriffstoken haben. Dieses kann erhalten werden über die Verwendung der Kennwort-Anmeldeinformationsfluss des Ressourcenbesitzers, die Verwendung einer Geräteanmeldung, die Verwendung einer zertifikatbasierten Authentifizierung usw.
In diesem Kapitel verwenden wir einen OAuth-Kennwort-Anmeldeinformationsfluss des Ressourcenbesitzers, der zu einem OAuth-Zugriffstoken führt. Dieses wird dann von CSOM zur Authentifizierung von Anforderungen gegen SharePoint Online verwendet, da dies das Verhalten der SharePointOnlineCredentials
-Klasse nachahmt.
Eine Anwendung in Azure AD konfigurieren
Nachstehend Schritte helfen Ihnen beim Erstellen und Konfigurieren einer Anwendung im Azure Active Directory:
- Wechseln Sie zum Azure AD-Portal über https://aad.portal.azure.com
- Wählen Sie Azure Active Directory und App-Registrierungen im linken Navigationsbereich aus.
- Wählen Sie Neue Registrierung aus.
- Geben Sie einen Namen für die Anwendung ein, und wählen Sie Registrieren aus.
- Wechseln Sie zu API-Berechtigungen, um Ihrer Anwendung Berechtigungen zu erteilen, wählen Sie Eine Berechtigung hinzufügen, dann SharePoint, Delegierte Berechtigungen und schließlich beispielsweise AllSites.Manage aus
- Wählen Sie Zustimmung des Administrators erteilen aus, um die Zustimmung für die erforderlichen Berechtigungen der Anwendung zu geben.
- Wählen Sie im linken Navigationsbereich Authentifizierung aus.
- Ändern Sie Öffentliche Clientflüsse zulassen von „Nein“ in Ja.
- Wählen Sie Überblick aus, und kopieren Sie die Anwendungs-ID in die Zwischenablage, da Sie diese später noch benötigen.
Abrufen eines Zugriffstokens von Azure AD und dieses in Ihrer CSOM für .NET Standard basierten Anwendung verwenden
Bei Verwendung von CSOM für .NET Standard liegt es in der Verantwortung des Entwicklers, ein Zugriffstoken für SharePoint Online zu erhalten und sicherzustellen, dass es in jeden Anruf nach SharePoint Online eingefügt wird. Ein gängiges Codemuster zur Realisierung dieses Ziels ist unten dargestellt:
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;
};
}
Der ClientContext
, der über die GetContext
-Methode erhalten wird, kann wie jeder andere ClientContext
verwendet werden und funktioniert mit Ihrem gesamten vorhandenen Code. Die folgenden Codeausschnitte zeigen eine Hilfsklasse und eine Konsolen-App, welche die Hilfsklasse verwenden. Durch die Wiederverwendung dieser Klassen lässt sich leicht ein Äquivalent für die SharePointOnlineCredentials
-Klasse implementieren.
Hinweis
Die PnP-Website Core-Bibliothek verfügt über eine ähnliche AuthenticationManager-Klasse, die viele weitere Azure AD-basierte Authentifizierungsflüsse unterstützt.
Beispiele einer Konsolen-App
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-Beispielklasse
Hinweis
Aktualisieren Sie die defaultAADAppId
mit der Anwendungs-ID der App, die Sie in Azure AD registriert haben
Hinweis
Wenn Sie CSOM für .NET Standard mit Azure Functions Version 3 verwenden, tritt möglicherweise ein Runtime-Fehler im Zusammenhang mit System.IdentityModel.Tokens.Jwt auf. Dies kann durch diese Problemumgehung behoben werden.
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);
}
}
}