Precedentemente avevo mostrato come effettuare chiamate batch con JavaScript e creare item in batch con Power Automate

In questo post mostro come realizzare la stessa chiamata, verso SharePoint Online, in C# e .NET 4.8.

Allo stato attuale non esistono librerie per fare chiamate batch con .NET 4.8, quindi ho realizzato questa classe, per ora l'ho testata solo con gli inserimenti:

C#: SPBatchManager.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.SharePoint.Client;
using Newtonsoft.Json;
using PnP.Core.Services;

namespace ConsoleAppNet48.Data
{
    /// <summary>
    /// https://github.com/pnp/pnpframework
    /// https://learn.microsoft.com/en-us/sharepoint/dev/sp-add-ins/make-batch-requests-with-the-rest-apis
    /// https://www.andrewconnell.com/blog/part-2-sharepoint-rest-api-batching-exploring-batch-requests-responses-and-changesets/
    /// </summary>
    public class SPBatchManager
    {
        private readonly string _accessToken = null;
        private readonly string _siteUrl;

        const string HEADER_ACCEPT_VALUE = "application/json;odata=nometadata";

        const string API_GET_WEB = "/_api/web?$select=Id,Title,Created";
        const string API_GET_LISTS = "/_api/web/lists?$select=Id,Title,Created,ItemCount";
        const string API_BATCH = "/_api/$batch";

        public SPBatchManager(string siteUrl, string user, string password)
        {
            _siteUrl = siteUrl.TrimEnd('/');

            _accessToken = GetAccessToken(siteUrl, user, password);
        }

        #region Authentication/Token 
        private string GetAccessToken(string siteUrl, string user, string password)
        {
            SecureString securePassword = ToSecureString(password);
            PnP.Framework.AuthenticationManager authManager = new PnP.Framework.AuthenticationManager(user, securePassword);
            return authManager.GetAccessToken(siteUrl);
        }

        private SecureString ToSecureString(string plainString)
        {
            if (plainString == null) return null;

            SecureString secureString = new SecureString();
            foreach (char c in plainString.ToCharArray())
            {
                secureString.AppendChar(c);
            }
            return secureString;
        }

        #endregion



        #region Http base methods

        private HttpClient GetHttpClient(bool includeAccept = true)
        {
            HttpClient client = new HttpClient();
            if (includeAccept == true)
            {
                client.DefaultRequestHeaders.Add("accept", HEADER_ACCEPT_VALUE);
            }
            client.DefaultRequestHeaders.Add("authorization", "Bearer " + _accessToken);
            return client;
        }
        private async Task<string> GetAsync(string relativeUrl)
        {
            try
            {
                using (HttpClient client = GetHttpClient())
                {
                    return await client.GetStringAsync(_siteUrl + relativeUrl);
                }
            }
            catch (Exception ex)
            {
                throw;
            }

        }

        private string GetMulipartAction(System.Net.Http.HttpMethod method, string url, object item)
        {
            StringBuilder sb = new StringBuilder(500);

            sb.AppendFormat("{0} {1} HTTP/1.1", method, url);
            sb.AppendLine();
            sb.AppendLine("Content-Type: application/json;odata=nometadata");
            sb.AppendLine("Accept: application/json;odata=nometadata");
            sb.AppendLine();
            if (item != null)
            {
                sb.AppendLine(JsonConvert.SerializeObject(item));
            }

            return sb.ToString();
        }

        private const string BOUNDARY_BATCH = "batch_19676457-165F-46AF-9A8C-8202812CCEEE_sgart-it";
        private const string BOUNDARY_CHANGESET = "changeset_C5946BFE-6A35-462D-9E3D-0A7CC4D9A1E3_sgart-it";

        private async Task PostMultipartAsync<T>(string relativeUrl, List<UpdateBatch<T>> batchItems) where T : IBatchId
        {
            try
            {
                // create changesets
                StringBuilder sbChangesets = new StringBuilder(batchItems.Count * 100);
                int index = 0;
                batchItems.ForEach(batch =>
                {
                    sbChangesets.AppendLine("--" + BOUNDARY_CHANGESET);
                    sbChangesets.AppendLine("Content-Type: application/http");
                    sbChangesets.AppendLine("Content-Transfer-Encoding: binary");
                    sbChangesets.AppendLine($"Content-ID: {++index}");  // crea delle variabili $1, S2, ecc. che conterrano l'id, da usare per altre rischieste 
                    sbChangesets.AppendLine();

                    string batchAction = GetMulipartAction(batch.Method, batch.Url, batch.Item);

                    sbChangesets.AppendLine(batchAction);
                });
                int changesetsLength = sbChangesets.Length;

                // create batch
                StringBuilder sbBatch = new StringBuilder(changesetsLength + 350);
                sbBatch.AppendLine("--" + BOUNDARY_BATCH);
                sbBatch.AppendLine("Content-Type: multipart/mixed;boundary=" + BOUNDARY_CHANGESET);
                sbBatch.AppendLine("Content-Lenght: " + changesetsLength);
                sbBatch.AppendLine("Content-Transfer-Encoding: binary");
                sbBatch.AppendLine();
                sbBatch.Append(sbChangesets);
                sbBatch.AppendLine("--" + BOUNDARY_CHANGESET + "--");
                sbBatch.AppendLine("--" + BOUNDARY_BATCH + "--");

                using (HttpClient client = GetHttpClient())
                {
                    using (var content = new StringContent(sbBatch.ToString()))
                    {
                        content.Headers.Remove("Content-Type");
                        content.Headers.TryAddWithoutValidation("Content-Type", "multipart/mixed;boundary=" + BOUNDARY_BATCH);

                        using (HttpResponseMessage response = await client.PostAsync(_siteUrl + relativeUrl, content))
                        {
                            //response.EnsureSuccessStatusCode();
                            var result = await response.Content.ReadAsStringAsync();

                            if (response.IsSuccessStatusCode)
                            {
                                string boundaryResponse = result.Substring(0, result.IndexOf(Environment.NewLine));

                                //le risposte dei cnagesets sono nell'ordne in cui sono state inviate
                                string[] blocks = result.Split(new[] { boundaryResponse }, StringSplitOptions.RemoveEmptyEntries);

                                for (int i = 0; i < blocks.Length; i++)
                                {
                                    string r = blocks[i].Trim();
                                    if (r == "--")
                                    {
                                        break;
                                    }
                                    // risposta del batch attuale
                                    var batch = batchItems[i];

                                    // trovo lo stato HTTP
                                    Regex reHttp = new Regex(@"^HTTP/1\.1 (?<status>\d+) .+$", RegexOptions.Multiline | RegexOptions.ExplicitCapture | RegexOptions.Compiled);
                                    var matchHttp = reHttp.Match(r);
                                    if (matchHttp.Success)
                                    {
                                        int.TryParse(matchHttp.Groups["status"].Value, out int status);

                                        batch.HTTPStatus = status;
                                        batch.Success = false;

                                        string objectResponse = r.Split('\n').Last();


                                        if (status >= 200 && status <= 299)
                                        {
                                            if (status == 201)
                                            {
                                                BatchId obj = JsonConvert.DeserializeObject<BatchId>(objectResponse);
                                                batch.Item.Id = obj.Id;
                                            }
                                            batch.Success = true;
                                        }
                                        else
                                        {
                                            var error = JsonConvert.DeserializeObject<SPError>(objectResponse);
                                            batch.Error = error.OdataError.Code + " - " + error.OdataError.Message.Value;
                                        }
                                    }
                                }
                            }
                            else
                            {
                                var error = JsonConvert.DeserializeObject<SPError>(result);
                                throw new Exception(error.OdataError.Code + " - " + error.OdataError.Message.Value);
                            }
                        }

                    }
                }
            }
            catch (Exception ex)
            {
                throw;
            }

        }

        #endregion


        #region Relative list url

