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#.

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 AuthResponse
C#
/// <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; }
}
Una volta ottenuto il bearer token posso accedere a SharePoint.

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 GetListItems
C#
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 GetApiUrlWithQueryStringParameters
C#
/// <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;
}
mentre per gestire la risposta, dovranno essere create queste classi: SPErrorMessage, SPError, SPErrorBase, SPListItemsResponse, SPBaseItem, SPListResponse, SPUpdateItemMetadata e SPUpdateItemValue
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; }
}
Potrebbe interessarti anche: