What a 2019 API client teaches you about modern .NET

johnvonoakland1 pts0 comments

what a 2019 api client teaches you about modern .net – Ivan Jurina

what a 2019 api client teaches you about modern .net<br>July 21, 2026<br>|In Uncategorized<br>|By ivanjurina

i’ve been playing with SerpApi lately. nice service. one GET request and you get structured json for google, bing, maps, news, shopping and about a hundred other search surfaces. captchas and layout changes handled for you. their python and ruby stories are strong. their .net story is a time capsule.

the official google-search-results-dotnet package was written around 2019 and it shows. Hashtable parameters. Newtonsoft.Json. a synchronous api that blocks on Task.Result. not a knock on SerpApi, every company has a long tail of sdks. but it makes a great case study. the distance between "working 2019 c#" and "good 2026 c#" is exactly the stuff that bites people in production. so i did two things. sent a set of prs to the official library, and built a modern client from scratch (serpapi-dotnet). here’s what changed and why it matters.

1. sync-over-async is a deadlock waiting for a synchronization context

the original core looks like this:

Task queryTask = createQuery(uri, parameter, jsonEnabled);<br>queryTask.ConfigureAwait(true); // does nothing here, by the way<br>return queryTask.Result;

two problems. first, Task.Result wraps any failure in an AggregateException. callers catch (SerpApiSearchException) and miss. second, blocking on a task that resumes on a captured context deadlocks classic asp.net and ui apps. the ConfigureAwait(true) on a task variable (not an await) is a no-op. a hint that the intent was understood but the mechanics weren’t.

the fix when you must keep a sync api for compatibility: GetAwaiter().GetResult(), which rethrows the original exception, plus ConfigureAwait(false) on every await inside the library. the real fix: expose GetJsonAsync(CancellationToken) and let callers be async end to end. my pr does both without breaking the existing surface.

2. catch (Exception ex) => throw new X(ex.ToString()) destroys the stack

the original wraps every failure like this:

catch (Exception ex)<br>throw new SerpApiSearchException(ex.ToString());

stringifying the exception into a message means no InnerException, no type to catch on, and cancellation gets swallowed into a generic error. the modern pattern: let OperationCanceledException flow untouched, wrap transport errors with the original as InnerException, and carry the http status code on your exception type. callers can then tell a 401 from a 429 without parsing strings.

3. Hashtable to typed request with an escape hatch

Hashtable is pre-generics .net. the interesting design question is what replaces it, because SerpApi has dozens of engines with different parameters. a rigid typed model can’t cover them all. a plain Dictionary gives up on discoverability. the answer is both:

var request = new SearchRequest<br>Engine = SearchEngine.GoogleNews,<br>Query = "dotnet 9",<br>Location = "Prague, Czechia",<br>AdditionalParameters = { ["so"] = "1" }, // anything engine-specific<br>};

common parameters are typed and documented. everything else passes through. same philosophy on the response side: typed accessors for organic_results and friends, and a raw JsonElement Root for the long tail of answer boxes and knowledge graphs.

4. Newtonsoft to System.Text.Json source generation

dropping Newtonsoft isn’t about fashion. with [JsonSerializable] source generation you get reflection-free deserialization that’s trim-safe and native-aot-compatible. the library ends up with zero external dependencies. for an sdk, every dependency you don’t take is a diamond-dependency conflict your users don’t have.

one real-world wrinkle worth showing: SerpApi’s local_results is sometimes an array and sometimes an object containing a places array, depending on the engine. that’s the kind of thing you only learn by reading actual responses. handle it in the library so your users never see it.

5. new HttpClient() per instance, or bring your own

the 2019 client news up its own HttpClient. in 2026 the library should accept one. that’s what makes it work with IHttpClientFactory, polly resilience pipelines, and unit tests with a fake HttpMessageHandler. ship a di package with services.AddSerpApi(...) and an ISerpApiClient interface, and testing a search feature no longer requires mocking http at all.

the payoff: a web-grounded agent in 30 lines

the reason i care about search apis in .net at all is ai agents. every llm’s knowledge stops at its training cutoff. search grounding fixes that, and everyone does it in python. with a modern client, the c# version is a Semantic Kernel plugin:

public sealed class SerpApiSearchPlugin(ISerpApiClient client)<br>[KernelFunction("search")]<br>[Description("Searches the web and returns the top results.")]<br>public async Task SearchAsync(string query, CancellationToken ct = default)<br>using var result = await client.SearchAsync(new SearchRequest { Query = query }, ct);<br>return string.Join("\n",...

client modern search task serpapi library

Related Articles