SharePoint CSOM For .NET Standard

Introduction

 
SharePoint has an object model known as Client-side object model (CSOM) which is available for .net framework. It wasn't available for .NET standard, but now Microsoft has provided a much-awaited CSOM for .NET standard.
 
With the release of this, we can easily connect to SharePoint using an Azure AD OAuth based approach from .net core applications. 
 
So to understand how CSOM for .NET standard work let's create a .net core console application and connect to SharePoint and fetch all the items from the list
 

Create Azure AD Application

 
Step 1
 
Navigate to here.
 
Step 2
 
Click on Azure Active Directory and click on App registration.
 
Step 3
 
Click on + New registration to register Azure AD application
 
Step 4
 
Provide an appropriate name for the application -- we will use NETStandardCSOM.
 
SharePoint CSOM For .NET Standard
 
Step 5
 
Navigate to API Permission and select SharePoint for providing appropriate permission. Select delegated permission and check all the required permission. For our demo we will select AllSites.FullControl
 
SharePoint CSOM For .NET Standard
 
Step 6
 
Navigate to the authentication section and Under Default Client type select "Yes".
 
SharePoint CSOM For .NET Standard
 
Step 7
 
Copy the client ID which got generated after creating the Azure AD application
 

Create a .Net Core Console Application

 
Step 1
 
Navigate to Visual Studio and create a new project with the template as .net core console application and  name the project as NetStandardCSOM.
 
Step 2
 
 Add all the below NuGet packages
  • Microsoft.SharePoint online.CSOM - This library is CSOM for .NET Standard
  • Newtonsoft.Json
  • System.Text.Json
  • System.IdentityModel.Token.Jwt
Step 3
 
Add a class file named AuthenticationManager and add the below code.
 
This is the actual file where all the magic happens to fetch the token store in the cache so we are using OAuth 2.0 Resource Owner Password Credentials for fetching the token.
 
