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.

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"]}");
    }
}
ad esempio con queste due righe in input
Dati di esempioDati di esempio
il risultato è questo
Watch BWatch B
dove 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
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 ritornato
C#: 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"]}");
    }
}
Watch CWatch C
In questo caso, in tutte le righe, indipendentemente dal contenuto, il campo viene sempre interpretato correttamente come stringa.

Codice completo

Codice completo dell'esempio
PowerShell
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"
      }
    }
  }
}
Potrebbe interessarti anche: