Sharepoint online chiamate batch con C#
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:
indicando nell'header
in questo caso si tratta di un inserendo di 5 item contemporaneamente (batch).
dove per ogni item inserito, ci viene ritornato:
esempio di output
Per il corretto funzionamento necessita di alcune classi e interfacce di supporto
I primi 3 parametri sono necessari per la chiamata:
poi va creato una collection con gli l'item da inserire
una volta preparate tutte le chiamate, possono essere eseguite con
e dei metodi nel manager
ma in questo caso usare le operazioni REST non da alcun vantaggio in quanto PnP.Framework supporta già tutte le operazioni.
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'endpointText: Batch
https://tenantName.sharepoint.com/_api/$batch
- 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--
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--
- 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 GetAccessTokenC#: 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 authorizationC#: 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 itemC#: 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();
}
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 IdC#: 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;
}
}
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 inserireC#: 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; }
}
- 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
- Success: impostato a true se lo stato HTTP è 2xx
- HTTPStatus: stato della riposta HTTP, es.: 201
Creazione chiamata di inserimento
Per inserire nuovi item va creata una classe che rappresenta le proprietà da aggiornare, questa deve implementare l'interfaccia IBatchIdC#: 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; }
}
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);
C#: Esecuzione chiamate batch
await sp.ExecuteBatch(batches);
Esempio
Questo è esempio completo che inserisce valori randomC#: 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 supportoC#: 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; }
}
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;
}