Please replace the client ID which we created in Step 1 in the string name defaultAADAppId.
  1. using Microsoft.SharePoint.Client;  
  2. using System;  
  3. using System.Collections.Concurrent;  
  4. using System.Net.Http;  
  5. using System.Security;  
  6. using System.Text;  
  7. using System.Text.Json;  
  8. using System.Threading;  
  9. using System.Threading.Tasks;  
  10. using System.Web;  
  11.   
  12. namespace NetStandardCSOM  
  13. {  
  14.     public class AuthenticationManager : IDisposable  
  15.     {  
  16.         private static readonly HttpClient httpClient = new HttpClient();  
  17.         private const string tokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";  
  18.   
  19.         private const string defaultAADAppId = "3de78f25-cbf5-4ec4-b9af-349c91904dc5";  
  20.   
  21.         // Token cache handling  
  22.         private static readonly SemaphoreSlim semaphoreSlimTokens = new SemaphoreSlim(1);  
  23.         private AutoResetEvent tokenResetEvent = null;  
  24.         private readonly ConcurrentDictionary<stringstring> tokenCache = new ConcurrentDictionary<stringstring>();  
  25.         private bool disposedValue;  
  26.   
  27.         internal class TokenWaitInfo  
  28.         {  
  29.             public RegisteredWaitHandle Handle = null;  
  30.         }  
  31.   
  32.         public ClientContext GetContext(Uri web, string userPrincipalName, SecureString userPassword)  
  33.         {  
  34.             var context = new ClientContext(web);  
  35.   
  36.             context.ExecutingWebRequest += (sender, e) =>  
  37.             {  
  38.                 string accessToken = EnsureAccessTokenAsync(new Uri($"{web.Scheme}://{web.DnsSafeHost}"), userPrincipalName, new System.Net.NetworkCredential(string.Empty, userPassword).Password).GetAwaiter().GetResult();  
  39.                 e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;  
  40.             };  
  41.   
  42.             return context;  
  43.         }  
  44.   
  45.   
  46.         public async Task<string> EnsureAccessTokenAsync(Uri resourceUri, string userPrincipalName, string userPassword)  
  47.         {  
  48.             string accessTokenFromCache = TokenFromCache(resourceUri, tokenCache);  
  49.             if (accessTokenFromCache == null)  
  50.             {  
  51.                 await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);  
  52.                 try  
  53.                 {  
  54.                     // No async methods are allowed in a lock section  
  55.                     string accessToken = await AcquireTokenAsync(resourceUri, userPrincipalName, userPassword).ConfigureAwait(false);  
  56.                     Console.WriteLine($"Successfully requested new access token resource {resourceUri.DnsSafeHost} for user {userPrincipalName}");  
  57.                     AddTokenToCache(resourceUri, tokenCache, accessToken);  
  58.   
  59.                     // Register a thread to invalidate the access token once's it's expired  
  60.                     tokenResetEvent = new AutoResetEvent(false);  
  61.                     TokenWaitInfo wi = new TokenWaitInfo();  
  62.                     wi.Handle = ThreadPool.RegisterWaitForSingleObject(  
  63.                         tokenResetEvent,  
  64.                         async (state, timedOut) =>  
  65.                         {  
  66.                             if (!timedOut)  
  67.                             {  
  68.                                 TokenWaitInfo wi1 = (TokenWaitInfo)state;  
  69.                                 if (wi1.Handle != null)  
  70.                                 {  
  71.                                     wi1.Handle.Unregister(null);  
  72.                                 }  
  73.                             }  
  74.                             else  
  75.                             {  
  76.                                 try  
  77.                                 {  
  78.                                     // Take a lock to ensure no other threads are updating the SharePoint Access token at this time  
  79.                                     await semaphoreSlimTokens.WaitAsync().ConfigureAwait(false);  
  80.                                     RemoveTokenFromCache(resourceUri, tokenCache);  
  81.                                     Console.WriteLine($"Cached token for resource {resourceUri.DnsSafeHost} and user {userPrincipalName} expired");  
  82.                                 }  
  83.                                 catch (Exception ex)  
  84.                                 {  
  85.                                     Console.WriteLine($"Something went wrong during cache token invalidation: {ex.Message}");  
  86.                                     RemoveTokenFromCache(resourceUri, tokenCache);  
  87.                                 }  
  88.                                 finally  
  89.                                 {  
  90.                                     semaphoreSlimTokens.Release();  
  91.                                 }  
  92.                             }  
  93.                         },  
  94.                         wi,  
  95.                         (uint)CalculateThreadSleep(accessToken).TotalMilliseconds,  
  96.                         true  
  97.                     );  
  98.   
  99.                     return accessToken;  
  100.   
  101.                 }  
  102.                 finally  
  103.                 {  
  104.                     semaphoreSlimTokens.Release();  
  105.                 }  
  106.             }  
  107.             else  
  108.             {  
  109.                 Console.WriteLine($"Returning token from cache for resource {resourceUri.DnsSafeHost} and user {userPrincipalName}");  
  110.                 return accessTokenFromCache;  
  111.             }  
  112.         }  
  113.   
  114.         private async Task<string> AcquireTokenAsync(Uri resourceUri, string username, string password)  
  115.         {  
  116.             string resource = $"{resourceUri.Scheme}://{resourceUri.DnsSafeHost}";  
  117.   
  118.             var clientId = defaultAADAppId;  
  119.             var body = $"resource={resource}&client_id={clientId}&grant_type=password&username={HttpUtility.UrlEncode(username)}&password={HttpUtility.UrlEncode(password)}";  
  120.             using (var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"))  
  121.             {  
  122.   
  123.                 var result = await httpClient.PostAsync(tokenEndpoint, stringContent).ContinueWith((response) =>  
  124.                 {  
  125.                     return response.Result.Content.ReadAsStringAsync().Result;  
  126.                 }).ConfigureAwait(false);  
  127.   
  128.                 var tokenResult = JsonSerializer.Deserialize<JsonElement>(result);  
  129.                 var token = tokenResult.GetProperty("access_token").GetString();  
  130.                 return token;  
  131.             }  
  132.         }  
  133.   
  134.         private static string TokenFromCache(Uri web, ConcurrentDictionary<stringstring> tokenCache)  
  135.         {  
  136.             if (tokenCache.TryGetValue(web.DnsSafeHost, out string accessToken))  
  137.             {  
  138.                 return accessToken;  
  139.             }  
  140.   
  141.             return null;  
  142.         }  
  143.   
  144.         private static void AddTokenToCache(Uri web, ConcurrentDictionary<stringstring> tokenCache, string newAccessToken)  
  145.         {  
  146.             if (tokenCache.TryGetValue(web.DnsSafeHost, out string currentAccessToken))  
  147.             {  
  148.                 tokenCache.TryUpdate(web.DnsSafeHost, newAccessToken, currentAccessToken);  
  149.             }  
  150.             else  
  151.             {  
  152.                 tokenCache.TryAdd(web.DnsSafeHost, newAccessToken);  
  153.             }  
  154.         }  
  155.   
  156.         private static void RemoveTokenFromCache(Uri web, ConcurrentDictionary<stringstring> tokenCache)  
  157.         {  
  158.             tokenCache.TryRemove(web.DnsSafeHost, out string currentAccessToken);  
  159.         }  
  160.   
  161.         private static TimeSpan CalculateThreadSleep(string accessToken)  
  162.         {  
  163.             var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(accessToken);  
  164.             var lease = GetAccessTokenLease(token.ValidTo);  
  165.             lease = TimeSpan.FromSeconds(lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds > 0 ? lease.TotalSeconds - TimeSpan.FromMinutes(5).TotalSeconds : lease.TotalSeconds);  
  166.             return lease;  
  167.         }  
  168.   
  169.         private static TimeSpan GetAccessTokenLease(DateTime expiresOn)  
  170.         {  
  171.             DateTime now = DateTime.UtcNow;  
  172.             DateTime expires = expiresOn.Kind == DateTimeKind.Utc ? expiresOn : TimeZoneInfo.ConvertTimeToUtc(expiresOn);  
  173.             TimeSpan lease = expires - now;  
  174.             return lease;  
  175.         }  
  176.   
  177.         protected virtual void Dispose(bool disposing)  
  178.         {  
  179.             if (!disposedValue)  
  180.             {  
  181.                 if (disposing)  
  182.                 {  
  183.                     if (tokenResetEvent != null)  
  184.                     {  
  185.                         tokenResetEvent.Set();  
  186.                         tokenResetEvent.Dispose();  
  187.                     }  
  188.                 }  
  189.   
  190.                 disposedValue = true;  
  191.             }  
  192.         }  
  193.   
  194.         public void Dispose()  
  195.         {  
  196.             // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method  
  197.             Dispose(disposing: true);  
  198.             GC.SuppressFinalize(this);  
  199.         }  
  200.     }  
  201. }   
