diff --git a/src/CommonLibrariesForNET/CommonLibrariesForNET.csproj b/src/CommonLibrariesForNET/CommonLibrariesForNET.csproj index ba151d83..bbf37867 100644 --- a/src/CommonLibrariesForNET/CommonLibrariesForNET.csproj +++ b/src/CommonLibrariesForNET/CommonLibrariesForNET.csproj @@ -75,6 +75,7 @@ + @@ -105,4 +106,4 @@ --> - \ No newline at end of file + diff --git a/src/CommonLibrariesForNET/Models/Xml/UpsertJobInfo.cs b/src/CommonLibrariesForNET/Models/Xml/UpsertJobInfo.cs new file mode 100644 index 00000000..09fe5444 --- /dev/null +++ b/src/CommonLibrariesForNET/Models/Xml/UpsertJobInfo.cs @@ -0,0 +1,23 @@ +using System.Xml.Serialization; + +namespace Salesforce.Common.Models.Xml +{ + [XmlRoot(Namespace = "http://www.force.com/2009/06/asyncapi/dataload", + ElementName = "jobInfo", + IsNullable = false)] + public class UpsertJobInfo + { + [XmlElement(ElementName = "operation")] + public string Operation { get; set; } + + [XmlElement(ElementName = "object")] + public string Object { get; set; } + + [XmlElement(ElementName = "externalIdFieldName")] + public string ExternalField { get; set; } + + [XmlElement(ElementName = "contentType")] + public string ContentType { get; set; } + + } +} diff --git a/src/ForceToolkitForNET/BulkForceClient.cs b/src/ForceToolkitForNET/BulkForceClient.cs new file mode 100644 index 00000000..9633113e --- /dev/null +++ b/src/ForceToolkitForNET/BulkForceClient.cs @@ -0,0 +1,104 @@ +using Salesforce.Common.Models.Xml; +using Salesforce.Force; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Salesforce.Force +{ + /// + /// Extends ForceClient for creating bulk upsert jobs using new UpsertJobInfo. + /// + /// For a complete list of possible JobInfo fields, see https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/asynch_api_reference_jobinfo.htm + /// + /// + /// This class requires the following properties in ForceClient to be minimally exposed as protected: + /// - _xmlHttpClient + /// - _jsonHttpClient + /// + public class BulkForceClient : ForceClient, IDisposable, IBulkForceClient + { + public BulkForceClient(string instanceUrl, string accessToken, string apiVersion) + : this(instanceUrl, accessToken, apiVersion, new HttpClient(), new HttpClient()) + { + } + + public BulkForceClient(string instanceUrl, string accessToken, string apiVersion, HttpClient httpClientForJson, HttpClient httpClientForXml) + : base(instanceUrl, accessToken, apiVersion, httpClientForJson, httpClientForXml) + { + } + + public async Task CreateUpsertJobAsync(string objectName, string externalField, BulkConstants.OperationType operationType) + { + if (string.IsNullOrEmpty(objectName)) throw new ArgumentNullException("objectName"); + + var jobInfo = new UpsertJobInfo + { + ContentType = "XML", + Object = objectName, + ExternalField = externalField, + Operation = operationType.Value() + }; + + return await _xmlHttpClient.HttpPostAsync(jobInfo, "/services/async/{0}/job"); + } + + public async Task> RunUpsertJobAsync(string objectName, string externalFieldName, BulkConstants.OperationType operationType, + IEnumerable> recordsLists) + { + if (recordsLists == null) throw new ArgumentNullException("recordsLists"); + + var jobInfoResult = await CreateUpsertJobAsync(objectName, externalFieldName, operationType); + var batchResults = new List(); + foreach (var recordList in recordsLists) + { + batchResults.Add(await CreateJobBatchAsync(jobInfoResult, recordList)); + } + await CloseJobAsync(jobInfoResult); + return batchResults; + } + + public async Task> RunUpsertJobAndPollAsync(string objectName, string externalFieldName, BulkConstants.OperationType operationType, + IEnumerable> recordsLists) + { + const float pollingStart = 1000; + const float pollingIncrease = 2.0f; + + var batchInfoResults = await RunUpsertJobAsync(objectName, externalFieldName, operationType, recordsLists); + + var currentPoll = pollingStart; + var finishedBatchInfoResults = new List(); + while (batchInfoResults.Count > 0) + { + var removeList = new List(); + foreach (var batchInfoResult in batchInfoResults) + { + var batchInfoResultNew = await PollBatchAsync(batchInfoResult); + if (batchInfoResultNew.State.Equals(BulkConstants.BatchState.Completed.Value()) || + batchInfoResultNew.State.Equals(BulkConstants.BatchState.Failed.Value()) || + batchInfoResultNew.State.Equals(BulkConstants.BatchState.NotProcessed.Value())) + { + finishedBatchInfoResults.Add(batchInfoResultNew); + removeList.Add(batchInfoResult); + } + } + foreach (var removeItem in removeList) + { + batchInfoResults.Remove(removeItem); + } + + await Task.Delay((int)currentPoll); + currentPoll *= pollingIncrease; + } + + + var batchResults = new List(); + foreach (var batchInfoResultComplete in finishedBatchInfoResults) + { + batchResults.Add(await GetBatchResultAsync(batchInfoResultComplete)); + } + return batchResults; + } + } +} diff --git a/src/ForceToolkitForNET/ForceClient.cs b/src/ForceToolkitForNET/ForceClient.cs index dd7149b1..511f271a 100644 --- a/src/ForceToolkitForNET/ForceClient.cs +++ b/src/ForceToolkitForNET/ForceClient.cs @@ -13,8 +13,8 @@ namespace Salesforce.Force { public class ForceClient : IForceClient, IDisposable { - private readonly XmlHttpClient _xmlHttpClient; - private readonly JsonHttpClient _jsonHttpClient; + protected readonly XmlHttpClient _xmlHttpClient; + protected readonly JsonHttpClient _jsonHttpClient; public ForceClient(string instanceUrl, string accessToken, string apiVersion) : this(instanceUrl, accessToken, apiVersion, new HttpClient(), new HttpClient()) diff --git a/src/ForceToolkitForNET/ForceToolkitForNET.csproj b/src/ForceToolkitForNET/ForceToolkitForNET.csproj index 9592b245..6cae4f4b 100644 --- a/src/ForceToolkitForNET/ForceToolkitForNET.csproj +++ b/src/ForceToolkitForNET/ForceToolkitForNET.csproj @@ -49,6 +49,8 @@ + + diff --git a/src/ForceToolkitForNET/IBulkForceClient.cs b/src/ForceToolkitForNET/IBulkForceClient.cs new file mode 100644 index 00000000..9e6f571d --- /dev/null +++ b/src/ForceToolkitForNET/IBulkForceClient.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Salesforce.Common.Models.Xml; +using Salesforce.Force; + +namespace Salesforce.Force +{ + public interface IBulkForceClient : IForceClient + { + Task CreateUpsertJobAsync(string objectName, string externalField, BulkConstants.OperationType operationType); + Task> RunUpsertJobAndPollAsync(string objectName, string externalFieldName, BulkConstants.OperationType operationType, IEnumerable> recordsLists); + Task> RunUpsertJobAsync(string objectName, string externalFieldName, BulkConstants.OperationType operationType, IEnumerable> recordsLists); + } +}