Precedentemente avevo mostrato come effettuare
chiamate batch con JavaScript e
creare item in batch con Power AutomateIn 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:
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
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
GetAccessTokenprivate 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
authorizationprivate 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
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
Idprivate 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
public interface IBatchId
{
int Id { get; set; }
}
// usato per deserializzare la risposta
public class BatchId : IBatchId
{
public int Id { get; set; }
}
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
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
IBatchIdpublic 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
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
await sp.ExecuteBatch(batches);
Esempio
Questo è esempio completo che inserisce valori random
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
// 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
}
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
}
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.