        /// <summary>
        /// Ritorna la url assoluta della lista
        /// es.:
        /// listName = TestBulkInsert => https://tenantName.sharepoint.com/sites/test/_api/web/lists/GetByTitle('TestBulkInsert')/items
        /// listName = dd4450ad-4fcc-440a-b188-493189631ddc => https://tenantName.sharepoint.com/sites/test/_api/web/lists(guid'dd4450ad-4fcc-440a-b188-493189631ddc')/items
        /// listName = /Lists/TestBulkInsert => https://tenantName.sharepoint.com/sites/test/_api/web/GetList('/sites/test/Lists/TestBulkInsert')/items
        /// listName = /NomeDocLib => https://tenantName.sharepoint.com/sites/test/_api/web/GetList('/sites/test/NomeDocLib')/items
        /// </summary>
        /// <param name="listName">Titolo della lista, oppure guid, oppure path relativo</param>
        /// <returns></returns>
        public string GetListUrl(string listName)
        {
            string urlPart;
            if (listName.StartsWith("/"))
            {
                urlPart = $"GetList('{new Uri(_siteUrl).AbsolutePath.TrimEnd('/')}{listName}')";
            }
            else
            {
                //{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
                bool isGuid = listName.Length == 38 && listName.StartsWith("{") && listName.EndsWith("}");
                if (isGuid)
                {
                    urlPart = $"lists(guid'{listName.TrimStart('{').TrimEnd('}')}')";
                }
                else
                {
                    urlPart = $"lists/GetByTitle('{listName}')";
                }
            }

            return $"{_siteUrl}/_api/web/{urlPart}";
        }

        public string GetListItemUrl(string listName, int id = 0)
        {
            return GetListUrl(listName) + "/items" + (id == 0 ? "" : $"({id})");
        }

        #endregion

        #region batch
        public async Task ExecuteBatch<T>(List<UpdateBatch<T>> batches) where T : IBatchId
        {
            await PostMultipartAsync(API_BATCH, batches);
        }

