/// <summary> /// Get SObject by ID /// </summary> /// <param name="sObjectTypeName">SObject name, e.g. "Account"</param> /// <param name="objectId">SObject ID</param> /// <param name="fields">(optional) List of fields to retrieve, if not supplied, all fields are retrieved.</param> public async Task <T> GetObjectById <T>(string sObjectTypeName, string objectId, List <string> fields = null) { Dictionary <string, string> headers = HeaderFormatter.SforceCallOptions(ClientName); var uri = UriFormatter.SObjectRows(InstanceUrl, ApiVersion, sObjectTypeName, objectId, fields); JsonClient client = new JsonClient(AccessToken, _httpClient); return(await client.HttpGetAsync <T>(uri, headers)); }
/// <summary> /// Retrieve (basic) metadata for an object. /// <para>Use the SObject Basic Information resource to retrieve metadata for an object.</para> /// </summary> /// <param name="objectTypeName">SObject name, e.g. Account</param> /// <returns>Returns SObjectMetadataBasic with basic object metadata and a list of recently created items.</returns> public async Task <SObjectBasicInfo> GetObjectBasicInfo(string objectTypeName) { Dictionary <string, string> headers = HeaderFormatter.SforceCallOptions(ClientName); var uri = UriFormatter.SObjectBasicInformation(InstanceUrl, ApiVersion, objectTypeName); JsonClient client = new JsonClient(AccessToken, _httpClient); return(await client.HttpGetAsync <SObjectBasicInfo>(uri, headers)); }
/// <summary> /// Delete record /// </summary> /// <param name="sObjectTypeName">SObject name, e.g. "Account"</param> /// <param name="objectId">Id of Object to update</param> /// <returns>void, API returns 204/NoContent</returns> /// <exception cref="ForceApiException">Thrown when update fails</exception> public async Task DeleteRecord(string sObjectTypeName, string objectId) { Dictionary <string, string> headers = HeaderFormatter.SforceCallOptions(ClientName); var uri = UriFormatter.SObjectRows(InstanceUrl, ApiVersion, sObjectTypeName, objectId); JsonClient client = new JsonClient(AccessToken, _httpClient); await client.HttpDeleteAsync <object>(uri, headers); return; }
/// <summary> /// Retrieve a <see cref="IAsyncEnumerable{T}"/> using a SOQL query. Batches will be retrieved asynchronously. /// <para>When using the iterator, the initial result batch will be returned as soon as it is received. The additional result batches will be retrieved only as needed.</para> /// </summary> /// <param name="queryString">SOQL query string, without any URL escaping/encoding</param> /// <param name="queryAll">Optional. True if deleted records are to be included.await Defaults to false.</param> /// <param name="batchSize">Optional. Size of result batches between 200 and 2000</param> /// <param name="cancellationToken">Optional. Cancellation token</param> /// <returns><see cref="IAsyncEnumerable{T}"/> of results</returns> public async IAsyncEnumerable <T> QueryAsync <T>(string queryString, bool queryAll = false, int?batchSize = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Dictionary <string, string> headers = new Dictionary <string, string>(); // Add call options Dictionary <string, string> callOptions = HeaderFormatter.SforceCallOptions(ClientName); headers.AddRange(callOptions); // Add query options headers if batch size specified if (batchSize.HasValue) { Dictionary <string, string> queryOptions = HeaderFormatter.SforceQueryOptions(batchSize.Value); headers.AddRange(queryOptions); } var jsonClient = new JsonClient(AccessToken, _httpClient); var nextRecordsUri = UriFormatter.Query(InstanceUrl, ApiVersion, queryString, queryAll); bool hasMoreRecords = true; while (hasMoreRecords) { var qr = await jsonClient.HttpGetAsync <QueryResult <T> >(nextRecordsUri, headers); #if DEBUG Debug.WriteLine($"Got query resuts, {qr.TotalSize} totalSize, {qr.Records.Count} in this batch, final batch: {qr.Done}"); #endif if (!string.IsNullOrEmpty(qr.NextRecordsUrl)) { nextRecordsUri = new Uri(new Uri(InstanceUrl), qr.NextRecordsUrl); hasMoreRecords = true; } else { // Normally if a query has remaining batches, NextRecordsUrl will have a value, and Done will be false. // In case of some unforseen error, consider the results done if we're missing the NextRecordsURL hasMoreRecords = false; } foreach (T record in qr.Records) { yield return(record); } } }
/// <summary> /// Get a basic SOQL COUNT() query result /// <para>The query must start with SELECT COUNT() FROM, with no named field in the count clause. COUNT() must be the only element in the SELECT list.</para> /// </summary> /// <param name="queryString">SOQL query string starting with SELECT COUNT() FROM</param> /// <param name="queryAll">True if deleted records are to be included</param> /// <returns>The <see cref="Task{Int}"/> returning the count</returns> public async Task <int> CountQuery(string queryString, bool queryAll = false) { // https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_count.htm // COUNT() must be the only element in the SELECT list. Dictionary <string, string> headers = HeaderFormatter.SforceCallOptions(ClientName); if (!queryString.Replace(" ", "").ToLower().StartsWith("selectcount()from")) { throw new ForceApiException("CountQueryAsync may only be used with a query starting with SELECT COUNT() FROM"); } var jsonClient = new JsonClient(AccessToken, _httpClient); var uri = UriFormatter.Query(InstanceUrl, ApiVersion, queryString); var qr = await jsonClient.HttpGetAsync <QueryResult <object> >(uri, headers); return(qr.TotalSize); }
/// <summary> /// Executes a SOSL search, returning a simple generic object in the results collection that primarly results in a list of object IDs /// </summary> /// <param name="searchString"></param> /// <returns>SearchResult{SObjectGeneric}</returns> public async Task <SearchResult <SObjectGeneric> > Search(string searchString) { try { Dictionary <string, string> headers = HeaderFormatter.SforceCallOptions(ClientName); var uri = UriFormatter.Search(InstanceUrl, ApiVersion, searchString); JsonClient client = new JsonClient(AccessToken, _httpClient); SearchResult <SObjectGeneric> searchResult = await client.HttpGetAsync <SearchResult <SObjectGeneric> >(uri, headers); return(searchResult); } catch (Exception ex) { Debug.WriteLine("Error searching: " + ex.Message); throw; } }
/// <summary> /// Inserts or Updates a records based on external id /// </summary> /// <param name="sObjectTypeName">SObject name, e.g. "Account"</param> /// <param name="fieldName">External ID field name</param> /// <param name="fieldValue">External ID field value</param> /// <param name="sObject">Object to update</param> /// <param name="customHeaders">Custom headers to include in request (Optional). await The HeaderFormatter helper class can be used to generate the custom header as needed.</param> /// <returns>CreateResponse object, includes new object's ID if record was created and no value if object was updated</returns> /// <exception cref="ForceApiException">Thrown when request fails</exception> public async Task <CreateResponse> InsertOrUpdateRecord <T>(string sObjectTypeName, string fieldName, string fieldValue, T sObject, Dictionary <string, string> customHeaders = null) { Dictionary <string, string> headers = new Dictionary <string, string>(); //Add call options Dictionary <string, string> callOptions = HeaderFormatter.SforceCallOptions(ClientName); headers.AddRange(callOptions); //Add custom headers if specified if (customHeaders != null) { headers.AddRange(customHeaders); } var uri = UriFormatter.SObjectRowsByExternalId(InstanceUrl, ApiVersion, sObjectTypeName, fieldName, fieldValue); JsonClient client = new JsonClient(AccessToken, _httpClient); return(await client.HttpPatchAsync <CreateResponse>(sObject, uri, headers)); }
/// <summary> /// Updates /// </summary> /// <param name="sObjectTypeName">SObject name, e.g. "Account"</param> /// <param name="objectId">Id of Object to update</param> /// <param name="sObject">Object to update</param> /// <param name="customHeaders">Custom headers to include in request (Optional). await The HeaderFormatter helper class can be used to generate the custom header as needed.</param> /// <returns>void, API returns 204/NoContent</returns> /// <exception cref="ForceApiException">Thrown when update fails</exception> public async Task UpdateRecord <T>(string sObjectTypeName, string objectId, T sObject, Dictionary <string, string> customHeaders = null) { Dictionary <string, string> headers = new Dictionary <string, string>(); //Add call options Dictionary <string, string> callOptions = HeaderFormatter.SforceCallOptions(ClientName); headers.AddRange(callOptions); //Add custom headers if specified if (customHeaders != null) { headers.AddRange(customHeaders); } var uri = UriFormatter.SObjectRows(InstanceUrl, ApiVersion, sObjectTypeName, objectId); JsonClient client = new JsonClient(AccessToken, _httpClient); await client.HttpPatchAsync <object>(sObject, uri, headers); return; }
/// <summary> /// Update multiple reocrds. /// The list can contain up to 200 objects. /// The list can contain objects of different types, including custom objects. /// Each object must contain an attributes map. The map must contain a value for type. /// Each object must contain an id field with a valid ID value. /// </summary> /// <param name="sObjects">Objects to update</param> /// <param name="allOrNone">Optional. Indicates whether to roll back the entire request when the update of any object fails (true) or to continue with the independent update of other objects in the request. The default is false.</param> /// <param name="customHeaders">Custom headers to include in request (Optional). await The HeaderFormatter helper class can be used to generate the custom header as needed.</param> /// <returns>List of UpdateMultipleResponse objects, includes response for each object (id, success, errors)</returns> /// <exception cref="ArgumentException">Thrown when missing required information</exception> /// <exception cref="ForceApiException">Thrown when update fails</exception> public async Task <List <UpdateMultipleResponse> > UpdateRecords(List <SObject> sObjects, bool allOrNone = false, Dictionary <string, string> customHeaders = null) { if (sObjects == null) { throw new ArgumentNullException("sObjects"); } foreach (SObject sObject in sObjects) { if (sObject.Attributes == null || string.IsNullOrEmpty(sObject.Attributes.Type)) { throw new ForceApiException("Objects are missing Type property in Attributes map"); } } Dictionary <string, string> headers = new Dictionary <string, string>(); //Add call options Dictionary <string, string> callOptions = HeaderFormatter.SforceCallOptions(ClientName); headers.AddRange(callOptions); //Add custom headers if specified if (customHeaders != null) { headers.AddRange(customHeaders); } var uri = UriFormatter.SObjectsComposite(InstanceUrl, ApiVersion); JsonClient client = new JsonClient(AccessToken, _httpClient); UpdateMultipleRequest updateMultipleRequest = new UpdateMultipleRequest(sObjects, allOrNone); return(await client.HttpPatchAsync <List <UpdateMultipleResponse> >(updateMultipleRequest, uri, headers, includeSObjectId : true)); }
/// <summary> /// Retrieve records using a SOQL query. /// <para>Will automatically retrieve the complete result set if split into batches. If you want to limit results, use the LIMIT operator in your query.</para> /// </summary> /// <param name="queryString">SOQL query string, without any URL escaping/encoding</param> /// <param name="queryAll">True if deleted records are to be included</param> /// <returns>List{T} of results</returns> public async Task <List <T> > Query <T>(string queryString, bool queryAll = false) { #if DEBUG Stopwatch sw = new Stopwatch(); sw.Start(); #endif try { Dictionary <string, string> headers = HeaderFormatter.SforceCallOptions(ClientName); var queryUri = UriFormatter.Query(InstanceUrl, ApiVersion, queryString, queryAll); JsonClient client = new JsonClient(AccessToken, _httpClient); List <T> results = new List <T>(); bool done = false; string nextRecordsUrl = string.Empty; //larger result sets will be split into batches (sized according to system and account settings) //if additional batches are indicated retrieve the rest and append to the result set. do { if (!string.IsNullOrEmpty(nextRecordsUrl)) { queryUri = new Uri(new Uri(InstanceUrl), nextRecordsUrl); } QueryResult <T> qr = await client.HttpGetAsync <QueryResult <T> >(queryUri, headers); #if DEBUG Debug.WriteLine(string.Format("Got query resuts, {0} totalSize, {1} in this batch, final batch: {2}", qr.TotalSize, qr.Records.Count.ToString(), qr.Done.ToString())); #endif results.AddRange(qr.Records); done = qr.Done; nextRecordsUrl = qr.NextRecordsUrl; if (!done && string.IsNullOrEmpty(nextRecordsUrl)) { //Normally if query has remaining batches, NextRecordsUrl will have a value, and Done will be false. //In case of some unforseen error, flag the result as done if we're missing the NextRecordsUrl //In this situation we'll just get the previous set again and be stuck in a loop. done = true; } } while (!done); #if DEBUG sw.Stop(); Debug.WriteLine(string.Format("Query results retrieved in {0}ms", sw.ElapsedMilliseconds.ToString())); #endif return(results); } catch (Exception ex) { Debug.WriteLine("Error querying: " + ex.Message); throw; } }
/// <summary> /// Retrieve a <see cref="IAsyncEnumerator{T}"/> using a SOQL query. Batches will be retrieved asynchronously. /// <para>When using the iterator, the initial result batch will be returned as soon as it is received. The additional result batches will be retrieved only as needed.</para> /// </summary> /// <param name="queryString">SOQL query string, without any URL escaping/encoding</param> /// <param name="queryAll">Optional. True if deleted records are to be included.await Defaults to false.</param> /// <param name="batchSize">Optional. Size of result batches between 200 and 2000</param> /// <returns><see cref="IAsyncEnumerator{T}"/> of results</returns> public IAsyncEnumerator <T> QueryAsyncEnumerator <T>(string queryString, bool queryAll = false, int?batchSize = null) { Dictionary <string, string> headers = new Dictionary <string, string>(); //Add call options Dictionary <string, string> callOptions = HeaderFormatter.SforceCallOptions(ClientName); headers.AddRange(callOptions); //Add query options headers if batch size specified if (batchSize.HasValue) { Dictionary <string, string> queryOptions = HeaderFormatter.SforceQueryOptions(batchSize.Value); headers.AddRange(queryOptions); } var jsonClient = new JsonClient(AccessToken, _httpClient); // Enumerator on the current batch items IEnumerator <T> currentBatchEnumerator = null; var done = false; var nextRecordsUri = UriFormatter.Query(InstanceUrl, ApiVersion, queryString, queryAll); return(AsyncEnumerable.CreateEnumerator(MoveNextAsync, Current, Dispose)); async Task <bool> MoveNextAsync(CancellationToken token) { if (token.IsCancellationRequested) { return(false); } // If items remain in the current Batch enumerator, go to next item if (currentBatchEnumerator?.MoveNext() == true) { return(true); } // if done, no more items. if (done) { return(false); } // else : no enumerator or currentBatchEnumerator ended // so get the next batch var qr = await jsonClient.HttpGetAsync <QueryResult <T> >(nextRecordsUri, headers); #if DEBUG Debug.WriteLine($"Got query resuts, {qr.TotalSize} totalSize, {qr.Records.Count} in this batch, final batch: {qr.Done}"); #endif currentBatchEnumerator = qr.Records.GetEnumerator(); if (!string.IsNullOrEmpty(qr.NextRecordsUrl)) { nextRecordsUri = new Uri(new Uri(InstanceUrl), qr.NextRecordsUrl); done = false; } else { //Normally if query has remaining batches, NextRecordsUrl will have a value, and Done will be false. //In case of some unforseen error, flag the result as done if we're missing the NextRecordsURL done = true; } return(currentBatchEnumerator.MoveNext()); } T Current() { return(currentBatchEnumerator == null ? default(T) : currentBatchEnumerator.Current); } void Dispose() { currentBatchEnumerator?.Dispose(); jsonClient.Dispose(); } }