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);
+ }
+}