        #endregion
}

Per gestire l'autenticazione e l'ottenimento del Bearer token JWT necessario per le chiamate alla API REST, viene usato il pacchetto Nuget PnP.Framework.

Un po' di teoria

Per fare le chiamate REST HTTP in batch va usato l'endpoint

Text: Batch

https://tenantName.sharepoint.com/_api/$batch
indicando nell'header
  • Content-Type=multipart/mixed;boundary=xxxxxxxxxx
  • Accept=application/json;odata=nometadata

Chiamata

Va poi eseguita una chiamata HTTP in POST con formato multipart/mixed simile a questo
--batch_19676457-165F-46AF-9A8C-8202812CCEEE_sgart-it
Content-Type: multipart/mixed;boundary=changeset_C5946BFE-6A35-462D-9E3D-0A7CC4D9A1E3_sgart-it
Content-Lenght: 2050
Content-Transfer-Encoding: binary

--changeset_C5946BFE-6A35-462D-9E3D-0A7CC4D9A1E3_sgart-it
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 1

POST https://sgart.sharepoint.com/_api/web/lists/getbytitle('TestBulkInsert')/items HTTP/1.1
Content-Type: application/json;odata=nometadata
Accept: application/json;odata=nometadata

{"Id":0,"Title":"Prova 13/11/2022 11:18:48 - 1","CAP":"1","ProvinciaId":66}

--changeset_C5946BFE-6A35-462D-9E3D-0A7CC4D9A1E3_sgart-it
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 2

POST https://sgart.sharepoint.com/_api/web/lists/getbytitle('TestBulkInsert')/items HTTP/1.1
Content-Type: application/json;odata=nometadata
Accept: application/json;odata=nometadata

{"Id":0,"Title":"Prova 13/11/2022 11:18:48 - 2","CAP":"2","ProvinciaId":81}

--changeset_C5946BFE-6A35-462D-9E3D-0A7CC4D9A1E3_sgart-it
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 3

POST https://sgart.sharepoint.com/_api/web/lists/getbytitle('TestBulkInsert')/items HTTP/1.1
Content-Type: application/json;odata=nometadata
Accept: application/json;odata=nometadata

{"Id":0,"Title":"Prova 13/11/2022 11:18:48 - 3","CAP":"3","ProvinciaId":80}

--changeset_C5946BFE-6A35-462D-9E3D-0A7CC4D9A1E3_sgart-it
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 4

POST https://sgart.sharepoint.com/_api/web/lists/getbytitle('TestBulkInsert')/items HTTP/1.1
Content-Type: application/json;odata=nometadata
Accept: application/json;odata=nometadata

{"Id":0,"Title":"Prova 13/11/2022 11:18:48 - 4","CAP":"4","ProvinciaId":27}

--changeset_C5946BFE-6A35-462D-9E3D-0A7CC4D9A1E3_sgart-it
Content-Type: application/http
Content-Transfer-Encoding: binary
Content-ID: 5

POST https://sgart.sharepoint.com/_api/web/lists/getbytitle('TestBulkInsert')/items HTTP/1.1
Content-Type: application/json;odata=nometadata
Accept: application/json;odata=nometadata

{"Id":0,"Title":"Prova 13/11/2022 11:18:48 - 5","CAP":"5","ProvinciaId":80}

--changeset_C5946BFE-6A35-462D-9E3D-0A7CC4D9A1E3_sgart-it--
--batch_19676457-165F-46AF-9A8C-8202812CCEEE_sgart-it--
in questo caso si tratta di un inserendo di 5 item contemporaneamente (batch).

Risposta

Il risultato della chiamata è simile a questo
--batchresponse_c28f9daf-ad92-4cff-bc94-cc802c1e2142
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 201 Created
CONTENT-TYPE: application/json;odata=nometadata;streaming=true;charset=utf-8
ETAG: "0ce8702e-9865-4535-974d-4d0399634aa2,1"
LOCATION: https://sgart.sharepoint.com/_api/Web/Lists(guid'dd4450ad-4fcc-440a-b188-493189631ddc')/Items(741)

{"FileSystemObjectType":0,"Id":741,"ServerRedirectedEmbedUri":null,"ServerRedirectedEmbedUrl":"","ID":741,"ContentTypeId":"0x01008BBC5180BF5265449EBD7959AA731CE500566988404409E0468CDB9D44849FD5DA","Title":"Prova 13/11/2022 11:18:48 - 1","Modified":"2022-11-13T10:20:39Z","Created":"2022-11-13T10:20:39Z","AuthorId":11,"EditorId":11,"OData__UIVersionString":"1.0","Attachments":false,"GUID":"feeb7f35-b147-4bc3-a386-50565c1668fa","ComplianceAssetId":null,"CAP":"1","ProvinciaId":66}
--batchresponse_c28f9daf-ad92-4cff-bc94-cc802c1e2142
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 201 Created
CONTENT-TYPE: application/json;odata=nometadata;streaming=true;charset=utf-8
ETAG: "5fd7d497-6085-411e-9eab-a3b9ef569cd6,1"
LOCATION: https://sgart.sharepoint.com/_api/Web/Lists(guid'dd4450ad-4fcc-440a-b188-493189631ddc')/Items(742)

{"FileSystemObjectType":0,"Id":742,"ServerRedirectedEmbedUri":null,"ServerRedirectedEmbedUrl":"","ID":742,"ContentTypeId":"0x01008BBC5180BF5265449EBD7959AA731CE500566988404409E0468CDB9D44849FD5DA","Title":"Prova 13/11/2022 11:18:48 - 2","Modified":"2022-11-13T10:20:40Z","Created":"2022-11-13T10:20:40Z","AuthorId":11,"EditorId":11,"OData__UIVersionString":"1.0","Attachments":false,"GUID":"26274682-07a1-4baa-8773-746e14476c49","ComplianceAssetId":null,"CAP":"2","ProvinciaId":81}
--batchresponse_c28f9daf-ad92-4cff-bc94-cc802c1e2142
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 201 Created
CONTENT-TYPE: application/json;odata=nometadata;streaming=true;charset=utf-8
ETAG: "3803f8e2-a787-4334-9ba5-ec259ab346a2,1"
LOCATION: https://sgart.sharepoint.com/_api/Web/Lists(guid'dd4450ad-4fcc-440a-b188-493189631ddc')/Items(743)

{"FileSystemObjectType":0,"Id":743,"ServerRedirectedEmbedUri":null,"ServerRedirectedEmbedUrl":"","ID":743,"ContentTypeId":"0x01008BBC5180BF5265449EBD7959AA731CE500566988404409E0468CDB9D44849FD5DA","Title":"Prova 13/11/2022 11:18:48 - 3","Modified":"2022-11-13T10:20:40Z","Created":"2022-11-13T10:20:40Z","AuthorId":11,"EditorId":11,"OData__UIVersionString":"1.0","Attachments":false,"GUID":"245882f4-962c-4253-b6c4-8e25f17bc503","ComplianceAssetId":null,"CAP":"3","ProvinciaId":80}
--batchresponse_c28f9daf-ad92-4cff-bc94-cc802c1e2142
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 201 Created
CONTENT-TYPE: application/json;odata=nometadata;streaming=true;charset=utf-8
ETAG: "d0ce1fb5-a007-437f-b670-59c1b5dae3fe,1"
LOCATION: https://sgart.sharepoint.com/_api/Web/Lists(guid'dd4450ad-4fcc-440a-b188-493189631ddc')/Items(744)

{"FileSystemObjectType":0,"Id":744,"ServerRedirectedEmbedUri":null,"ServerRedirectedEmbedUrl":"","ID":744,"ContentTypeId":"0x01008BBC5180BF5265449EBD7959AA731CE500566988404409E0468CDB9D44849FD5DA","Title":"Prova 13/11/2022 11:18:48 - 4","Modified":"2022-11-13T10:20:40Z","Created":"2022-11-13T10:20:40Z","AuthorId":11,"EditorId":11,"OData__UIVersionString":"1.0","Attachments":false,"GUID":"b60ec29b-3104-45b0-9b70-63307484f95a","ComplianceAssetId":null,"CAP":"4","ProvinciaId":27}
--batchresponse_c28f9daf-ad92-4cff-bc94-cc802c1e2142
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 201 Created
CONTENT-TYPE: application/json;odata=nometadata;streaming=true;charset=utf-8
ETAG: "810892c7-66d2-48df-9daf-a13c3d19067d,1"
LOCATION: https://sgart.sharepoint.com/_api/Web/Lists(guid'dd4450ad-4fcc-440a-b188-493189631ddc')/Items(745)

{"FileSystemObjectType":0,"Id":745,"ServerRedirectedEmbedUri":null,"ServerRedirectedEmbedUrl":"","ID":745,"ContentTypeId":"0x01008BBC5180BF5265449EBD7959AA731CE500566988404409E0468CDB9D44849FD5DA","Title":"Prova 13/11/2022 11:18:48 - 5","Modified":"2022-11-13T10:20:40Z","Created":"2022-11-13T10:20:40Z","AuthorId":11,"EditorId":11,"OData__UIVersionString":"1.0","Attachments":false,"GUID":"f2fd4b61-8a1d-4eda-bd82-381c5fe4f51e","ComplianceAssetId":null,"CAP":"5","ProvinciaId":80}
--batchresponse_c28f9daf-ad92-4cff-bc94-cc802c1e2142--
dove per ogni item inserito, ci viene ritornato:
  • una stringa di boudary che separa le varie chiamate (--batchresponse_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
  • lo stato HTTP della chiamata, se tutto ok deve essere 201 Created
  • la url esatta dell'item inserito (parametro LOCATION)
  • l'oggetto che rappresenta l'item inserito in formato JSON, compreso l'Id assegnato
L'ordine della risposta corrisponde all'ordine in cui sono stati inseriti nella richiesta.

Autenticazione / Bearer

Il primo passo è recuperare il Bearer token tramite il metodo GetAccessToken

C#: Bearer

private string GetAccessToken(string siteUrl, string user, string password)
{
    SecureString securePassword = ToSecureString(password);
    PnP.Framework.AuthenticationManager authManager = new PnP.Framework.AuthenticationManager(user, securePassword);
    return authManager.GetAccessToken(siteUrl);
}

private SecureString ToSecureString(string plainString)
{
    if (plainString == null) return null;

    SecureString secureString = new SecureString();
    foreach (char c in plainString.ToCharArray())
    {
        secureString.AppendChar(c);
    }
    return secureString;
}
Per l'esempio ho utilizzato l'autenticazione tramite user e password, ma sono disponibili anche gli altri metodi di autenticazione.

Metodi base

Per gestire la chiamata multipart/mixed ho creato una serie di metodi base.

GetHttpClient

Il metodo GetHttpClient ritorna l'oggetto HttpClient con già presenti gli header HTTP accept e authorization

C#: GetHttpClient

private HttpClient GetHttpClient(bool includeAccept = true)
{
    HttpClient client = new HttpClient();
    if (includeAccept == true)
    {
        client.DefaultRequestHeaders.Add("accept", HEADER_ACCEPT_VALUE);
    }
    client.DefaultRequestHeaders.Add("authorization", "Bearer " + _accessToken);
    return client;
}

GetMulipartAction

Il metodo GetMulipartAction crea la singola chiamata di inserimento item

C#: GetHttpClient

private string GetMulipartAction(System.Net.Http.HttpMethod method, string url, object item)
{
    StringBuilder sb = new StringBuilder(500);

    sb.AppendFormat("{0} {1} HTTP/1.1", method, url);
    sb.AppendLine();
    sb.AppendLine("Content-Type: application/json;odata=nometadata");
    sb.AppendLine("Accept: application/json;odata=nometadata");
    sb.AppendLine();
    if (item != null)
    {
        sb.AppendLine(JsonConvert.SerializeObject(item));
    }

    return sb.ToString();
}
esempio di output
POST https://sgart.sharepoint.com/_api/web/lists/getbytitle('TestBulkInsert')/items HTTP/1.1
Content-Type: application/json;odata=nometadata
Accept: application/json;odata=nometadata

{"Id":0,"Title":"Prova 13/11/2022 11:18:48 - 5","CAP":"5","ProvinciaId":80}

PostMultipartAsync

Il metodo generico PostMultipartAsync è quello che si occupa di effettuare la chiamata multipart/mixed e fare il parse del risultato per aggiornare gli Id

C#: PostMultipartAsync

private const string BOUNDARY_BATCH = "batch_19676457-165F-46AF-9A8C-8202812CCEEE_sgart-it";
private const string BOUNDARY_CHANGESET = "changeset_C5946BFE-6A35-462D-9E3D-0A7CC4D9A1E3_sgart-it";

private async Task PostMultipartAsync<T>(string relativeUrl, List<UpdateBatch<T>> batchItems) where T : IBatchId
{
    try
    {
        // create changesets
        StringBuilder sbChangesets = new StringBuilder(batchItems.Count * 100);
        int index = 0;
        batchItems.ForEach(batch =>
        {
            sbChangesets.AppendLine("--" + BOUNDARY_CHANGESET);
            sbChangesets.AppendLine("Content-Type: application/http");
            sbChangesets.AppendLine("Content-Transfer-Encoding: binary");
            sbChangesets.AppendLine($"Content-ID: {++index}");  // crea delle variabili $1, S2, ecc. che conterrano l'id, da usare per altre rischieste 
            sbChangesets.AppendLine();

            string batchAction = GetMulipartAction(batch.Method, batch.Url, batch.Item);

            sbChangesets.AppendLine(batchAction);
        });
        int changesetsLength = sbChangesets.Length;

        // create batch
        StringBuilder sbBatch = new StringBuilder(changesetsLength + 350);
        sbBatch.AppendLine("--" + BOUNDARY_BATCH);
        sbBatch.AppendLine("Content-Type: multipart/mixed;boundary=" + BOUNDARY_CHANGESET);
        sbBatch.AppendLine("Content-Lenght: " + changesetsLength);
        sbBatch.AppendLine("Content-Transfer-Encoding: binary");
        sbBatch.AppendLine();
        sbBatch.Append(sbChangesets);
        sbBatch.AppendLine("--" + BOUNDARY_CHANGESET + "--");
        sbBatch.AppendLine("--" + BOUNDARY_BATCH + "--");

        using (HttpClient client = GetHttpClient())
        {
            using (var content = new StringContent(sbBatch.ToString()))
            {
                content.Headers.Remove("Content-Type");
                content.Headers.TryAddWithoutValidation("Content-Type", "multipart/mixed;boundary=" + BOUNDARY_BATCH);

                using (HttpResponseMessage response = await client.PostAsync(_siteUrl + relativeUrl, content))
                {
                    //response.EnsureSuccessStatusCode();
                    var result = await response.Content.ReadAsStringAsync();

                    if (response.IsSuccessStatusCode)
                    {
                        string boundaryResponse = result.Substring(0, result.IndexOf(Environment.NewLine));

                        //le risposte dei cnagesets sono nell'ordne in cui sono state inviate
                        string[] blocks = result.Split(new[] { boundaryResponse }, StringSplitOptions.RemoveEmptyEntries);

                        for (int i = 0; i < blocks.Length; i++)
                        {
                            string r = blocks[i].Trim();
                            if (r == "--")
                            {
                                break;
                            }
                            // risposta del batch attuale
                            var batch = batchItems[i];

                            // trovo lo stato HTTP
                            Regex reHttp = new Regex(@"^HTTP/1\.1 (?<status>\d+) .+$", RegexOptions.Multiline | RegexOptions.ExplicitCapture | RegexOptions.Compiled);
                            var matchHttp = reHttp.Match(r);
                            if (matchHttp.Success)
                            {
                                int.TryParse(matchHttp.Groups["status"].Value, out int status);

                                batch.HTTPStatus = status;
                                batch.Success = false;

                                string objectResponse = r.Split('\n').Last();


                                if (status >= 200 && status <= 299)
                                {
                                    if (status == 201)
                                    {
                                        BatchId obj = JsonConvert.DeserializeObject<BatchId>(objectResponse);
                                        batch.Item.Id = obj.Id;
                                    }
                                    batch.Success = true;
                                }
                                else
                                {
                                    var error = JsonConvert.DeserializeObject<SPError>(objectResponse);
                                    batch.Error = error.OdataError.Code + " - " + error.OdataError.Message.Value;
                                }
                            }
                        }
                    }
                    else
                    {
                        var error = JsonConvert.DeserializeObject<SPError>(result);
                        throw new Exception(error.OdataError.Code + " - " + error.OdataError.Message.Value);
                    }
                }

            }
        }
    }
    catch (Exception ex)
    {
        throw;
    }
}
Per il corretto funzionamento necessita di alcune classi e interfacce di supporto

C#: IBatchId.cs

public interface IBatchId
{
    int Id { get; set; }
}

// usato per deserializzare la risposta
public class BatchId : IBatchId
{
    public int Id { get; set; }
}

C#: SPError,cs

public class SPError
{
    [JsonProperty("odata.error")]
    public SPErrorOdata OdataError { get; set; }
}

public class SPErrorOdata
{
    [JsonProperty("code")]
    public string Code { get; set; }

    [JsonProperty("message")]
    public SPErrorMessage Message { get; set; }
}

public class SPErrorMessage
{
    [JsonProperty("lang")]
    public string Lang { get; set; }

    [JsonProperty("value")]
    public string Value { get; set; }

}

UpdateBatch<T>

Quest'ultima classe, UpdateBatch, è quella che andrà passata al metodo PostMultipartAsync, una per ogni item da inserire

C#: UpdateBatch.cs

public class UpdateBatch<T> where T : IBatchId
{
    public bool Success { get; set; }

    public System.Net.Http.HttpMethod Method { get; set; }

    public string Url { get; set; }

    public T Item { get; set; }

    public int HTTPStatus { get; set; }

    public string Error { get; set; }

}
I primi 3 parametri sono necessari per la chiamata:
  • Method: metodo HTTP da usare nella singola chimata (GET, POST, PATCH, PUT, DELETE, MERGE, ...)
  • Url: url univoco della risorsa, es.: https://tenantName.sharepoint.com/_api/web/lists/getbytitle('TestBulkInsert')/items)
  • Item oggetto con i dati da aggiornare, deve implementare l'interfaccia IBatch
mentre gli altri 2 vengono valorizzati dopo la risposta
  • Success: impostato a true se lo stato HTTP è 2xx
  • HTTPStatus: stato della riposta HTTP, es.: 201
inoltre viene aggiornato la proprietà Id presente in item.

Creazione chiamata di inserimento

Per inserire nuovi item va creata una classe che rappresenta le proprietà da aggiornare, questa deve implementare l'interfaccia IBatchId

C#: Esempio di entita da aggiornare

public class TestBatchData : IBatchId
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string CAP { get; set; }
    public int ProvinciaId { get; set; }
}
poi va creato una collection con gli l'item da inserire

C#: Esempio batch singolo

string itemAddUrl = sp.GetListItemUrl("TestBulkInsert");

List<UpdateBatch<TestBatchData>> batches = new List<UpdateBatch<TestBatchData>>();

UpdateBatch<TestBatchData> batch = new UpdateBatch<TestBatchData>
{
    Method = System.Net.Http.HttpMethod.Post,
    Url = itemAddUrl,
    Item = new TestBatchData
    {
        Id = 0,
        Title = $"Prova {DateTime.Now} - {i + 1}",
        CAP = (i + 1).ToString(),
        ProvinciaId = p
    }
};

batches.Add(batch);
una volta preparate tutte le chiamate, possono essere eseguite con

C#: Esecuzione chiamate batch

await sp.ExecuteBatch(batches);

Esempio

Questo è esempio completo che inserisce valori random

C#: Creazione chiamate multiple

static async Task Main(string[] args)
{
    string siteUrl = "https://tenantName.sharepoint.com/";
    string user = "aaaaa@tenantName.onmicrosoft.com";
    string password = "xxxxxxxxxxx";

    SPBatchManager sp = new SPBatchManager(siteUrl, user, password);

    // recupero la url di inserimento item (sempre uguale)
    string itemAddUrl = sp.GetListItemUrl("TestBulkInsert");

    // creo la collection degli items da inserire
    List<UpdateBatch<TestBatchData>> batches = new List<UpdateBatch<TestBatchData>>();
    Random rnd = new Random();
    for (int i = 0; i < 50; i++)
    {
        int p = rnd.Next(1, 105);

        // creo la singola chiamata batch
        UpdateBatch<TestBatchData> batch = new UpdateBatch<TestBatchData>
        {
            Method = System.Net.Http.HttpMethod.Post,
            Url = itemAddUrl,
            Item = new TestBatchData
            {
                Id = 0,
                Title = $"Prova {DateTime.Now} - {i + 1}",
                CAP = (i + 1).ToString(),
                ProvinciaId = p
            }
        };

        batches.Add(batch);
    }

    DateTime startDate = DateTime.Now;

    // eseguo l'inserimento in batch
    await sp.ExecuteBatch(batches);

    Console.WriteLine($"Time: {DateTime.Now - startDate}");
}

Get

Ovviamente si possono gestire anche le chiamate in GET semplicemente aggiungendo alcune classi di supporto

C#: SPWeb.cs

// per leggere le proprietà dei siti
public class SPWeb
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public DateTime Created { get; set; }
    // TODO: aggiungere le proprietà necessarie
}

C#: SPList.cs

public class SPList
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public DateTime Created { get; set; }
    public int ItemCount { get; set; }
    // TODO: aggiungere le proprietà necessarie
}

C#: SPValueArray.cs

public class SPValueArray<T>
{
    public List<T> Value { get; set; }
}
e dei metodi nel manager
public async Task<SPWeb> GetWeb()
{
    var content = await GetAsync(API_GET_WEB);
    var obj = JsonConvert.DeserializeObject<SPWeb>(content);
    return obj;
}

public async Task<List<SPList>> GetLists()
{
    var content = await GetAsync(API_GET_LISTS);
    var obj = JsonConvert.DeserializeObject<SPValueArray<SPList>>(content);
    return obj.Value;
}
ma in questo caso usare le operazioni REST non da alcun vantaggio in quanto PnP.Framework supporta già tutte le operazioni.

Conclusione

Conoscendo i dettagli delle chiamate REST API è possibile implementare qualunque funzione mancante nelle librerie.
Tags:
C#235 SharePoint Online75 SharePoint 201667
Potrebbe interessarti anche: