
Elenco dei file in una DocLib con SharePoint PnP
Normalmente per leggere i file contenuti in una folder SharePoint Online con la libreria PnP.Core in .NET 10 si usa questo codice:
Il codice funziona perfettamente ma risulta molto inefficiente dal punto di vista del numero di chiamate che deve fare per avere l'elenco dei files.
Questo perchè deve fare 2 chiamate REST, una per recuperare la folder e una per l'elenco dei files.
Entrando nel dettaglio della libreria, si vede che, dietro le quinte, effettua queste 2 chiamate alle API:
Si può raggiungere lo scopo componendo manualmente la URL della chiamata REST alle API:
A questo punto il codice C# diventa:
e
Oltre ad una classe helper statica per la deserializazione delle risposta JSON
L'inizializzazione della console application:
infine l'handler per intercettare e stampare le chiamate HTTP:
Onprem si usava il modello a oggetti di SharePoint perchè tipicamente si lavorava sulle macchine della farm, quindi tutte operazioni in memoria e/o su SQL Server.
Online si deve tenere conto del tempo necessario a costruire la chiamata https verso SharePoint Online, inviare le richiesta e attendere la risposta.
Pur essendo tempi che sembrano trascurabili, sono N volte più lenti dell'approccio onprem.
Risparmiare anche una sola chiamata, su centinaia o migliaia di richieste può fare le differenza, in base alla situazione, di secondi / minuti / ore. Soprattutto se si incorre anche nel throttling.
Questo vuol dire che con SharePoint Online, deve esserci sempre una maggiore attenzione alle prestazione e all'ottimizzazione del codice/chiamate.
C#: Esempio classico
using var pnpContext = await pnpContextFactory.CreateAsync(new Uri("https://mioSito.sharepoint.com"));
string folderUrl = "/Shared documents/Flow";
// Recupera la cartella
IFolder folder = await pnpContext.Web.GetFolderByServerRelativeUrlAsync(
folderUrl,
p => p.Files.QueryProperties(f => f.ServerRelativeUrl));
// Itera sui file
foreach (IFile file in folder.Files)
{
Console.WriteLine($"URL File: {file.ServerRelativeUrl}");
}
Questo perchè deve fare 2 chiamate REST, una per recuperare la folder e una per l'elenco dei files.
Entrando nel dettaglio della libreria, si vede che, dietro le quinte, effettua queste 2 chiamate alle API:
Text: Chiamate REST
GET https://mioSito.sharepoint.com/_api/Web/getFolderByServerRelativePath(decodedUrl=@u)?@u='%2FShared documents%2FFlow'&$select=UniqueId%2cFiles%2fServerRelativeUrl%2cFiles%2fUniqueId&$expand=Files
GET https://mioSito.sharepoint.com/_api/Web/getFolderById('52d377af-5bfd-4458-9abf-69df64bc0544')/Files?$top=100
Nel caso di molte chiamate consecutive, si rischia di raggiungere subito il limiti della libreria e ricevere un errore di throttling (HTT 429, limitazione della velocità di trasmissione), oltre ad aumentare i tempi di esecuzione.
Alternativa
Un miglioramento a questo codice C# consiste fare una sola chiamata REST alle API, dimezzando quindi le chiamate necessarie.Si può raggiungere lo scopo componendo manualmente la URL della chiamata REST alle API:
Text
GET https://mioSito.sharepoint.com/_api/Web/getFolderByServerRelativePath(decodedUrl='%2FShared documents%2FFlow')/Files?$select=ServerRelativeUrl
C#
using var pnpContext = await pnpContextFactory.CreateAsync(new Uri("https://mioSito.sharepoint.com"));
string folderUrl = "/Shared documents/Flow";
// Costruisco l'endpoint con l'encoding della url
string restUrl = $"_api/Web/getFolderByServerRelativePath(decodedUrl='{Uri.EscapeDataString(folderUrl)}')/Files?$select=ServerRelativeUrl";
// Rendo la risposta la più breve possibile usando nometadata
Dictionary<string, string> headers = new() { { "Accept", "application/json;odata=nometadata" } };
// creo la richiesta
var apiRequest = new ApiRequest(HttpMethod.Get, ApiRequestType.SPORest, restUrl, string.Empty, headers);
// Invia l'unica chiamata HTTP sfruttando il contesto PnP
ApiRequestResponse response = await pnpContext.Web.ExecuteRequestAsync(apiRequest);
IEnumerable<FileUrl> files = JsonHelper.DeserializeSPResponseValue<FileUrl>(response.Response);
foreach (var file in files)
{
Console.WriteLine($"URL File: {file.ServerRelativeUrl}");
}
Classi di supporto
Per semplificare la gestione della risposta ho creato dei DTO di tipo recordC#: SPResponseValue.cs
internal record SPResponseValue<T>
{
[JsonPropertyName("value")]
public IEnumerable<T>? Value { get; set; }
}
C#: FileUrl.cs
internal record FileUrl
{
public required string ServerRelativeUrl { get; set; }
}
C#: JsonHelper.cs
internal static class JsonHelper
{
public static IEnumerable<T> DeserializeSPResponseValue<T>(string json)
{
var response = System.Text.Json.JsonSerializer.Deserialize<SPResponseValue<T>>(json);
return response?.Value ?? [];
}
}
C#: Program.cs
# <PackageReference Include="PnP.Core.Auth" Version="1.18.0" />
var host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
// Registra l'handler per stampare le chiamate HTTP
services.AddTransient<PnPLoggingHandler>();
var pnpCoreSection = context.Configuration.GetSection("PnPCore");
var appOptions = context.Configuration.GetSection(AppOptions.SECTION_NAME).Get<AppOptions>();
services.Configure<PnPCoreOptions>(pnpCoreSection);
services.AddPnPCore(
options => { },
httpClientBuilder =>
{
httpClientBuilder.AddHttpMessageHandler<PnPLoggingHandler>();
}
);
services.AddPnPCoreAuthentication(options =>
{
// Load the certificate that will be used to authenticate
var certificate = X509CertificateLoader.LoadPkcs12FromFile(appOptions.SharePoint.CertificatePath, appOptions.SharePoint.CertificatePassword);
// Configure certificate based auth
options.Credentials.Configurations.Add("CertAuth", new PnPCoreAuthenticationCredentialConfigurationOptions
{
ClientId = appOptions.SharePoint.ClientId,
TenantId = appOptions.SharePoint.TenantId,
X509Certificate = new PnPCoreAuthenticationX509CertificateOptions
{
Certificate = certificate
}
});
// Configure the default authentication provider
options.Credentials.DefaultConfiguration = "CertAuth";
});
services.AddHttpClient("SharePointRestClient")
.AddHttpMessageHandler<PnPLoggingHandler>();
services.AddHttpClient("MicrosoftGraphClient")
.AddHttpMessageHandler<PnPLoggingHandler>();
})
.Build();
C#: PnPLoggingHandler .cs
public class PnPLoggingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine($"PNPDEBUG: \n[→ HTTP REQ] {request.Method} {request.RequestUri}");
if (request.Content != null)
{
var body = await request.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"PNPDEBUG: [Payload] {body}");
}
var stopwatch = Stopwatch.StartNew();
var response = await base.SendAsync(request, cancellationToken);
stopwatch.Stop();
Console.WriteLine($"PNPDEBUG: [← HTTP RES] {(int)response.StatusCode} {response.ReasonPhrase} ({stopwatch.ElapsedMilliseconds}ms)");
return response;
}
}
Morale
Lavorare con SharePoint Online è molto diverso in termini di prestazioni rispetto a lavorare con uno SharePoint 2016 Onprem.Onprem si usava il modello a oggetti di SharePoint perchè tipicamente si lavorava sulle macchine della farm, quindi tutte operazioni in memoria e/o su SQL Server.
Online si deve tenere conto del tempo necessario a costruire la chiamata https verso SharePoint Online, inviare le richiesta e attendere la risposta.
Pur essendo tempi che sembrano trascurabili, sono N volte più lenti dell'approccio onprem.
Risparmiare anche una sola chiamata, su centinaia o migliaia di richieste può fare le differenza, in base alla situazione, di secondi / minuti / ore. Soprattutto se si incorre anche nel throttling.
Questo vuol dire che con SharePoint Online, deve esserci sempre una maggiore attenzione alle prestazione e all'ottimizzazione del codice/chiamate.