Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -102,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).
Expand Down
34 changes: 27 additions & 7 deletions serpapi/SerpApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task<SerpApiResponse> 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);
}

Expand All @@ -99,7 +99,21 @@ public async Task<string> 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);
}

/// <summary>
/// Execute a search and return results rendered as Markdown.
/// </summary>
public async Task<string> MarkdownAsync(
Dictionary<string, string> 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);
}

Expand All @@ -115,7 +129,7 @@ public async Task<SerpApiResponse> 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<string, string>(), outputJson: true);
var url = BuildUrl($"/searches/{Uri.EscapeDataString(searchId)}.json", new Dictionary<string, string>(), outputFormat: "json");
return await GetResponseAsync(url, cancellationToken).ConfigureAwait(false);
}

Expand All @@ -124,7 +138,7 @@ public async Task<SerpApiResponse> SearchArchiveAsync(
/// </summary>
public async Task<SerpApiResponse> AccountAsync(CancellationToken cancellationToken = default)
{
var url = BuildUrl("/account", new Dictionary<string, string>(), outputJson: true);
var url = BuildUrl("/account", new Dictionary<string, string>(), outputFormat: "json");
return await GetResponseAsync(url, cancellationToken).ConfigureAwait(false);
}

Expand All @@ -145,7 +159,7 @@ public async Task<JsonElement> 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
Expand Down Expand Up @@ -276,6 +290,12 @@ public SerpApiResponse Search(Dictionary<string, string> parameters)
public string Html(Dictionary<string, string> parameters)
=> Task.Run(() => HtmlAsync(parameters)).GetAwaiter().GetResult();

/// <summary>
/// Get Markdown results synchronously.
/// </summary>
public string Markdown(Dictionary<string, string> parameters)
=> Task.Run(() => MarkdownAsync(parameters)).GetAwaiter().GetResult();

/// <summary>
/// Get search archive synchronously.
/// </summary>
Expand All @@ -299,7 +319,7 @@ public JsonElement Location(string query, int limit = 5)
private string BuildUrl(
string endpoint,
Dictionary<string, string> parameters,
bool outputJson,
string outputFormat,
bool includeOutput = true)
{
var queryParts = new List<string>();
Expand All @@ -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}");
Expand Down
2 changes: 1 addition & 1 deletion serpapi/serpapi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<TargetFrameworks>netstandard2.0;net7.0;net8.0;net9.0;net10.0</TargetFrameworks>
<RootNamespace>SerpApi</RootNamespace>
<PackageId>serpapi</PackageId>
<Version>1.0.0</Version>
<Version>1.1.0</Version>
<Authors>SerpApi</Authors>
<Company>SerpApi LLC</Company>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
21 changes: 21 additions & 0 deletions test/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
{
["engine"] = "google",
["q"] = "coffee",
["no_cache"] = "true"
});

Assert.True(markdown.Length > 100);
}
}

[Trait("Category", "Integration")]
public class PaginationNextPageTest
{
Expand Down
57 changes: 57 additions & 0 deletions test/SerpApiClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>
{
["engine"] = "google",
["q"] = "test"
});

Assert.Equal(markdown, result);
}

[Fact]
public async Task SearchArchiveAsync_BuildsCorrectUrl()
{
Expand Down Expand Up @@ -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<ArgumentNullException>(
() => client.MarkdownAsync(null!));

Assert.Equal("parameters", exception.ParamName);
}

[Fact]
public void SearchPagesAsync_ThrowsOnNullParameters()
{
Expand Down Expand Up @@ -763,6 +799,27 @@ public void Html_SyncWorks()
Assert.Contains("</body>", 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<string, string>
{
["engine"] = "google",
["q"] = "test"
});

Assert.Contains("results", result);
}

[Fact]
public void SearchArchive_SyncWorks()
{
Expand Down