Accedere a SharePoint da C# tramite Add-in Permissions in .NET Core
In .NET Core 3 attualmente non esiste una libreria nuget per accedere a SharePoint tramite Add-in.
Si può sopperire alla mancanza scrivendo del codice C#.
Una volta ottenuto il bearer token posso accedere a SharePoint.
mentre per gestire la risposta, dovranno essere create queste classi: SPErrorMessage, SPError, SPErrorBase, SPListItemsResponse, SPBaseItem, SPListResponse, SPUpdateItemMetadata e SPUpdateItemValue
Si può sopperire alla mancanza scrivendo del codice C#.
Access token
La prima cosa da fare è autenticarsi per ottenere un bearer token, tramite la funzione GetAccessToken, che verrà usato nelle successive chiamate alle API:C#
// da spostare nel file di configurazione e aggiornare in base al proprio tenant / add-in
private const string CFG_TENANTID = "<mio tenant id>";
private const string CFG_TENANTNAME = "<mio tenant name>";
private const string CFG_CLIENTID = "<mio client id>";
private const string CFG_CLIENTSECRET = "<mio client secret>";
//-----------------------------
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Net.Http.Headers;
using System.Threading.Tasks;
public ILogger log;
private const string LOGIN_ENDPOINT = "https://accounts.accesscontrol.windows.net/{0}/tokens/OAuth/2";
private const string SHAREPOINT_DOMAIN = "sharepoint.com";
private const string SHAREPOINT_PRINCIPAL = "00000003-0000-0ff1-ce00-000000000000";
private const string CONTENT_TYPE_JSON = "application/json";
public async Task<AuthResponse> GetAccessToken()
{
try
{
AuthResponse accessToken;
string url = string.Format(LOGIN_ENDPOINT, Settings.TenantID);
var values = new List<KeyValuePair<string, string>>();
values.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
values.Add(new KeyValuePair<string, string>("client_id", $"{CFG_CLIENTID }@{CFG_TENANTID}"));
values.Add(new KeyValuePair<string, string>("client_secret", CFG_CLIENTSECRET ));
values.Add(new KeyValuePair<string, string>("resource", $"{SHAREPOINT_PRINCIPAL}/{CFG_TENANTNAME}.{SHAREPOINT_DOMAIN}@{CFG_TENANTID}"));
using (HttpClient httpClient = new HttpClient())
{
using (var content = new FormUrlEncodedContent(values))
{
using (var postResponse = await httpClient.PostAsync(url, content))
{
string serverResponse = await postResponse.Content.ReadAsStringAsync();
log.LogTrace($"autentication response: {serverResponse}");
accessToken = JsonConvert.DeserializeObject<AuthResponse>(serverResponse);
if (accessToken.HasError)
{
throw new Exception(accessToken.Error);
}
}
}
}
return accessToken;
}
catch (Exception ex)
{
#log.LogError(ex, "GetAccessToken: An exception was thrown while fetching the token.");
throw;
}
}
Ovviamente va prima creato l'Add-in e recuperati i paramteri: TenantId, TenantName, ClientId e ClientSecret
perchè funzioni è necessario creare queste classi di supporto: ErrorResponse e AuthResponseC#
/// <summary>
/// errore base della risposta
/// </summary>
public class ErrorResponse
{
public bool HasError
{
get
{
return string.IsNullOrEmpty(Error) == false;
}
set { }
}
[JsonProperty("error")]
public string Error { get; set; }
[JsonProperty("error_description")]
public string ErrorDescription { get; set; }
[JsonProperty("error_codes")]
public long[] ErrorCodes { get; set; }
[JsonProperty("timestamp")]
public string Timestamp { get; set; }
[JsonProperty("trace_id")]
public string TraceId { get; set; }
[JsonProperty("correlation_id")]
public string CorrelationId { get; set; }
[JsonProperty("error_uri")]
public string ErrorUri { get; set; }
}
/// <summary>
/// Usato per ottenere il token di autenticazione
/// </summary>
public class AuthResponse : ErrorResponse
{
[JsonProperty("token_type")] //Baerer
public string TokenType { get; set; }
[JsonProperty("expires_in")]
public long ExpiresIn { get; set; }
[JsonProperty("not_before")]
public long NotBefore { get; set; }
[JsonProperty("expires_on")]
public long ExpiresOn { get; set; }
[JsonProperty("resource")]
public string Resource { get; set; }
[JsonProperty("access_token")]
public string AccessToken { get; set; }
}
Accesso a SharePoint
Tramite il metodo GetHttpClient ottengo il token di autenticazione e creo l'oggetto HttpClient che userò nelle successive chiamate:C#
using System.Net.Http;
using System.Net.Http.Headers;
private async Task<HttpClient> GetHttpClient(string accessToken = null)
{
if (accessToken == null)
accessToken = (await GetAccessToken()).AccessToken;
// setup http client.
HttpClient httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(300)
};
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
httpClient.DefaultRequestHeaders.Add("client-request-id", _settings.Azure.PublisherIdentifier);
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(CONTENT_TYPE_JSON));
return httpClient;
}
Elencare gli item di una lista
Ad esempio per accedere agli items di una lista posso usare questa funzione GetListItemsC#
public async Task<SPListItemsResponse> GetListItems(string webRelativeUrl, string listTitle, string[] select = null, string[] orderBy = null, string filter = null, int take = 0, int skip = 0)
{
try
{
SPListItemsResponse result = null;
string url = GetApiUrlWithQueryStringParameters(webRelativeUrl, $"/web/lists/getbytitle('{listTitle}')/items", select, orderBy, filter, take, skip);
using (HttpClient httpClient = await GetHttpClient())
{
using (var postResponse = await httpClient.GetAsync(url))
{
string serverResponse = await postResponse.Content.ReadAsStringAsync();
log.LogTrace(serverResponse);
result = JsonConvert.DeserializeObject<SPListItemsResponse>(serverResponse);
if (result.HasError)
{
log.LogError($"GetListItems:Error:{result.GetError()}");
}
}
}
return result;
}
catch (Exception ex)
{
#log.LogError(ex, "GetListItems");
throw;
}
}
Metodi e classi di base
Per funzionare richiede il seguente metodo GetApiUrlWithQueryStringParametersC#
/// <summary>
/// </summary>
/// <param name="webRelativeUrl">url relativa del sito web</param>
/// <param name="listTitle">titolo della lista</param>
/// <param name="apiRelativeUrl">chiamata api, ad es.: "/web/lists/getbytitle('listTitle')/items"</param>
/// <param name="select">nomi interni da restituire</param>
/// <param name="orderBy">nomi interni con cui filtrare</param>
/// <param name="filter">query di filtro</param>
/// <param name="take">numero di record da ritornare</param>
/// <param name="skip">numero di record da saltare (skip)</param>
/// <returns></returns>
private string GetApiUrlWithQueryStringParameters(string webRelativeUrl, string apiRelativeUrl, string[] select = null, string[] orderBy = null, string filter = null, int take = 0, int skip = 0)
{
if (apiRelativeUrl.StartsWith("/_api/", StringComparison.InvariantCultureIgnoreCase) == false)
{
// mi assicuro che ci sia il prefisso
apiRelativeUrl = "/_api" + apiRelativeUrl.TrimEnd('/');
}
string url = $"https://{CFG_TENANTNAME }.{SHAREPOINT_DOMAIN}/{webRelativeUrl.TrimEnd('/').TrimStart('/')}/{apiRelativeUrl.TrimStart('/')}";
string qs = "";
if (select != null && select.Length > 0)
{
qs += (qs.Length == 0 ? "?" : "&") + "$select=" + string.Join(',', select);
}
if (orderBy != null && orderBy.Length > 0)
{
qs += (qs.Length == 0 ? "?" : "&") + "$orderBy=" + string.Join(',', orderBy);
}
if (string.IsNullOrWhiteSpace(filter) == false)
{
qs += (qs.Length == 0 ? "?" : "&") + "$filter=" + filter;
}
if (take != 0)
{
qs += (qs.Length == 0 ? "?" : "&") + "$take=" + take.ToString();
}
if (skip != 0)
{
qs += (qs.Length == 0 ? "?" : "&") + "$skip=" + skip.ToString();
}
url += qs;
#log.LogTrace($"GetApiUrlWithQueryStringParameters url: {url}");
return url;
}
C#
public class SPErrorMessage
{
[JsonProperty("lang")]
public string Language { get; set; }
[JsonProperty("value")]
public string Value { get; set; }
}
/// <summary>
/// errore base della risposta
/// </summary>
public class SPError
{
[JsonProperty("code")]
public string Code { get; set; }
[JsonProperty("message")]
public SPErrorMessage Message { get; set; }
}
public class SPErrorBase
{
public bool HasError
{
get
{
return Error != null && Error.Message != null;
}
set { }
}
[JsonProperty("odata.error")]
public SPError Error { get; set; }
public string GetError()
{
if (Error == null)
{
return "error undefined";
}
return $"{Error.Code}, message: {Error.Message.Value}";
}
}
public class SPListItemsResponse : SPErrorBase
{
[JsonProperty("odata.metadata")]
public string ODataMetadata { get; set; }
[JsonProperty("value")]
public JArray Value { get; set; }
}
public class SPBaseItem : SPErrorBase
{
[JsonProperty("odata.id")]
public string ODataID { get; set; }
[JsonProperty("odata.etag")]
public string Etag { get; set; }
[JsonProperty("odata.type")]
public string ODataType { get; set; }
[JsonProperty("Id")]
public int ID { get; set; }
public string Title { get; set; }
}
public class SPListResponse : SPErrorBase
{
[JsonProperty("odata.metadata")]
public string ODataMetadata { get; set; }
[JsonProperty("odata.type")]
public string ODataType { get; set; }
[JsonProperty("odata.id")]
public string ODataID { get; set; }
//[JsonProperty("odata.editLink")]
//public string ODataEditLink { get; set; }
[JsonProperty("ListItemEntityTypeFullName")]
public string ListItemEntityTypeFullName { get; set; }
//TODO: da completare con le altre proprietà quando servono
}
public class SPUpdateItemMetadata
{
[JsonProperty("type")]
public string Type { get; set; }
}
public class SPUpdateItemValue : Dictionary<string, object>
{
public SPUpdateItemValue(string listTitle, string type = null) : base()
{
Metadata = new SPUpdateItemMetadata
{
Type = type != null ? type : $"SP.Data.{listTitle}ListItem" // Nota: sempra funzionare anche in caso di type null
};
}
[JsonProperty("__metadata")]
public SPUpdateItemMetadata Metadata { get; set; }
}