Step 4
 
Now let us use this context and fetch the title of the site collection.
 
Replace the below code in Program.cs file.
 
Update the URI with the site collection URL for which we require a title.
 
Update Username and Password of the User.
  1. using System;  
  2. using System.Net;  
  3. using System.Security;  
  4. using System.Threading.Tasks;  
  5.   
  6. namespace NetStandardCSOM  
  7. {  
  8.     class Program  
  9.     {  
  10.         public static async Task Main(string[] args)  
  11.         {  
  12.             Uri site = new Uri("https://testinglala.sharepoint.com/");  
  13.             string user = "<<userName>>;  
  14.             string rawPassword = <<password>>;  
  15.             SecureString password = new SecureString();  
  16.             foreach (char c in rawPassword) password.AppendChar(c);  
  17.   
  18.             // Note: The PnP Sites Core AuthenticationManager class also supports this  
  19.             using (var authenticationManager = new AuthenticationManager())  
  20.             using (var context = authenticationManager.GetContext(site, user, password))  
  21.             {  
  22.                 context.Load(context.Web, p => p.Title);  
  23.                 await context.ExecuteQueryAsync();  
  24.                 Console.WriteLine($"Title: {context.Web.Title}");  
  25.             }  
  26.         }  
  27.     }  
  28. }  
Outcome
 
SharePoint CSOM For .NET Standard
 

Conclusion

 
Now with the release of CSOM for .NET Standard, we can use this in Azure Function v2 which is based on .NET core and connect to SharePoint. We can even use it in all other applications which are based on .NET Standard so we can connect to SharePoint from any platform.