Problema con PnPCore e LoadItemsByCamlQueryAsync (SharePoint)
Nella libreria PnPcore per SharePoint Online, Il metodo LoadItemsByCamlQueryAsync ha un bug quando si leggono i campi di testo.
In pratica se il campo di testo che si sta leggendo, contiene una stringa in formato data, come ad esempio 20/12/2023, viene ritornato un oggetto DateTime anziché una stringa.
Ovvero esegue un parsing custom del dato contenuto per determinare il tipo, aziché usare la proprietà TypeAsString dell'oggetto Field.
ad esempio con queste due righe in inputil risultato è questodove si vede chiaramente che la prima riga (Id=1) viene ritornata come stringa e la seconda (Id=2) come DateTime.
Anche l'accesso diretto tramite la collection Items presenta lo stesso problema
In pratica se il campo di testo che si sta leggendo, contiene una stringa in formato data, come ad esempio 20/12/2023, viene ritornato un oggetto DateTime anziché una stringa.
Ovvero esegue un parsing custom del dato contenuto per determinare il tipo, aziché usare la proprietà TypeAsString dell'oggetto Field.
LoadItemsByCamlQueryAsync
Questo esempio in C# permette di riprodurre il problema:C#: LoadItemsByCamlQueryAsync
private static async Task EsempioB(IList list)
{
string viewXml = @"<View>
<ViewFields><FieldRef Name='Title' /><FieldRef Name='CampoDiTesto' /></ViewFields>
<OrderBy Override='TRUE'><FieldRef Name= 'ID' Ascending= 'FALSE' /></OrderBy>
</View>";
await list.LoadItemsByCamlQueryAsync(new CamlQueryOptions()
{
ViewXml = viewXml,
DatesInUtc = true
});
foreach (var item in list.Items.AsRequested())
{
Console.WriteLine($"B - {item.Id} - {item["Title"]} - {item["CampoDiTesto"]}");
}
}
Anche l'accesso diretto tramite la collection Items presenta lo stesso problema
C#: Items
private static async Task EsempioA(IList list)
{
foreach (var item in list.Items)
{
Console.WriteLine($"A - {item.Id} - {item["Title"]} - {item["CampoDiTesto"]}");
}
}
Ovviamente questo comportamento non è il desiderata, diventa molto difficile gestire questa situazione.
LoadListDataAsStreamAsync
Per ovviare al problema si può usare il metodo LoadListDataAsStreamAsync che non esegue nessuna trasformazione sul dato ritornatoC#: LoadListDataAsStreamAsync
private static async Task EsempioC(IList list)
{
string viewXml = @"<View>
<ViewFields><FieldRef Name='Title' /><FieldRef Name='CampoDiTesto' /></ViewFields>
<OrderBy Override='TRUE'><FieldRef Name= 'ID' Ascending= 'FALSE' /></OrderBy>
</View>";
var output = await list.LoadListDataAsStreamAsync(new RenderListDataOptions()
{
ViewXml = viewXml,
DatesInUtc = true,
RenderOptions = RenderListDataOptionsFlags.ListData
});
foreach (var item in list.Items.AsRequested())
{
Console.WriteLine($"C - {item.Id} - {item["Title"]} - {item["CampoDiTesto"]}");
}
}
In questo caso, in tutte le righe, indipendentemente dal contenuto, il campo viene sempre interpretato correttamente come stringa.
Codice completo
Codice completo dell'esempioPowerShell
dotnet add package PnP.Core.Auth --version 1.11.0
C#: Program.cs
using ConsoleAppNet8.Service;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PnP.Core.Auth.Services.Builder.Configuration;
using PnP.Core.Services.Builder.Configuration;
using System.Security.Cryptography.X509Certificates;
var host = Host.CreateDefaultBuilder()
// Configure logging
.ConfigureServices((hostingContext, services) =>
{
// Add the PnP Core SDK library services
services.AddPnPCore(options =>
{
// https://pnp.github.io/pnpcore/using-the-sdk/readme.html
options.PnPContext.GraphFirst = false;
options.PnPContext.GraphCanUseBeta = false;
options.PnPContext.GraphAlwaysUseBeta = false;
});
// Add the PnP Core SDK library services configuration from the appsettings.json file
services.Configure<PnPCoreOptions>(hostingContext.Configuration.GetSection("PnPCore"));
// Add the PnP Core SDK Authentication Providers
services.AddPnPCoreAuthentication();
services.AddPnPCoreAuthentication(
options =>
{
// Configure an Authentication Provider relying on Windows Credential Manager
options.Credentials.Configurations.Add("x509certificate",
new PnPCoreAuthenticationCredentialConfigurationOptions
{
ClientId = "511a....61",
TenantId = "b32.....dca9",
X509Certificate = new PnPCoreAuthenticationX509CertificateOptions
{
StoreName = StoreName.My,
StoreLocation = StoreLocation.CurrentUser,
Thumbprint = "0CC.....2C14"
}
});
// Configure the default authentication provider
options.Credentials.DefaultConfiguration = "x509certificate";
// Map the site defined in AddPnPCore with the
// Authentication Provider configured in this action
options.Sites.Add("SiteToWorkWith",
new PnPCoreAuthenticationSiteOptions
{
AuthenticationProviderName = "x509certificate"
});
});
// Add the PnP Core SDK Authentication Providers configuration from the appsettings.json file
services.Configure<PnPCoreAuthenticationOptions>(hostingContext.Configuration.GetSection("PnPCore"));
services.AddSingleton<SPService>();
})
// Let the builder know we're running in a console
.UseConsoleLifetime()
// Add services to the container
.Build();
// Start console host
await host.StartAsync();
using var scope = host.Services.CreateScope();
//var services = scope.ServiceProvider;
try
{
await scope.ServiceProvider.GetRequiredService<SPService>().Run();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
host.Dispose();
}
C#: SPService.cs
using PnP.Core.Model.SharePoint;
using PnP.Core.QueryModel;
using PnP.Core.Services;
using IList = PnP.Core.Model.SharePoint.IList;
namespace ConsoleAppNet8.Service;
internal class SPService(IPnPContextFactory contextFactory)
{
private readonly IPnPContextFactory _contextFactory = contextFactory;
private async Task<PnPContext> GetContext()
{
return await _contextFactory.CreateAsync("SiteToWorkWith");
}
public async Task Run()
{
using var context = await GetContext();
await context.Web.LoadAsync(p => p.Title);
Console.WriteLine($"The title of the web is {context.Web.Title}");
IList list = await context.Web.Lists.GetByTitleAsync("ConsoleAppNet8",
p => p.Title,
p => p.Items
);
await EsempioA(list);
await EsempioB(list);
await EsempioC(list);
}
private static async Task EsempioA(IList list)
{
foreach (var item in list.Items)
{
Console.WriteLine($"A - {item.Id} - {item["Title"]} - {item["CampoDiTesto"]}");
}
}
private static async Task EsempioB(IList list)
{
string viewXml = @"<View>
<ViewFields>
<FieldRef Name='Title' />
<FieldRef Name='CampoDiTesto' />
</ViewFields>
<OrderBy Override='TRUE'><FieldRef Name= 'ID' Ascending= 'FALSE' /></OrderBy>
</View>";
// Execute the query
await list.LoadItemsByCamlQueryAsync(new CamlQueryOptions()
{
ViewXml = viewXml,
DatesInUtc = true
});
// Iterate over the retrieved list items
foreach (var item in list.Items.AsRequested())
{
Console.WriteLine($"B - {item.Id} - {item["Title"]} - {item["CampoDiTesto"]}");
}
}
private static async Task EsempioC(IList list)
{
string viewXml = @"<View>
<ViewFields>
<FieldRef Name='Title' />
<FieldRef Name='CampoDiTesto' />
</ViewFields>
<OrderBy Override='TRUE'><FieldRef Name= 'ID' Ascending= 'FALSE' /></OrderBy>
</View>";
// Execute the query
var output = await list.LoadListDataAsStreamAsync(new RenderListDataOptions()
{
ViewXml = viewXml,
DatesInUtc = true,
RenderOptions = RenderListDataOptionsFlags.ListData
});
// Iterate over the retrieved list items
foreach (var item in list.Items.AsRequested())
{
Console.WriteLine($"C - {item.Id} - {item["Title"]} - {item["CampoDiTesto"]}");
}
}
}
JSON: appsettings.json
{
"PnPCore": {
"DisableTelemetry": "false",
"HttpRequests": {
"UserAgent": "ISV|Sgart.IT|ConsoleAppNet8",
"Timeout": "100",
"SharePointRest": {
"UseRetryAfterHeader": "false",
"MaxRetries": "10",
"DelayInSeconds": "3",
"UseIncrementalDelay": "true"
},
"MicrosoftGraph": {
"UseRetryAfterHeader": "true",
"MaxRetries": "10",
"DelayInSeconds": "3",
"UseIncrementalDelay": "true"
}
},
"Sites": {
"SiteToWorkWith": {
"SiteUrl": "https://XXXXX.sharepoint.com/",
"AuthenticationProviderName": "x509certificate"
}
}
}
}