From 3be7e0b32553cc0355a4a51c2a47274ae12b1a03 Mon Sep 17 00:00:00 2001 From: Victor Benarbia Date: Mon, 10 Aug 2026 07:16:40 -0500 Subject: [PATCH 1/2] remove note that the package is not available --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index a611eca..90d06f8 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,6 @@ [![NuGet](https://img.shields.io/nuget/v/serpapi)](https://www.nuget.org/packages/serpapi) [![Build](https://github.com/serpapi/serpapi-dotnet/actions/workflows/ci.yml/badge.svg)](https://github.com/serpapi/serpapi-dotnet/actions/workflows/ci.yml) -> **Not yet published.** The `serpapi` package hasn't shipped its first NuGet release — the badge above will go green once v1.0.0 is out. Until then, build from source (see [Contributing](#contributing)) or reference the project directly. - Integrate search data into your AI workflow, RAG / fine-tuning, or .NET application using this official wrapper for [SerpApi](https://serpapi.com). SerpApi supports Google, Google Maps, Google Shopping, Bing, Baidu, Yandex, Yahoo, DuckDuckGo, eBay, Walmart, YouTube, App Stores, and [more](https://serpapi.com). From dee494266fdc6ef146daab87619c315f221dce4d Mon Sep 17 00:00:00 2001 From: Victor Benarbia Date: Sun, 16 Aug 2026 11:26:58 -0500 Subject: [PATCH 2/2] Add Markdown output support via output=md Adds MarkdownAsync/Markdown methods mirroring the existing Html/HtmlAsync pair, hitting /search with output=md so results can be fetched as token-efficient Markdown (useful for LLM/agent consumption). Bumps the package version to 1.1.0. Co-Authored-By: Claude Sonnet 5 --- README.md | 3 ++ serpapi/SerpApiClient.cs | 34 ++++++++++++++++++----- serpapi/serpapi.csproj | 2 +- test/IntegrationTests.cs | 21 ++++++++++++++ test/SerpApiClientTests.cs | 57 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 90d06f8..306d225 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,9 @@ Console.WriteLine(results["local_results"]); // search results as a raw HTML string string rawHtml = await client.HtmlAsync(parameters); + +// search results as token-efficient Markdown, optimized for LLMs and AI agents +string markdown = await client.MarkdownAsync(parameters); ``` → [SerpApi documentation](https://serpapi.com/search-api). diff --git a/serpapi/SerpApiClient.cs b/serpapi/SerpApiClient.cs index afaa67d..09aea42 100644 --- a/serpapi/SerpApiClient.cs +++ b/serpapi/SerpApiClient.cs @@ -85,7 +85,7 @@ public async Task SearchAsync( if (parameters is null) throw new ArgumentNullException(nameof(parameters)); - var url = BuildUrl("/search", parameters, outputJson: true); + var url = BuildUrl("/search", parameters, outputFormat: "json"); return await GetResponseAsync(url, cancellationToken).ConfigureAwait(false); } @@ -99,7 +99,21 @@ public async Task HtmlAsync( if (parameters is null) throw new ArgumentNullException(nameof(parameters)); - var url = BuildUrl("/search", parameters, outputJson: false); + var url = BuildUrl("/search", parameters, outputFormat: "html"); + return await GetStringAsync(url, cancellationToken).ConfigureAwait(false); + } + + /// + /// Execute a search and return results rendered as Markdown. + /// + public async Task MarkdownAsync( + Dictionary parameters, + CancellationToken cancellationToken = default) + { + if (parameters is null) + throw new ArgumentNullException(nameof(parameters)); + + var url = BuildUrl("/search", parameters, outputFormat: "md"); return await GetStringAsync(url, cancellationToken).ConfigureAwait(false); } @@ -115,7 +129,7 @@ public async Task SearchArchiveAsync( if (string.IsNullOrWhiteSpace(searchId)) throw new ArgumentException("searchId must not be empty.", nameof(searchId)); - var url = BuildUrl($"/searches/{Uri.EscapeDataString(searchId)}.json", new Dictionary(), outputJson: true); + var url = BuildUrl($"/searches/{Uri.EscapeDataString(searchId)}.json", new Dictionary(), outputFormat: "json"); return await GetResponseAsync(url, cancellationToken).ConfigureAwait(false); } @@ -124,7 +138,7 @@ public async Task SearchArchiveAsync( /// public async Task AccountAsync(CancellationToken cancellationToken = default) { - var url = BuildUrl("/account", new Dictionary(), outputJson: true); + var url = BuildUrl("/account", new Dictionary(), outputFormat: "json"); return await GetResponseAsync(url, cancellationToken).ConfigureAwait(false); } @@ -145,7 +159,7 @@ public async Task LocationAsync( ["limit"] = limit.ToString() }; - var url = BuildUrl("/locations.json", parameters, outputJson: true, includeOutput: false); + var url = BuildUrl("/locations.json", parameters, outputFormat: "json", includeOutput: false); var json = await GetStringAsync(url, cancellationToken).ConfigureAwait(false); try @@ -276,6 +290,12 @@ public SerpApiResponse Search(Dictionary parameters) public string Html(Dictionary parameters) => Task.Run(() => HtmlAsync(parameters)).GetAwaiter().GetResult(); + /// + /// Get Markdown results synchronously. + /// + public string Markdown(Dictionary parameters) + => Task.Run(() => MarkdownAsync(parameters)).GetAwaiter().GetResult(); + /// /// Get search archive synchronously. /// @@ -299,7 +319,7 @@ public JsonElement Location(string query, int limit = 5) private string BuildUrl( string endpoint, Dictionary parameters, - bool outputJson, + string outputFormat, bool includeOutput = true) { var queryParts = new List(); @@ -317,7 +337,7 @@ private string BuildUrl( // Add output format if (includeOutput) - queryParts.Add($"output={( outputJson ? "json" : "html" )}"); + queryParts.Add($"output={outputFormat}"); // Add source identifier queryParts.Add($"source={DefaultSource}"); diff --git a/serpapi/serpapi.csproj b/serpapi/serpapi.csproj index b682209..be8e7c4 100644 --- a/serpapi/serpapi.csproj +++ b/serpapi/serpapi.csproj @@ -3,7 +3,7 @@ netstandard2.0;net7.0;net8.0;net9.0;net10.0 SerpApi serpapi - 1.0.0 + 1.1.0 SerpApi SerpApi LLC MIT diff --git a/test/IntegrationTests.cs b/test/IntegrationTests.cs index 4479b7a..655abfa 100644 --- a/test/IntegrationTests.cs +++ b/test/IntegrationTests.cs @@ -360,6 +360,27 @@ public async Task Google_HtmlEndpoint_ReturnsHtml() } } +[Trait("Category", "Integration")] +public class MarkdownIntegrationTest +{ + [SkippableFact] + public async Task Google_MarkdownEndpoint_ReturnsMarkdown() + { + var apiKey = Environment.GetEnvironmentVariable("SERPAPI_KEY"); + Skip.If(string.IsNullOrEmpty(apiKey), "SERPAPI_KEY not set"); + + using var client = new SerpApiClient(apiKey!); + var markdown = await client.MarkdownAsync(new Dictionary + { + ["engine"] = "google", + ["q"] = "coffee", + ["no_cache"] = "true" + }); + + Assert.True(markdown.Length > 100); + } +} + [Trait("Category", "Integration")] public class PaginationNextPageTest { diff --git a/test/SerpApiClientTests.cs b/test/SerpApiClientTests.cs index ae40c84..ef494b5 100644 --- a/test/SerpApiClientTests.cs +++ b/test/SerpApiClientTests.cs @@ -251,6 +251,31 @@ public async Task HtmlAsync_ReturnsHtmlString() Assert.Equal(html, result); } + [Fact] + public async Task MarkdownAsync_ReturnsMarkdownString() + { + var markdown = "# Search results"; + var handler = new MockHttpHandler((request, _) => + { + Assert.Contains("output=md", request.RequestUri!.ToString()); + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(markdown) + }); + }); + + using var client = new SerpApiClient(new HttpClient(handler), + new SerpApiClientOptions { ApiKey = "key" }); + + var result = await client.MarkdownAsync(new Dictionary + { + ["engine"] = "google", + ["q"] = "test" + }); + + Assert.Equal(markdown, result); + } + [Fact] public async Task SearchArchiveAsync_BuildsCorrectUrl() { @@ -587,6 +612,17 @@ public async Task HtmlAsync_ThrowsOnNullParameters() Assert.Equal("parameters", exception.ParamName); } + [Fact] + public async Task MarkdownAsync_ThrowsOnNullParameters() + { + using var client = new SerpApiClient("key"); + + var exception = await Assert.ThrowsAsync( + () => client.MarkdownAsync(null!)); + + Assert.Equal("parameters", exception.ParamName); + } + [Fact] public void SearchPagesAsync_ThrowsOnNullParameters() { @@ -763,6 +799,27 @@ public void Html_SyncWorks() Assert.Contains("", result); } + [Fact] + public void Markdown_SyncWorks() + { + var handler = new MockHttpHandler((_, _) => + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("# results") + })); + + using var client = new SerpApiClient(new HttpClient(handler), + new SerpApiClientOptions { ApiKey = "key" }); + + var result = client.Markdown(new Dictionary + { + ["engine"] = "google", + ["q"] = "test" + }); + + Assert.Contains("results", result); + } + [Fact] public void SearchArchive_SyncWorks() {