Skip to content

Commit a4d34d9

Browse files
committed
csom for dotnet standard page
1 parent 61b2163 commit a4d34d9

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
---
2+
title: Using CSOM for .Net Standard instead of CSOM for .Net Framework
3+
description: Explains the differences between using CSOM for .Net Standard versus CSOM for .Net Framework
4+
ms.date: 6/15/2020
5+
ms.prod: sharepoint
6+
localization_priority: Priority
7+
---
8+
9+
# Using CSOM for .NET Standard instead of CSOM for .NET Framework
10+
11+
You can use the SharePoint client object model (CSOM) to retrieve, update, and manage data in SharePoint. SharePoint makes the CSOM available in several forms:
12+
13+
- .NET Framework redistributable assemblies
14+
- .NET Standard redistributable assemblies
15+
- JavaScript library (JSOM)
16+
- REST/OData endpoints
17+
18+
In this article we'll focus on explaining what the differences are between the .NET Framework version and the .NET Standard version redistributable. 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 will for the most part still be relevant when working with the .NET Standard version.
19+
20+
## Key differences between the .NET Framework version and the .NET Standard version
21+
22+
Below table outlines the differences between both versions and provides guidelines on how to handle the differences.
23+
24+
CSOM feature | .NET Framework version | .NET Standard version | Guidelines
25+
-------------|------------------------|-----------------------|------------
26+
.NET supportability | .NET Framework 4.5+ | .NET Framework 4.6.1+, .NET Core 2.0+, Mono 5.4+ (see https://docs.microsoft.com/en-us/dotnet/standard/net-standard) | It's recommended to use the CSOM for .NET Standard version for all your CSOM developments
27+
Cross platform | No | Yes (can be used on any platform that support .NET Standard) | For cross platform you have to use CSOM for .NET Standard
28+
On-Premises SharePoint support | Yes | No | The CSOM .NET Framework versions are still fully supported and being updated, so use those for on-premises SharePoint development
29+
Support for legacy authentication flows (so called cookie based auth using the `SharePointOnlineCredentials` class) | Yes | No | See the **Using modern authentication with CSOM for .NET Standard** chapter. Using Azure AD applications to configure authentication for SharePoint Online is the recommended approach
30+
`SaveBinaryDirect` / `OpenBinaryDirect` API's (webdav based) | Yes | No | Use the regular file API's in CSOM as it's not recommended to use the BinaryDirect API's, even not when using the .NET Framework version
31+
`Microsoft.SharePoint.Client.Utilities.HttpUtility` class | Yes | No | Switch to similar classes in .NET such as `System.Web.HttpUtility`
32+
`Microsoft.SharePoint.Client.EventReceivers` namespace | Yes | No | Switch to modern eventing concepts such as [Web Hooks](../apis/webhooks/get-started-webhooks.md).
33+
34+
## Using modern authentication with CSOM for .NET Standard
35+
36+
Using user/password based authentication, implemented via the `SharePointOnlineCredentials` class, is a common approach for developers using CSOM for .NET Framework. In CSOM for .NET Standard this is not 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. The recommended approach for getting access tokens for SharePoint Online is by setting up an Azure AD application. For CSOM for .NET Standard the only thing that matters is that you obtain a valid access token, this can be using resource owner password credential flow, using device login, using certificate based auth,...
37+
38+
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.
39+
40+
### Configuring an application in Azure AD
41+
42+
Below steps will help you create and configure an application in Azure Active Directory:
43+
44+
- Go to Azure AD Portal via https://aad.portal.azure.com
45+
- Click on **Azure Active Directory** and on **App registrations** in the left navigation
46+
- Click on **New registration**
47+
- Enter a name for your application and click on **Register**
48+
- Go to **API permissions** to grant permissions to your application, click on **Add a permission**, choose **SharePoint**, **Delegated permissions** and select for example **AllSites.Manage**
49+
- Click on **Grant admin consent** to consent the application's requested permissions
50+
- Click on **Authentication** in the left navigation
51+
- Change **Default client type - Treat application as public client** from No to **Yes**
52+
- Click on **Overview** and copy the application id to the clipboard (you'll need it later on)
53+
54+
### Getting an access token from Azure AD and using that in your CSOM for .NET Standard based application
55+
56+
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:
57+
58+
```csharp
59+
public ClientContext GetContext(Uri web, string userPrincipalName, SecureString userPassword)
60+
{
61+
var context = new ClientContext(web)
62+
{
63+
// Important to turn off FormDigestHandling when using access tokens
64+
FormDigestHandlingEnabled = false
65+
};
66+
67+
context.ExecutingWebRequest += (sender, e) =>
68+
{
69+
// Get an access token using your preferred approach
70+
string accessToken = MyCodeToGetAnAccessToken(new Uri($"{web.Scheme}://{web.DnsSafeHost}"), userPrincipalName, new System.Net.NetworkCredential(string.Empty, userPassword).Password);
71+
// Insert the access token in the request
72+
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
73+
};
74+
}
75+
```
76+
77+
The `ClientContext` obtained via the `GetContext` method can be used like any other `ClientContext` and will work with all your existing code. 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.
78+
79+
> [!Note]
80+
> The [PnP Sites Core library](https://github.com/pnp/PnP-Sites-Core) has a [similar AuthenticationManager class](https://github.com/pnp/PnP-Sites-Core/blob/master/Core/OfficeDevPnP.Core/AuthenticationManager.cs) that supports many more Azure AD based authentication flows.
81+
82+
#### Console app sample
83+
84+
```csharp
85+
public static async Task Main(string[] args)
86+
{
87+
Uri site = new Uri("https://contoso.sharepoint.com/sites/siteA");
88+
string user = "[email protected]";
89+
SecureString password = GetSecureString($"Password for {user}");
90+
91+
// Note: The PnP Sites Core AuthenticationManager class also supports this
92+
using (var authenticationManager = new AuthenticationManager())
93+
using (var context = authenticationManager.GetContext(site, user, password))
94+
{
95+
context.Load(context.Web, p => p.Title);
96+
await context.ExecuteQueryAsync();
97+
Console.WriteLine($"Title: {context.Web.Title}");
98+
}
99+
}
100+
```
101+
102+
#### AuthenticationManager sample class
103+
104+
> [!Note]
105+
> Update the defaultAADAppId with the application id of the app you've registered in Azure AD
106+
107+
```csharp
108+
using Microsoft.SharePoint.Client;
109+
using System;
110+
using System.Collections.Concurrent;
111+
using System.Net.Http;
112+
using System.Security;
113+
using System.Text;
114+
using System.Text.Json;
115+
using System.Threading;
116+
using System.Threading.Tasks;
117+
using System.Web;
118+
119+
namespace CSOMDemo
120+
{
121+
public class AuthenticationManager: IDisposable
122+
{
123+
private static readonly HttpClient httpClient = new HttpClient();
124+
private const string tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";
125+
126+
private const string defaultAADAppId = "986002f6-c3f6-43ab-913e-78cca185c392";
127+
128+
// Token cache handling
129+
private static readonly SemaphoreSlim semaphoreSlimTokens = new SemaphoreSlim(1);
130+
private AutoResetEvent tokenResetEvent = null;
131+
private readonly ConcurrentDictionary<string, string> tokenCache = new ConcurrentDictionary<string, string>();
132+
private bool disposedValue;
133+
134+
internal class TokenWaitInfo
135+
{
136+
public RegisteredWaitHandle Handle = null;
137+
}
138+
139+
public ClientContext GetContext(Uri web, string userPrincipalName, SecureString userPassword)
140+
{
141+
var context = new ClientContext(web)
142+
{
143+
// Important to turn off FormDigestHandling when using access tokens
144+
FormDigestHandlingEnabled = false
145+
};
146+
147+
context.ExecutingWebRequest += (sender, e) =>
148+
{
149+
string accessToken = EnsureAccessTokenAsync(new Uri($"{web.Scheme}://{web.DnsSafeHost}"), userPrincipalName, new System.Net.NetworkCredential(string.Empty, userPassword).Password).GetAwaiter().GetResult();
150+
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
151+
};
152+
153+
return context;
154+
}
155+
156+
157+
public async Task<string> EnsureAccessTokenAsync(Uri resourceUri, string userPrincipalName, string userPassword)
158+
{
159+
string accessTokenFromCache = TokenFromCache(resourceUri, tokenCache);
160+
if (accessTokenFromCache == null)
161+
{
162+
await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);
163+
try
164+
{
165+
// No async methods are allowed in a lock section
166+
string accessToken = await AcquireTokenAsync(resourceUri, userPrincipalName, userPassword).ConfigureAwait(false);
167+
Console.WriteLine($"Successfully requested new access token resource {resourceUri.DnsSafeHost} for user {userPrincipalName}");
168+
AddTokenToCache(resourceUri, tokenCache, accessToken);
169+
170+
// Register a thread to invalidate the access token once's it's expired
171+
tokenResetEvent = new AutoResetEvent(false);
172+
TokenWaitInfo wi = new TokenWaitInfo();
173+
wi.Handle = ThreadPool.RegisterWaitForSingleObject(
174+
tokenResetEvent,
175+
async (state, timedOut) =>
176+
{
177+
if (!timedOut)
178+
{
179+
TokenWaitInfo wi = (TokenWaitInfo)state;
180+
if (wi.Handle != null)
181+
{
182+
wi.Handle.Unregister(null);
183+
}
184+
}
185+
else
186+
{
187+
try
188+
{
189+
// Take a lock to ensure no other threads are updating the SharePoint Access token at this time
190+
await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);
191+
RemoveTokenFromCache(resourceUri, tokenCache);
192+
Console.WriteLine($"Cached token for resource {resourceUri.DnsSafeHost} and user {userPrincipalName} expired");
193+
}
194+
catch (Exception ex)
195+
{
196+
Console.WriteLine($"Something went wrong during cache token invalidation: {ex.Message}");
197+
RemoveTokenFromCache(resourceUri, tokenCache);
198+
}
199+
finally
200+
{
201+
semaphoreSlimTokens.Release();
202+
}
203+
}
204+
},
205+
wi,
206+
(uint)CalculateThreadSleep(accessToken).TotalMilliseconds,
207+
true
208+
);
209+
210+
return accessToken;
211+
212+
}
213+
finally
214+
{
215+
semaphoreSlimTokens.Release();
216+
}
217+
}
218+
else
219+
{
220+
Console.WriteLine($"Returning token from cache for resource {resourceUri.DnsSafeHost} and user {userPrincipalName}");
221+
return accessTokenFromCache;
222+
}
223+
}
224+
225+
private async Task<string> AcquireTokenAsync(Uri resourceUri, string username, string password)
226+
{
227+
string resource = $"{resourceUri.Scheme}://{resourceUri.DnsSafeHost}";
228+
229+
var clientId = defaultAADAppId;
230+
var body = $"resource={resource}&client_id={clientId}&grant_type=password&username={HttpUtility.UrlEncode(username)}&password={HttpUtility.UrlEncode(password)}";
231+
using (var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"))
232+
{
233+
234+
var result = await httpClient.PostAsync(tokenEndpoint, stringContent).ContinueWith((response) =>
235+
{
236+
return response.Result.Content.ReadAsStringAsync().Result;
237+
}).ConfigureAwait(false);
238+
239+
var tokenResult = JsonSerializer.Deserialize<JsonElement>(result);
240+
var token = tokenResult.GetProperty("access_token").GetString();
241+
return token;
242+
}
243+
}
244+
245+
private static string TokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)
246+
{
247+
if (tokenCache.TryGetValue(web.DnsSafeHost, out string accessToken))
248+
{
249+
return accessToken;
250+
}
251+
252+
return null;
253+
}
254+
255+
private static void AddTokenToCache(Uri web, ConcurrentDictionary<string, string> tokenCache, string newAccessToken)
256+
{
257+
if (tokenCache.TryGetValue(web.DnsSafeHost, out string currentAccessToken))
258+
{
259+
tokenCache.TryUpdate(web.DnsSafeHost, newAccessToken, currentAccessToken);
260+
}
261+
else
262+
{
263+
tokenCache.TryAdd(web.DnsSafeHost, newAccessToken);
264+
}
265+
}
266+
267+
private static void RemoveTokenFromCache(Uri web, ConcurrentDictionary<string, string> tokenCache)
268+
{
269+
tokenCache.TryRemove(web.DnsSafeHost, out string currentAccessToken);
270+
}
271+
272+
private static TimeSpan CalculateThreadSleep(string accessToken)
273+
{
274+
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(accessToken);
275+
var lease = GetAccessTokenLease(token.ValidTo);
276+
lease = TimeSpan.FromSeconds(lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds > 0 ? lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds : lease.TotalSeconds);
277+
return lease;
278+
}
279+
280+
private static TimeSpan GetAccessTokenLease(DateTime expiresOn)
281+
{
282+
DateTime now = DateTime.UtcNow;
283+
DateTime expires = expiresOn.Kind == DateTimeKind.Utc ? expiresOn : TimeZoneInfo.ConvertTimeToUtc(expiresOn);
284+
TimeSpan lease = expires - now;
285+
return lease;
286+
}
287+
288+
protected virtual void Dispose(bool disposing)
289+
{
290+
if (!disposedValue)
291+
{
292+
if (disposing)
293+
{
294+
if (tokenResetEvent != null)
295+
{
296+
tokenResetEvent.Set();
297+
tokenResetEvent.Dispose();
298+
}
299+
}
300+
301+
disposedValue = true;
302+
}
303+
}
304+
305+
public void Dispose()
306+
{
307+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
308+
Dispose(disposing: true);
309+
GC.SuppressFinalize(this);
310+
}
311+
}
312+
}
313+
```

docs/toc.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,6 +1863,9 @@
18631863
href: sp-add-ins/sharepoint-net-server-csom-jsom-and-rest-api-index.md
18641864
- name: "Complete basic operations: CSOM"
18651865
href: sp-add-ins/complete-basic-operations-using-sharepoint-client-library-code.md
1866+
items:
1867+
- name: Using CSOM for .NET Standard
1868+
href: sp-add-ins/using-csom-for-dotnet-standard.md
18661869
- name: "Complete basic operations: JSOM"
18671870
href: sp-add-ins/complete-basic-operations-using-javascript-library-code-in-sharepoint.md
18681871
- name: SharePoint REST API v1

0 commit comments

Comments
 (0)