/// <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. 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); return(qr.TotalSize); }
/// <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> /// Retrieve records using a SOQL query. /// <para>Will automatically retrieve the complete result set if split into batches. If you wan tto 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 { 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); #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 ex; } }
/// <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(); } }