google-ads-dotnet icon indicating copy to clipboard operation
google-ads-dotnet copied to clipboard

`SearchAsync()` method doesn't return anything when `SearchSettings.ReturnTotalResultsCount == true`

Open SetTrend opened this issue 4 months ago • 5 comments

Forwarded from this Google Ads API forum thread:


When using a SearchSettings object with property ReturnTotalResultsCount set to true, nothing is returned.

So, the potential total number of rows cannot be retrieved.

SearchGoogleAdsRequest request = new SearchGoogleAdsRequest()
{
  CustomerId = ...,
  Query = ...,
  SearchSettings = new SearchSettings() { OmitResults = true, ReturnTotalResultsCount = true };
};

await foreach (GoogleAdsRow test in service.SearchAsync(request))
{
  // never entered,
}

I propose to add an optional method ref long parameter that's supposed to receive the totalResultsCount return value.

SetTrend avatar Sep 02 '25 17:09 SetTrend

You can retrieve the totalResultsCount from the SearchAsync response like this:

service.SearchAsync(request).AsRawResponses().GetAsyncEnumerator().Current.TotalResultsCount.

Raibaz avatar Sep 03 '25 10:09 Raibaz

Hi @Raibaz, thank you for clarifying! 👍👍

Wouldn't it make sense to have the Search()/SearchAsync() methods return an SearchGoogleAdsResponse instead in the first place?

I feel it's odd to have an Async method not returning a Task. If SearchAsync() returned a Task<SearchGoogleAdsResponse> and Search() returned a SearchGoogleAdsResponse , I believe these methods would rather act by the book.

SetTrend avatar Sep 03 '25 10:09 SetTrend

service.SearchAsync(request).AsRawResponses().GetAsyncEnumerator().Current always yields null:

Image

SetTrend avatar Sep 03 '25 13:09 SetTrend

Ah, you're right, because the AsyncEnumerator is evaluated lazily, so you'd need to call MoveNextAsync on it first:

IAsyncEnumerator<SearchGoogleAdsResponse> enumerator = service.searchAsync(request).asRawResponses().getAsyncEnumerator();

Task.WaitAll(enumerator.MoveNextAsync().asTask());

retValue = enumerator.Current.TotalResultsCount;

The reason why Search and SearchAsync return a PagedEnumerable instead of a Task or a SearchGoogleAdsResponse is to allow looping directly on the returned value, as we do in code examples, which is the most common use case.

Raibaz avatar Sep 03 '25 16:09 Raibaz

Actually, I rewrote my repository's Google Ads accessor class today for it to return SearchGoogleAdsResponse for both, Search() and SearchStream(). It turned out that I can easily iterate through SearchGoogleAdsResponse.

SearchGoogleAdsResponse also implements IEnumerable<> and apparently forwards the iteration to its SearchGoogleAdsResponse.Results property:

Image

So, "good news, everyone" 😉 – Search() and SearchAsync() can return SearchGoogleAdsResponse without breaking existing code. Plus, they can also provide the additional SearchGoogleAdsResponse properties.


PS: And in my code, I return SearchGoogleAdsResponse.TotalResultsCount = -1 if SearchSettings.ReturnTotalResultsCount is false, to distinguish valid values from an unset value.

SetTrend avatar Sep 03 '25 17:09 SetTrend

Hi @Raibaz, would you want to forward my proposal to the corresponding team?

SetTrend avatar Sep 22 '25 13:09 SetTrend

Uhm, I'm not sure I understand what your proposal is.

Search and SearchAsync already return, respectively, a PagedEnumerable<SearchGoogleAdsResponse, GoogleAdsRow> and a PagedAsyncEnumerable<SearchGoogleAdsResponse, GoogleAdsRow> and you can get the SearchGoogleAdsResponse from them already, so what should we change?

Raibaz avatar Sep 22 '25 13:09 Raibaz

Sure, thanks for letting me explain my reasoning:

Currently, GoogleAdsServiceClient.SearchAsync() returns a GoogleAdsRow enumerator. With this return data type, it's not possible to get the total results count.

If, instead, GoogleAdsServiceClient.SearchAsync() returned a SearchGoogleAdsResponse, users can as well iterate over it – plus they get access to the total results count and the summary row.

For example, this is an excerpt from my current Google Ads API accessor implementation:

if (settings.ReturnTotalResultsCount || settings.ReturnSummaryRow)
{
  IAsyncEnumerator<SearchGoogleAdsResponse> enumerator = service.SearchAsync(request).AsRawResponses().GetAsyncEnumerator();

  await enumerator.MoveNextAsync();

  retValue = enumerator.Current;
}
else
{
  retValue = new SearchGoogleAdsResponse { TotalResultsCount = -1 };

  using GoogleAdsServiceClient.SearchStreamStream stream
    = await service.SearchStreamAsync(request
                                    , response =>
                                      {
                                        foreach (GoogleAdsRow row in response.Results)
                                          retValue.Results.Add(row);
                                      }
                                    );
}

Then, my client can simply query both, total count and rows, in a single call:

SearchGoogleAdsResponse result = await _gaqlAccessor.GetAsync(query, ...);

So, all data would be available without the current ado.

SetTrend avatar Sep 23 '25 12:09 SetTrend

Ok, I see what you mean.

Unfortunately, we can't make changes to the SearchAsync method itself because it's generated by the GAPIC generator; however, we could encapsulate the behavior you are describing in the AdsPagedAsyncEnumerable class, which is the one SearchAsync returns.

Would you be able to make the necessary changes to the AdsPagedAsyncEnumerable class and send a PR?

Raibaz avatar Sep 29 '25 12:09 Raibaz

Hi @Raibaz,

apologies for my late reply.

I would have loved to create a PR. Unfortunately, I'm currently busy working for a client. So, this time I cannot walk the long road of programming, having merge request discussions and (eventually) having my (unsolicited) improvements rejected due to the team contributors' other plans.

However, I quickly needed a solution for my client, so I created a .NET NuGet package based on the Google Ads Query Language and the Google.Ads.GoogleAds NuGet package, extending both. I decided to share this NuGet package publicly, so everyone can benefit from it:

https://www.nuget.org/packages/AxDa.GaqlContext/

I don't want to brag here. Please excuse me if I accidentally give that impression.

Here's what I did:

My first step was to add an OFFSET and a COUNT() clause to GAQL. Next, I added a batch operator because I needed to concatenate the result from multiple queries. Then I added a USER clause (because our MCC account manages 23 customer ids) to concatenate the results from multiple customer IDs, and finally I added a parameter feature to GAQL, so I didn't have to repeat the same queries over and over again. Not to mention the new ability to add comments to a GAQL query.

Along with writing my NuGet package, I created a convenient REPL tool in WPF that allows me to write GAQL-E queries and send them to Google Ads servers.

Here is a short screencast, demonstrating how well they both work together:

https://github.com/user-attachments/assets/b32718ee-3897-4b8f-9ef0-71eda076a900

My GaqlRepl tool, however, I am not going to publish.

SetTrend avatar Oct 15 '25 21:10 SetTrend

That's actually pretty cool! :)

Glad you found a way to make this work.

Raibaz avatar Oct 16 '25 09:10 Raibaz