From d01797926221e355beb7d7f489d87dce2d043254 Mon Sep 17 00:00:00 2001
From: Andrew Tarr
Date: Mon, 11 Aug 2025 19:08:12 -0500
Subject: [PATCH 1/7] Filling out the search option query models to be more
real and clean
---
.../GuardianApiClientTests.cs | 4 +-
.../GuardianClient/GuardianApiClient.cs | 92 +-
.../GuardianClient/GuardianApiOptions.cs | 10 -
.../GuardianClient/Models/BaseResponse.cs | 11 +-
.../GuardianClient/Models/ContentItem.cs | 20 +-
.../Models/ContentSearchResponse.cs | 22 +-
...nApiContentAdditionalInformationOptions.cs | 32 +
.../Search/GuardianApiContentDateOptions.cs | 25 +
.../Search/GuardianApiContentFilterOptions.cs | 61 +
.../Search/GuardianApiContentOrderOptions.cs | 17 +
.../Search/GuardianApiContentPageOptions.cs | 19 +
.../Search/GuardianApiContentSearchOptions.cs | 44 +
.../Options/Search/GuardianApiOrderBy.cs | 22 +
.../Options/Search/GuardianApiOrderDate.cs | 22 +
guardian-api-spec.yaml | 1148 -----------------
15 files changed, 341 insertions(+), 1208 deletions(-)
delete mode 100644 GuardianClient/GuardianClient/GuardianApiOptions.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentDateOptions.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiOrderBy.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiOrderDate.cs
delete mode 100644 guardian-api-spec.yaml
diff --git a/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs b/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs
index 357e7c0..fadc4d7 100644
--- a/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs
+++ b/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs
@@ -1,3 +1,5 @@
+using GuardianClient.Options.Search;
+
namespace GuardianClient.Tests;
using Shouldly;
@@ -45,7 +47,7 @@ public async Task GetItemAsyncSmokeTest()
var contentItem = searchResult.Results.First();
var itemId = contentItem.Id;
- var singleItemResult = await ApiClient.GetItemAsync(itemId, new GuardianApiOptions { ShowFields = ["body"] });
+ var singleItemResult = await ApiClient.GetItemAsync(itemId, new GuardianApiContentAdditionalInformationOptions { ShowFields = ["body"] });
singleItemResult.ShouldNotBeNull("GetItem result should not be null");
singleItemResult.Status.ShouldBe("ok", "API response status should be 'ok'");
diff --git a/GuardianClient/GuardianClient/GuardianApiClient.cs b/GuardianClient/GuardianClient/GuardianApiClient.cs
index 204b3ff..c070f05 100644
--- a/GuardianClient/GuardianClient/GuardianApiClient.cs
+++ b/GuardianClient/GuardianClient/GuardianApiClient.cs
@@ -1,6 +1,7 @@
using System.Reflection;
using System.Text.Json;
using GuardianClient.Models;
+using GuardianClient.Options.Search;
namespace GuardianClient;
@@ -72,6 +73,7 @@ private string GetPackageVersion()
return packageVersion;
}
+ // TODO comment out of date
///
/// Search for Guardian content
///
@@ -82,53 +84,53 @@ private string GetPackageVersion()
/// Cancellation token
/// Content search results
public async Task SearchAsync(
- string? query = null,
- int? pageSize = null,
- int? page = null,
- GuardianApiOptions? options = null,
- CancellationToken cancellationToken = default)
+ GuardianApiContentSearchOptions? options = null,
+ CancellationToken cancellationToken = default
+ )
{
- var parameters = new List { $"api-key={Uri.EscapeDataString(_apiKey)}" };
-
- if (!string.IsNullOrWhiteSpace(query))
- {
- parameters.Add($"q={Uri.EscapeDataString(query)}");
- }
-
- if (pageSize.HasValue)
- {
- parameters.Add($"page-size={pageSize.Value}");
- }
-
- if (page.HasValue)
- {
- parameters.Add($"page={page.Value}");
- }
+ options ??= new GuardianApiContentSearchOptions();
- if (options?.ShowFields?.Length > 0)
- {
- parameters.Add($"show-fields={string.Join(",", options.ShowFields)}");
- }
-
- if (options?.ShowTags?.Length > 0)
- {
- parameters.Add($"show-tags={string.Join(",", options.ShowTags)}");
- }
-
- if (options?.ShowElements?.Length > 0)
- {
- parameters.Add($"show-elements={string.Join(",", options.ShowElements)}");
- }
-
- if (options?.ShowReferences?.Length > 0)
- {
- parameters.Add($"show-references={string.Join(",", options.ShowReferences)}");
- }
+ var parameters = new List { $"api-key={Uri.EscapeDataString(_apiKey)}" };
- if (options?.ShowBlocks?.Length > 0)
- {
- parameters.Add($"show-blocks={string.Join(",", options.ShowBlocks)}");
- }
+ // if (!string.IsNullOrWhiteSpace(query))
+ // {
+ // parameters.Add($"q={Uri.EscapeDataString(query)}");
+ // }
+ //
+ // if (pageSize.HasValue)
+ // {
+ // parameters.Add($"page-size={pageSize.Value}");
+ // }
+ //
+ // if (page.HasValue)
+ // {
+ // parameters.Add($"page={page.Value}");
+ // }
+ //
+ // if (options?.ShowFields?.Length > 0)
+ // {
+ // parameters.Add($"show-fields={string.Join(",", options.ShowFields)}");
+ // }
+ //
+ // if (options?.ShowTags?.Length > 0)
+ // {
+ // parameters.Add($"show-tags={string.Join(",", options.ShowTags)}");
+ // }
+ //
+ // if (options?.ShowElements?.Length > 0)
+ // {
+ // parameters.Add($"show-elements={string.Join(",", options.ShowElements)}");
+ // }
+ //
+ // if (options?.ShowReferences?.Length > 0)
+ // {
+ // parameters.Add($"show-references={string.Join(",", options.ShowReferences)}");
+ // }
+ //
+ // if (options?.ShowBlocks?.Length > 0)
+ // {
+ // parameters.Add($"show-blocks={string.Join(",", options.ShowBlocks)}");
+ // }
var url = $"/search?{string.Join("&", parameters)}";
var response = await _httpClient.GetAsync(url, cancellationToken);
@@ -150,7 +152,7 @@ private string GetPackageVersion()
/// Single item response with content details
public async Task GetItemAsync(
string itemId,
- GuardianApiOptions? options = null,
+ GuardianApiContentAdditionalInformationOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(itemId);
diff --git a/GuardianClient/GuardianClient/GuardianApiOptions.cs b/GuardianClient/GuardianClient/GuardianApiOptions.cs
deleted file mode 100644
index 505cc6b..0000000
--- a/GuardianClient/GuardianClient/GuardianApiOptions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace GuardianClient;
-
-public class GuardianApiOptions
-{
- public string[]? ShowFields { get; set; }
- public string[]? ShowTags { get; set; }
- public string[]? ShowElements { get; set; }
- public string[]? ShowReferences { get; set; }
- public string[]? ShowBlocks { get; set; }
-}
diff --git a/GuardianClient/GuardianClient/Models/BaseResponse.cs b/GuardianClient/GuardianClient/Models/BaseResponse.cs
index 91a091b..2458949 100644
--- a/GuardianClient/GuardianClient/Models/BaseResponse.cs
+++ b/GuardianClient/GuardianClient/Models/BaseResponse.cs
@@ -4,12 +4,21 @@ namespace GuardianClient.Models;
public class BaseResponse
{
+ ///
+ /// The status of the response. It refers to the state of the API. Successful calls will receive an "ok" even if your query did not return any results.
+ ///
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
+ ///
+ /// The user tier for the API account.
+ ///
[JsonPropertyName("userTier")]
public string? UserTier { get; set; }
+ ///
+ /// The number of results available for your search overall.
+ ///
[JsonPropertyName("total")]
public int Total { get; set; }
-}
\ No newline at end of file
+}
diff --git a/GuardianClient/GuardianClient/Models/ContentItem.cs b/GuardianClient/GuardianClient/Models/ContentItem.cs
index c201d81..6c0aab8 100644
--- a/GuardianClient/GuardianClient/Models/ContentItem.cs
+++ b/GuardianClient/GuardianClient/Models/ContentItem.cs
@@ -4,27 +4,45 @@ namespace GuardianClient.Models;
public class ContentItem
{
+ ///
+ /// The path to content.
+ ///
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
+ ///
+ /// The id of the section.
+ ///
[JsonPropertyName("sectionId")]
public string? SectionId { get; set; }
+ ///
+ /// The name of the section.
+ ///
[JsonPropertyName("sectionName")]
public string? SectionName { get; set; }
+ ///
+ /// The combined date and time of publication.
+ ///
[JsonPropertyName("webPublicationDate")]
public DateTime? WebPublicationDate { get; set; }
[JsonPropertyName("webTitle")]
public string WebTitle { get; set; } = string.Empty;
+ ///
+ /// The URL of the html content.
+ ///
[JsonPropertyName("webUrl")]
public string WebUrl { get; set; } = string.Empty;
+ ///
+ /// The URL of the raw content.
+ ///
[JsonPropertyName("apiUrl")]
public string ApiUrl { get; set; } = string.Empty;
@@ -51,4 +69,4 @@ public class ContentItem
[JsonPropertyName("blocks")]
public ContentBlocks? Blocks { get; set; }
-}
\ No newline at end of file
+}
diff --git a/GuardianClient/GuardianClient/Models/ContentSearchResponse.cs b/GuardianClient/GuardianClient/Models/ContentSearchResponse.cs
index 45b4654..399ea7f 100644
--- a/GuardianClient/GuardianClient/Models/ContentSearchResponse.cs
+++ b/GuardianClient/GuardianClient/Models/ContentSearchResponse.cs
@@ -4,21 +4,39 @@ namespace GuardianClient.Models;
public class ContentSearchResponse : BaseResponse
{
+ ///
+ /// The starting index for the current result set.
+ ///
[JsonPropertyName("startIndex")]
public int StartIndex { get; set; }
+ ///
+ /// The number of items returned in this call.
+ ///
[JsonPropertyName("pageSize")]
public int PageSize { get; set; }
+ ///
+ /// The number of the page you are browsing.
+ ///
[JsonPropertyName("currentPage")]
public int CurrentPage { get; set; }
+ ///
+ /// The total amount of pages that are in this call.
+ ///
[JsonPropertyName("pages")]
public int Pages { get; set; }
+ ///
+ /// The sort order used.
+ ///
[JsonPropertyName("orderBy")]
public string? OrderBy { get; set; }
+ ///
+ /// The collection of content items returned by the search.
+ ///
[JsonPropertyName("results")]
- public List Results { get; set; } = new();
-}
\ No newline at end of file
+ public ICollection Results { get; set; } = new HashSet();
+}
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs
new file mode 100644
index 0000000..8f37c27
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs
@@ -0,0 +1,32 @@
+namespace GuardianClient.Options.Search;
+
+public class GuardianApiContentAdditionalInformationOptions
+{
+ ///
+ ///
+ ///
+ public string[]? ShowFields { get; set; }
+
+ ///
+ ///
+ ///
+ public string[]? ShowTags { get; set; }
+
+ ///
+ ///
+ ///
+ public string[]? ShowElements { get; set; }
+
+ ///
+ ///
+ ///
+ public string[]? ShowReferences { get; set; }
+
+ ///
+ ///
+ ///
+ public string[]? ShowBlocks { get; set; }
+}
+
+// TODO additional information table below shows all the options that are available in the Show* methods, so
+// we could strongly type those.
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentDateOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentDateOptions.cs
new file mode 100644
index 0000000..0fe11f5
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentDateOptions.cs
@@ -0,0 +1,25 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Options for filtering search results by date ranges.
+///
+public class GuardianApiContentDateOptions
+{
+ ///
+ /// Return only content published on or after that date.
+ /// Example: 2014-02-16.
+ ///
+ public DateOnly FromDate { get; set; }
+
+ ///
+ /// Return only content published on or before that date.
+ /// Example: 2014-02-17.
+ ///
+ public DateOnly ToDate { get; set; }
+
+ ///
+ /// Changes which type of date is used to filter the results using FromDate and ToDate.
+ /// Accepted values: "published" (default - the date the content has been last published), "first-publication" (the date the content has been first published), "newspaper-edition" (the date the content appeared in print), "last-modified" (the date the content was last updated).
+ ///
+ public string? UseDate { get; set; }
+}
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs
new file mode 100644
index 0000000..220b3e9
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs
@@ -0,0 +1,61 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Options for filtering content search results by various criteria.
+///
+public class GuardianApiContentFilterOptions
+{
+ ///
+ /// Return only content in those sections. Supports boolean operators.
+ /// Example: "football".
+ ///
+ public string? Section { get; set; }
+
+ ///
+ /// Return only content with those references. Supports boolean operators.
+ /// Example: "isbn/9780718178949".
+ ///
+ public string? Reference { get; set; }
+
+ ///
+ /// Return only content with references of those types. Supports boolean operators.
+ /// Example: "isbn".
+ ///
+ public string? ReferenceType { get; set; }
+
+ ///
+ /// Return only content with those tags. Supports boolean operators.
+ /// Example: "technology/apple".
+ ///
+ public string? Tag { get; set; }
+
+ ///
+ /// Return only content with those rights. Does not support boolean operators.
+ /// Accepted values: "syndicatable", "subscription-databases".
+ ///
+ public string? Rights { get; set; }
+
+ ///
+ /// Return only content with those IDs. Does not support boolean operators.
+ /// Example: "technology/2014/feb/17/flappy-bird-clones-apple-google".
+ ///
+ public string? Ids { get; set; }
+
+ ///
+ /// Return only content from those production offices. Supports boolean operators.
+ /// Example: "aus".
+ ///
+ public string? ProductionOffice { get; set; }
+
+ ///
+ /// Return only content in those languages. Supports boolean operators.
+ /// Accepts ISO language codes. Examples: "en", "fr".
+ ///
+ public string? Language { get; set; }
+
+ ///
+ /// Return only content with a given star rating. Does not support boolean operators.
+ /// Accepted values: 1 to 5.
+ ///
+ public int? StarRating { get; set; }
+}
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
new file mode 100644
index 0000000..09e960c
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
@@ -0,0 +1,17 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Options for controlling the ordering of search results.
+///
+public class GuardianApiContentOrderOptions
+{
+ ///
+ /// Returns results in the specified order. Defaults to Newest in most cases, or Relevance when a query parameter is specified.
+ ///
+ public GuardianApiOrderBy? OrderBy { get; set; }
+
+ ///
+ /// Changes which type of date is used to order the results. Defaults to Published.
+ ///
+ public GuardianApiOrderDate OrderDate { get; set; }
+}
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs
new file mode 100644
index 0000000..df86b78
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs
@@ -0,0 +1,19 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Options for controlling pagination of search results.
+///
+public class GuardianApiContentPageOptions
+{
+ ///
+ /// Return only the result set from a particular page.
+ /// Example: 5.
+ ///
+ public int Page { get; set; }
+
+ ///
+ /// Modify the number of items displayed per page.
+ /// Accepted values: 1 to 50.
+ ///
+ public int PageSize { get; set; }
+}
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs
new file mode 100644
index 0000000..49c7768
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs
@@ -0,0 +1,44 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Options for searching content using the Guardian API.
+///
+public class GuardianApiContentSearchOptions
+{
+ ///
+ /// Request content containing this free text. Supports AND, OR and NOT operators, and exact phrase queries using double quotes.
+ /// Examples: "sausages", "pork sausages", "sausages AND (mash OR chips)", "sausages AND NOT (saveloy OR battered)".
+ ///
+ public string? Query { get; set; }
+
+ ///
+ /// Specify in which indexed fields query terms should be searched on.
+ /// Examples: ["body"], ["body", "thumbnail"].
+ ///
+ public string[]? QueryFields { get; set; }
+
+ ///
+ /// Options for requesting additional information to be included with search results.
+ ///
+ public GuardianApiContentAdditionalInformationOptions AdditionalInformationOptions { get; set; } = new();
+
+ ///
+ /// Options for filtering search results by various criteria.
+ ///
+ public GuardianApiContentFilterOptions FilterOptions { get; set; } = new();
+
+ ///
+ /// Options for filtering search results by date ranges.
+ ///
+ public GuardianApiContentDateOptions DateOptions { get; set; } = new();
+
+ ///
+ /// Options for controlling pagination of search results.
+ ///
+ public GuardianApiContentPageOptions PageOptions { get; set; } = new();
+
+ ///
+ /// Options for controlling the ordering of search results.
+ ///
+ public GuardianApiContentOrderOptions OrderOptions { get; set; } = new();
+}
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderBy.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderBy.cs
new file mode 100644
index 0000000..f5ee5a8
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderBy.cs
@@ -0,0 +1,22 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Specifies the order in which search results should be returned.
+///
+public enum GuardianApiOrderBy
+{
+ ///
+ /// Order by newest content first. Default in all other cases.
+ ///
+ Newest,
+
+ ///
+ /// Order by oldest content first.
+ ///
+ Oldest,
+
+ ///
+ /// Order by relevance to search query. Default where q parameter is specified.
+ ///
+ Relevance
+}
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderDate.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderDate.cs
new file mode 100644
index 0000000..31d89e6
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderDate.cs
@@ -0,0 +1,22 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Specifies which type of date is used to order the results.
+///
+public enum GuardianApiOrderDate
+{
+ ///
+ /// The date the content appeared on the web. Default.
+ ///
+ Published,
+
+ ///
+ /// The date the content appeared in print.
+ ///
+ NewspaperEdition,
+
+ ///
+ /// The date the content was last updated.
+ ///
+ LastModified
+}
diff --git a/guardian-api-spec.yaml b/guardian-api-spec.yaml
deleted file mode 100644
index c92beec..0000000
--- a/guardian-api-spec.yaml
+++ /dev/null
@@ -1,1148 +0,0 @@
-openapi: 3.0.3
-info:
- title: Guardian Content API
- description: |
- The Guardian Content API provides access to Guardian news content, tags, sections, and editions.
-
- ## Authentication
- All requests require an API key passed as a query parameter: `api-key`
-
- ## Rate Limiting
- The API is rate-limited. For elevated limits, please contact The Guardian.
-
- ## HTTPS Support
- Available at https://content.guardianapis.com/
- version: "1.0.0"
- contact:
- name: Guardian API Support
- url: https://groups.google.com/group/guardian-api-talk/
- license:
- name: Guardian API Terms
- url: https://open-platform.theguardian.com/access/
-
-servers:
- - url: https://content.guardianapis.com
- description: Production server
-
-security:
- - ApiKeyAuth: []
-
-paths:
- /search:
- get:
- summary: Search for content
- description: Returns all pieces of content in the API with optional filtering
- operationId: searchContent
- tags:
- - Content
- parameters:
- - $ref: '#/components/parameters/ApiKey'
- - name: q
- in: query
- description: |
- Search query supporting AND, OR, NOT operators and exact phrase queries using double quotes.
- Examples: "debate", "debate AND economy", "debate AND NOT immigration", "debate AND (economy OR immigration)"
- schema:
- type: string
- example: "debate AND economy"
- - name: query-fields
- in: query
- description: Specify which indexed fields to search in
- schema:
- type: array
- items:
- type: string
- enum: [body, thumbnail]
- style: form
- explode: false
- example: ["body", "thumbnail"]
- - name: section
- in: query
- description: Return only content in specified sections (supports boolean operators)
- schema:
- type: string
- example: "football"
- - name: reference
- in: query
- description: Return only content with specified references
- schema:
- type: string
- example: "isbn/9780718178949"
- - name: reference-type
- in: query
- description: Return only content with references of specified types
- schema:
- type: string
- example: "isbn"
- - name: tag
- in: query
- description: Return only content with specified tags (supports boolean operators)
- schema:
- type: string
- example: "technology/apple"
- - name: rights
- in: query
- description: Return only content with specified rights
- schema:
- type: string
- enum: [syndicatable, subscription-databases]
- - name: ids
- in: query
- description: Return only content with specified IDs
- schema:
- type: string
- example: "technology/2014/feb/17/flappy-bird-clones-apple-google"
- - name: production-office
- in: query
- description: Return only content from specified production offices
- schema:
- type: string
- example: "aus"
- - name: lang
- in: query
- description: Return only content in specified languages (ISO language codes)
- schema:
- type: string
- example: "en"
- - name: star-rating
- in: query
- description: Return only content with specified star rating
- schema:
- type: integer
- minimum: 1
- maximum: 5
- - name: from-date
- in: query
- description: Return only content published on or after this date
- schema:
- type: string
- format: date
- example: "2014-02-16"
- - name: to-date
- in: query
- description: Return only content published on or before this date
- schema:
- type: string
- format: date
- example: "2014-02-17"
- - name: use-date
- in: query
- description: Type of date to use for filtering
- schema:
- type: string
- enum: [published, first-publication, newspaper-edition, last-modified]
- default: published
- - name: page
- in: query
- description: Page number for pagination
- schema:
- type: integer
- minimum: 1
- default: 1
- - name: page-size
- in: query
- description: Number of items per page
- schema:
- type: integer
- minimum: 1
- maximum: 50
- default: 10
- - name: order-by
- in: query
- description: Sort order for results
- schema:
- type: string
- enum: [newest, oldest, relevance]
- default: newest
- - name: order-date
- in: query
- description: Type of date to use for ordering
- schema:
- type: string
- enum: [published, newspaper-edition, last-modified]
- default: published
- - name: show-fields
- in: query
- description: Additional fields to include with content
- schema:
- type: array
- items:
- type: string
- enum: [
- trailText, headline, showInRelatedContent, body, lastModified,
- hasStoryPackage, score, standfirst, shortUrl, thumbnail, wordcount,
- commentable, isPremoderated, allowUgc, byline, publication,
- internalPageCode, productionOffice, shouldHideAdverts, liveBloggingNow,
- commentCloseDate, starRating, all
- ]
- style: form
- explode: false
- - name: show-tags
- in: query
- description: Types of tags to include
- schema:
- type: array
- items:
- type: string
- enum: [
- blog, contributor, keyword, newspaper-book, newspaper-book-section,
- publication, series, tone, type, all
- ]
- style: form
- explode: false
- - name: show-section
- in: query
- description: Include section metadata
- schema:
- type: boolean
- - name: show-blocks
- in: query
- description: |
- Include content blocks. Supports:
- - main, body, all
- - body:latest (default 20), body:latest:10 (limit 10)
- - body:oldest, body:oldest:10
- - body: (specific block)
- - body:around: (block + 20 around it)
- - body:around::10 (block + 10 around it)
- - body:key-events
- - body:published-since:
- schema:
- type: array
- items:
- type: string
- style: form
- explode: false
- - name: show-elements
- in: query
- description: Include media elements
- schema:
- type: array
- items:
- type: string
- enum: [audio, image, video, all]
- style: form
- explode: false
- - name: show-references
- in: query
- description: Include reference data
- schema:
- type: array
- items:
- type: string
- enum: [
- author, bisac-prefix, esa-cricket-match, esa-football-match,
- esa-football-team, esa-football-tournament, isbn, imdb, musicbrainz,
- musicbrainzgenre, opta-cricket-match, opta-football-match,
- opta-football-team, opta-football-tournament, pa-football-competition,
- pa-football-match, pa-football-team, r1-film, reuters-index-ric,
- reuters-stock-ric, witness-assignment
- ]
- style: form
- explode: false
- - name: show-rights
- in: query
- description: Include rights information
- schema:
- type: array
- items:
- type: string
- enum: [syndicatable, subscription-databases, all]
- style: form
- explode: false
- - name: format
- in: query
- description: Response format
- schema:
- type: string
- enum: [json]
- default: json
- responses:
- '200':
- description: Successful response
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ContentSearchResponse'
- '400':
- description: Bad request
- '401':
- description: Unauthorized - invalid API key
- '429':
- description: Rate limit exceeded
-
- /content/{itemId}/next:
- get:
- summary: Get next page of content for deep pagination
- description: |
- For deep pagination beyond standard page limits. Use the ID of the last content item
- from a previous search to get the next set of results.
- operationId: getNextContent
- tags:
- - Content
- parameters:
- - name: itemId
- in: path
- required: true
- description: ID of the last content item from previous results
- schema:
- type: string
- example: "food/2022/jul/04/peperonata-sausages-recipe-rachel-roddy"
- - $ref: '#/components/parameters/ApiKey'
- - name: q
- in: query
- description: Original search query (must match original search)
- schema:
- type: string
- - name: page-size
- in: query
- description: Number of items per page (must match original search)
- schema:
- type: integer
- minimum: 1
- maximum: 50
- default: 10
- - name: order-by
- in: query
- description: Sort order (must match original search)
- schema:
- type: string
- enum: [newest, oldest, relevance]
- responses:
- '200':
- description: Successful response
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/ContentSearchResponse'
- '400':
- description: Bad request
- '401':
- description: Unauthorized - invalid API key
- '429':
- description: Rate limit exceeded
-
- /tags:
- get:
- summary: Search for tags
- description: Returns all tags in the API with optional filtering
- operationId: searchTags
- tags:
- - Tags
- parameters:
- - $ref: '#/components/parameters/ApiKey'
- - name: q
- in: query
- description: Return tags containing this free text
- schema:
- type: string
- example: "sausages"
- - name: web-title
- in: query
- description: Return tags starting with this free text
- schema:
- type: string
- example: "sausa"
- - name: type
- in: query
- description: Return only tags of specified type
- schema:
- type: string
- enum: [keyword, series, contributor, tone, type, blog]
- - name: section
- in: query
- description: Return only tags in specified sections (supports boolean operators)
- schema:
- type: string
- example: "football"
- - name: reference
- in: query
- description: Return only tags with specified references
- schema:
- type: string
- example: "isbn/9780349108391"
- - name: reference-type
- in: query
- description: Return only tags with references of specified types
- schema:
- type: string
- example: "isbn"
- - name: page
- in: query
- description: Page number for pagination
- schema:
- type: integer
- minimum: 1
- default: 1
- - name: page-size
- in: query
- description: Number of items per page
- schema:
- type: integer
- minimum: 1
- maximum: 50
- default: 10
- - name: show-references
- in: query
- description: Include reference data
- schema:
- type: array
- items:
- type: string
- enum: [
- author, bisac-prefix, esa-cricket-match, esa-football-match,
- esa-football-team, esa-football-tournament, isbn, imdb, musicbrainz,
- musicbrainzgenre, opta-cricket-match, opta-football-match,
- opta-football-team, opta-football-tournament, pa-football-competition,
- pa-football-match, pa-football-team, r1-film, reuters-index-ric,
- reuters-stock-ric, witness-assignment
- ]
- style: form
- explode: false
- responses:
- '200':
- description: Successful response
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/TagsResponse'
- '400':
- description: Bad request
- '401':
- description: Unauthorized - invalid API key
- '429':
- description: Rate limit exceeded
-
- /sections:
- get:
- summary: Get all sections
- description: Returns all sections in the API
- operationId: getSections
- tags:
- - Sections
- parameters:
- - $ref: '#/components/parameters/ApiKey'
- - name: q
- in: query
- description: Return sections based on query term
- schema:
- type: string
- example: "business"
- responses:
- '200':
- description: Successful response
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SectionsResponse'
- '400':
- description: Bad request
- '401':
- description: Unauthorized - invalid API key
- '429':
- description: Rate limit exceeded
-
- /editions:
- get:
- summary: Get all editions
- description: Returns all editions in the API (regionalized front pages)
- operationId: getEditions
- tags:
- - Editions
- parameters:
- - $ref: '#/components/parameters/ApiKey'
- - name: q
- in: query
- description: Return editions based on query term
- schema:
- type: string
- example: "UK"
- responses:
- '200':
- description: Successful response
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/EditionsResponse'
- '400':
- description: Bad request
- '401':
- description: Unauthorized - invalid API key
- '429':
- description: Rate limit exceeded
-
- /{itemPath}:
- get:
- summary: Get single item by path
- description: |
- Returns all data for a single item (content, tag, or section) by its path.
- The path matches the URL structure on theguardian.com.
- operationId: getSingleItem
- tags:
- - Single Item
- parameters:
- - name: itemPath
- in: path
- required: true
- description: |
- The path to the item. Examples:
- - Content: technology/2014/feb/18/doge-such-questions-very-answered
- - Tag: world/france
- - Section: lifeandstyle
- schema:
- type: string
- example: "technology/2014/feb/18/doge-such-questions-very-answered"
- - $ref: '#/components/parameters/ApiKey'
- - name: show-story-package
- in: query
- description: Display related content in same story package
- schema:
- type: boolean
- - name: show-editors-picks
- in: query
- description: Display editor-chosen content for tags/sections
- schema:
- type: boolean
- - name: show-most-viewed
- in: query
- description: Display most viewed content
- schema:
- type: boolean
- - name: show-related
- in: query
- description: Display related content items
- schema:
- type: boolean
- - name: show-fields
- in: query
- description: Additional fields to include
- schema:
- type: array
- items:
- type: string
- enum: [
- trailText, headline, showInRelatedContent, body, lastModified,
- hasStoryPackage, score, standfirst, shortUrl, thumbnail, wordcount,
- commentable, isPremoderated, allowUgc, byline, publication,
- internalPageCode, productionOffice, shouldHideAdverts, liveBloggingNow,
- commentCloseDate, starRating, all
- ]
- style: form
- explode: false
- - name: show-tags
- in: query
- description: Types of tags to include
- schema:
- type: array
- items:
- type: string
- enum: [
- blog, contributor, keyword, newspaper-book, newspaper-book-section,
- publication, series, tone, type, all
- ]
- style: form
- explode: false
- - name: show-sections
- in: query
- description: Include section metadata
- schema:
- type: boolean
- - name: show-blocks
- in: query
- description: |
- Include content blocks. Supports:
- - main, body, all
- - body:latest (default 20), body:latest:10 (limit 10)
- - body:oldest, body:oldest:10
- - body: (specific block)
- - body:around: (block + 20 around it)
- - body:around::10 (block + 10 around it)
- - body:key-events
- - body:published-since:
- schema:
- type: array
- items:
- type: string
- style: form
- explode: false
- - name: show-elements
- in: query
- description: Include media elements
- schema:
- type: array
- items:
- type: string
- enum: [audio, image, video, all]
- style: form
- explode: false
- - name: show-references
- in: query
- description: Include reference data
- schema:
- type: array
- items:
- type: string
- enum: [
- author, bisac-prefix, esa-cricket-match, esa-football-match,
- esa-football-team, esa-football-tournament, isbn, imdb, musicbrainz,
- musicbrainzgenre, opta-cricket-match, opta-football-match,
- opta-football-team, opta-football-tournament, pa-football-competition,
- pa-football-match, pa-football-team, r1-film, reuters-index-ric,
- reuters-stock-ric, witness-assignment
- ]
- style: form
- explode: false
- responses:
- '200':
- description: Successful response
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/SingleItemResponse'
- '400':
- description: Bad request
- '401':
- description: Unauthorized - invalid API key
- '404':
- description: Item not found
- '429':
- description: Rate limit exceeded
-
-components:
- securitySchemes:
- ApiKeyAuth:
- type: apiKey
- in: query
- name: api-key
- description: API key for authentication
-
- parameters:
- ApiKey:
- name: api-key
- in: query
- required: true
- description: Your Guardian API key
- schema:
- type: string
- example: "test"
-
- schemas:
- BaseResponse:
- type: object
- properties:
- status:
- type: string
- description: API response status
- enum: [ok, error]
- example: "ok"
- userTier:
- type: string
- description: User's API tier
- example: "developer"
- total:
- type: integer
- description: Total number of results available
- example: 1
- required:
- - status
-
- ContentItem:
- type: object
- properties:
- id:
- type: string
- description: Unique identifier/path to the content
- example: "world/2022/oct/21/russia-ukraine-war-latest-what-we-know-on-day-240-of-the-invasion"
- type:
- type: string
- description: Type of content
- example: "article"
- sectionId:
- type: string
- description: ID of the section
- example: "world"
- sectionName:
- type: string
- description: Name of the section
- example: "World news"
- webPublicationDate:
- type: string
- format: date-time
- description: Date and time of publication
- example: "2022-10-21T14:06:14Z"
- webTitle:
- type: string
- description: Title of the content
- example: "Russia-Ukraine war latest: what we know on day 240 of the invasion"
- webUrl:
- type: string
- format: uri
- description: URL of the HTML content
- example: "https://www.theguardian.com/world/2022/oct/21/russia-ukraine-war-latest-what-we-know-on-day-240-of-the-invasion"
- apiUrl:
- type: string
- format: uri
- description: URL of the API content
- example: "https://content.guardianapis.com/world/2022/oct/21/russia-ukraine-war-latest-what-we-know-on-day-240-of-the-invasion"
- isHosted:
- type: boolean
- description: Whether the content is hosted
- example: false
- pillarId:
- type: string
- description: ID of the pillar
- example: "pillar/news"
- pillarName:
- type: string
- description: Name of the pillar
- example: "News"
- fields:
- $ref: '#/components/schemas/ContentFields'
- tags:
- type: array
- items:
- $ref: '#/components/schemas/Tag'
- elements:
- type: array
- items:
- $ref: '#/components/schemas/Element'
- references:
- type: array
- items:
- $ref: '#/components/schemas/Reference'
- blocks:
- $ref: '#/components/schemas/ContentBlocks'
- required:
- - id
- - type
- - webTitle
- - webUrl
- - apiUrl
-
- ContentFields:
- type: object
- description: Additional fields for content (when requested via show-fields)
- properties:
- trailText:
- type: string
- description: Trail text HTML
- headline:
- type: string
- description: Headline HTML
- showInRelatedContent:
- type: string
- description: Whether content can appear in related content (boolean as string)
- body:
- type: string
- description: Full body content HTML
- lastModified:
- type: string
- format: date-time
- description: Last modification date
- hasStoryPackage:
- type: string
- description: Whether content has related story package (boolean as string)
- score:
- type: string
- description: Relevance score (float as string)
- standfirst:
- type: string
- description: Standfirst HTML
- shortUrl:
- type: string
- format: uri
- description: Short URL
- thumbnail:
- type: string
- format: uri
- description: Thumbnail image URL
- wordcount:
- type: string
- description: Word count (integer as string)
- commentable:
- type: string
- description: Whether comments are allowed (boolean as string)
- isPremoderated:
- type: string
- description: Whether comments are premoderated (boolean as string)
- allowUgc:
- type: string
- description: Whether user generated content is allowed (boolean as string)
- byline:
- type: string
- description: Byline HTML
- publication:
- type: string
- description: Publication name
- internalPageCode:
- type: string
- description: Internal page code
- productionOffice:
- type: string
- description: Production office
- shouldHideAdverts:
- type: string
- description: Whether to hide adverts (boolean as string)
- liveBloggingNow:
- type: string
- description: Whether currently live blogging (boolean as string)
- commentCloseDate:
- type: string
- format: date-time
- description: Date when comments were closed
- starRating:
- type: string
- description: Star rating (integer as string)
-
- ContentSearchResponse:
- allOf:
- - $ref: '#/components/schemas/BaseResponse'
- - type: object
- properties:
- startIndex:
- type: integer
- description: Starting index of results
- example: 1
- pageSize:
- type: integer
- description: Number of items per page
- example: 10
- currentPage:
- type: integer
- description: Current page number
- example: 1
- pages:
- type: integer
- description: Total number of pages
- example: 1
- orderBy:
- type: string
- description: Sort order used
- example: "newest"
- results:
- type: array
- items:
- $ref: '#/components/schemas/ContentItem'
- required:
- - results
-
- SingleItemResponse:
- allOf:
- - $ref: '#/components/schemas/BaseResponse'
- - type: object
- properties:
- content:
- $ref: '#/components/schemas/ContentItem'
- leadContent:
- type: array
- description: Key pieces of lead content for tags
- items:
- $ref: '#/components/schemas/ContentItem'
- storyPackage:
- type: array
- description: Related content in same story
- items:
- $ref: '#/components/schemas/ContentItem'
- editorsPicks:
- type: array
- description: Editor-chosen content
- items:
- $ref: '#/components/schemas/ContentItem'
- mostViewed:
- type: array
- description: Most viewed content
- items:
- $ref: '#/components/schemas/ContentItem'
- relatedContent:
- type: array
- description: Related content items
- items:
- $ref: '#/components/schemas/ContentItem'
-
- Tag:
- type: object
- properties:
- id:
- type: string
- description: Tag identifier
- example: "katine/football"
- type:
- type: string
- description: Type of tag
- enum: [keyword, series, contributor, tone, type, blog]
- example: "keyword"
- webTitle:
- type: string
- description: Web title of the tag
- example: "Football"
- webUrl:
- type: string
- format: uri
- description: Web URL of the tag
- example: "http://www.theguardian.com/katine/football"
- apiUrl:
- type: string
- format: uri
- description: API URL of the tag
- example: "http://beta.content.guardianapis.com/katine/football"
- sectionId:
- type: string
- description: Section ID of the tag
- example: "katine"
- sectionName:
- type: string
- description: Section name of the tag
- example: "Katine"
- references:
- type: array
- items:
- $ref: '#/components/schemas/Reference'
- required:
- - id
- - type
- - webTitle
-
- TagsResponse:
- allOf:
- - $ref: '#/components/schemas/BaseResponse'
- - type: object
- properties:
- startIndex:
- type: integer
- description: Starting index of results
- example: 1
- pageSize:
- type: integer
- description: Number of items per page
- example: 10
- currentPage:
- type: integer
- description: Current page number
- example: 1
- pages:
- type: integer
- description: Total number of pages
- example: 7
- results:
- type: array
- items:
- $ref: '#/components/schemas/Tag'
- required:
- - results
-
- Section:
- type: object
- properties:
- id:
- type: string
- description: Section identifier
- example: "football"
- webTitle:
- type: string
- description: Web title of the section
- example: "Football"
- webUrl:
- type: string
- format: uri
- description: Web URL of the section
- example: "https://www.theguardian.com/football"
- apiUrl:
- type: string
- format: uri
- description: API URL of the section
- example: "https://content.guardianapis.com/football"
- editions:
- type: array
- items:
- $ref: '#/components/schemas/Edition'
- required:
- - id
- - webTitle
-
- SectionsResponse:
- allOf:
- - $ref: '#/components/schemas/BaseResponse'
- - type: object
- properties:
- results:
- type: array
- items:
- $ref: '#/components/schemas/Section'
- required:
- - results
-
- Edition:
- type: object
- properties:
- id:
- type: string
- description: Edition identifier
- example: "au"
- path:
- type: string
- description: Path of the edition
- example: "au"
- edition:
- type: string
- description: Edition name
- example: "AU"
- webTitle:
- type: string
- description: Web title of the edition
- example: "new guardian australia front page"
- webUrl:
- type: string
- format: uri
- description: Web URL of the edition
- example: "https://www.theguardian.com/au"
- apiUrl:
- type: string
- format: uri
- description: API URL of the edition
- example: "https://content.guardianapis.com/au"
- code:
- type: string
- description: Edition code
- example: "default"
- required:
- - id
- - edition
-
- EditionsResponse:
- allOf:
- - $ref: '#/components/schemas/BaseResponse'
- - type: object
- properties:
- results:
- type: array
- items:
- $ref: '#/components/schemas/Edition'
- required:
- - results
-
- Element:
- type: object
- description: Media element (image, video, audio)
- properties:
- id:
- type: string
- description: Element identifier
- relation:
- type: string
- description: Relationship to content
- type:
- type: string
- enum: [image, video, audio]
- description: Type of element
- assets:
- type: array
- items:
- $ref: '#/components/schemas/Asset'
-
- Asset:
- type: object
- description: Media asset within an element
- properties:
- type:
- type: string
- description: Asset type
- mimeType:
- type: string
- description: MIME type of the asset
- file:
- type: string
- format: uri
- description: URL of the asset file
- typeData:
- type: object
- description: Additional type-specific data
-
- Reference:
- type: object
- description: Reference data (ISBN, IMDB, etc.)
- properties:
- type:
- type: string
- description: Type of reference
- enum: [
- author, bisac-prefix, esa-cricket-match, esa-football-match,
- esa-football-team, esa-football-tournament, isbn, imdb, musicbrainz,
- musicbrainzgenre, opta-cricket-match, opta-football-match,
- opta-football-team, opta-football-tournament, pa-football-competition,
- pa-football-match, pa-football-team, r1-film, reuters-index-ric,
- reuters-stock-ric, witness-assignment
- ]
- id:
- type: string
- description: Reference identifier
-
- ContentBlocks:
- type: object
- description: Content blocks for articles and live blogs
- properties:
- main:
- $ref: '#/components/schemas/Block'
- body:
- type: array
- items:
- $ref: '#/components/schemas/Block'
-
- Block:
- type: object
- description: Individual content block
- properties:
- id:
- type: string
- description: Block identifier
- bodyHtml:
- type: string
- description: HTML content of the block
- bodyTextSummary:
- type: string
- description: Text summary of the block
- title:
- type: string
- description: Block title
- attributes:
- type: object
- description: Block attributes
- published:
- type: boolean
- description: Whether the block is published
- createdDate:
- type: string
- format: date-time
- description: Block creation date
- firstPublishedDate:
- type: string
- format: date-time
- description: First publication date
- publishedDate:
- type: string
- format: date-time
- description: Publication date
- lastModifiedDate:
- type: string
- format: date-time
- description: Last modification date
- contributors:
- type: array
- items:
- type: string
- description: Block contributors
- elements:
- type: array
- items:
- $ref: '#/components/schemas/Element'
-
-tags:
- - name: Content
- description: Search and retrieve Guardian content
- - name: Tags
- description: Manage content tags and categories
- - name: Sections
- description: Browse content sections
- - name: Editions
- description: Access regional editions
- - name: Single Item
- description: Retrieve individual items by path
\ No newline at end of file
From 46e808012cd02f9b10320d6a5a7b8f0582390651 Mon Sep 17 00:00:00 2001
From: Andrew Tarr
Date: Mon, 11 Aug 2025 19:42:03 -0500
Subject: [PATCH 2/7] Cleaning up the client quite a bit
---
.../GuardianClient/GuardianApiClient.cs | 136 ++++++------------
.../GuardianClient/Internal/AssemblyInfo.cs | 16 +++
.../Internal/UrlParameterBuilder.cs | 117 +++++++++++++++
.../Search/GuardianApiContentOrderOptions.cs | 2 +-
.../Search/GuardianApiContentSearchOptions.cs | 3 +-
5 files changed, 179 insertions(+), 95 deletions(-)
create mode 100644 GuardianClient/GuardianClient/Internal/AssemblyInfo.cs
create mode 100644 GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs
diff --git a/GuardianClient/GuardianClient/GuardianApiClient.cs b/GuardianClient/GuardianClient/GuardianApiClient.cs
index c070f05..0ef4181 100644
--- a/GuardianClient/GuardianClient/GuardianApiClient.cs
+++ b/GuardianClient/GuardianClient/GuardianApiClient.cs
@@ -1,5 +1,5 @@
-using System.Reflection;
using System.Text.Json;
+using GuardianClient.Internal;
using GuardianClient.Models;
using GuardianClient.Options.Search;
@@ -8,11 +8,8 @@ namespace GuardianClient;
public class GuardianApiClient : IDisposable
{
private readonly HttpClient _httpClient;
-
private readonly string _apiKey;
-
private readonly bool _ownsHttpClient;
-
private bool _disposed;
private const string BaseUrl = "https://content.guardianapis.com";
@@ -55,34 +52,31 @@ public GuardianApiClient(string apiKey)
ConfigureHttpClient();
}
- private void ConfigureHttpClient()
- {
- var packageVersion = GetPackageVersion();
-
- _httpClient.BaseAddress = new Uri(BaseUrl);
- _httpClient.DefaultRequestHeaders.Add("User-Agent", $"GuardianClient.NET/{packageVersion}");
- }
-
- private string GetPackageVersion()
- {
- var packageVersion = Assembly
- .GetExecutingAssembly()
- .GetCustomAttribute()!
- .InformationalVersion;
-
- return packageVersion;
- }
-
- // TODO comment out of date
///
- /// Search for Guardian content
+ /// Search for Guardian content using comprehensive search options.
///
- /// Search query (supports AND, OR, NOT operators)
- /// Number of results per page (1-50)
- /// Page number for pagination
- /// API options for including additional response data
- /// Cancellation token
- /// Content search results
+ ///
+ /// Search options including query terms, filters, pagination, ordering, date ranges, and additional information to include.
+ /// If null, returns all content with default settings (newest first, page 1, 10 items per page).
+ ///
+ /// Cancellation token to cancel the HTTP request
+ ///
+ /// A containing the search results, pagination info, and metadata.
+ /// Returns null if the API response cannot be deserialized.
+ ///
+ /// Thrown when the API request fails or returns a non-success status code
+ /// Thrown when the request is cancelled via the cancellation token
+ ///
+ /// This method provides access to the full Guardian Content API search functionality, including:
+ ///
+ /// - Free text search with boolean operators (AND, OR, NOT)
+ /// - Filtering by section, tags, references, production office, language, star rating, and more
+ /// - Date range filtering with different date types (published, first-publication, newspaper-edition, last-modified)
+ /// - Pagination and result ordering options
+ /// - Additional fields, tags, elements, references, and blocks in responses
+ ///
+ /// For simple searches, you can create basic options: new GuardianApiContentSearchOptions { Query = "your search terms" }
+ ///
public async Task SearchAsync(
GuardianApiContentSearchOptions? options = null,
CancellationToken cancellationToken = default
@@ -92,45 +86,12 @@ private string GetPackageVersion()
var parameters = new List { $"api-key={Uri.EscapeDataString(_apiKey)}" };
- // if (!string.IsNullOrWhiteSpace(query))
- // {
- // parameters.Add($"q={Uri.EscapeDataString(query)}");
- // }
- //
- // if (pageSize.HasValue)
- // {
- // parameters.Add($"page-size={pageSize.Value}");
- // }
- //
- // if (page.HasValue)
- // {
- // parameters.Add($"page={page.Value}");
- // }
- //
- // if (options?.ShowFields?.Length > 0)
- // {
- // parameters.Add($"show-fields={string.Join(",", options.ShowFields)}");
- // }
- //
- // if (options?.ShowTags?.Length > 0)
- // {
- // parameters.Add($"show-tags={string.Join(",", options.ShowTags)}");
- // }
- //
- // if (options?.ShowElements?.Length > 0)
- // {
- // parameters.Add($"show-elements={string.Join(",", options.ShowElements)}");
- // }
- //
- // if (options?.ShowReferences?.Length > 0)
- // {
- // parameters.Add($"show-references={string.Join(",", options.ShowReferences)}");
- // }
- //
- // if (options?.ShowBlocks?.Length > 0)
- // {
- // parameters.Add($"show-blocks={string.Join(",", options.ShowBlocks)}");
- // }
+ UrlParameterBuilder.AddQueryParameters(options, parameters);
+ UrlParameterBuilder.AddFilterParameters(options.FilterOptions, parameters);
+ UrlParameterBuilder.AddDateParameters(options.DateOptions, parameters);
+ UrlParameterBuilder.AddPageParameters(options.PageOptions, parameters);
+ UrlParameterBuilder.AddOrderParameters(options.OrderOptions, parameters);
+ UrlParameterBuilder.AddAdditionalInformationParameters(options.AdditionalInformationOptions, parameters);
var url = $"/search?{string.Join("&", parameters)}";
var response = await _httpClient.GetAsync(url, cancellationToken);
@@ -159,30 +120,11 @@ private string GetPackageVersion()
var parameters = new List { $"api-key={Uri.EscapeDataString(_apiKey)}" };
- if (options?.ShowFields?.Length > 0)
- {
- parameters.Add($"show-fields={string.Join(",", options.ShowFields)}");
- }
-
- if (options?.ShowTags?.Length > 0)
- {
- parameters.Add($"show-tags={string.Join(",", options.ShowTags)}");
- }
-
- if (options?.ShowElements?.Length > 0)
- {
- parameters.Add($"show-elements={string.Join(",", options.ShowElements)}");
- }
-
- if (options?.ShowReferences?.Length > 0)
- {
- parameters.Add($"show-references={string.Join(",", options.ShowReferences)}");
- }
-
- if (options?.ShowBlocks?.Length > 0)
- {
- parameters.Add($"show-blocks={string.Join(",", options.ShowBlocks)}");
- }
+ UrlParameterBuilder.AddParameterIfAny(parameters, "show-fields", options?.ShowFields);
+ UrlParameterBuilder.AddParameterIfAny(parameters, "show-tags", options?.ShowTags);
+ UrlParameterBuilder.AddParameterIfAny(parameters, "show-elements", options?.ShowElements);
+ UrlParameterBuilder.AddParameterIfAny(parameters, "show-references", options?.ShowReferences);
+ UrlParameterBuilder.AddParameterIfAny(parameters, "show-blocks", options?.ShowBlocks);
var url = $"/{itemId}?{string.Join("&", parameters)}";
var response = await _httpClient.GetAsync(url, cancellationToken);
@@ -201,6 +143,14 @@ public void Dispose()
GC.SuppressFinalize(this);
}
+ private void ConfigureHttpClient()
+ {
+ var packageVersion = AssemblyInfo.GetPackageVersion();
+
+ _httpClient.BaseAddress = new Uri(BaseUrl);
+ _httpClient.DefaultRequestHeaders.Add("User-Agent", $"GuardianClient.NET/{packageVersion}");
+ }
+
///
/// Disposes the if:
/// 1. This instance owns it, and
diff --git a/GuardianClient/GuardianClient/Internal/AssemblyInfo.cs b/GuardianClient/GuardianClient/Internal/AssemblyInfo.cs
new file mode 100644
index 0000000..bbb6fe7
--- /dev/null
+++ b/GuardianClient/GuardianClient/Internal/AssemblyInfo.cs
@@ -0,0 +1,16 @@
+using System.Reflection;
+
+namespace GuardianClient.Internal;
+
+internal static class AssemblyInfo
+{
+ internal static string GetPackageVersion()
+ {
+ var packageVersion = Assembly
+ .GetExecutingAssembly()
+ .GetCustomAttribute()!
+ .InformationalVersion;
+
+ return packageVersion;
+ }
+}
\ No newline at end of file
diff --git a/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs b/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs
new file mode 100644
index 0000000..f7ad8ac
--- /dev/null
+++ b/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs
@@ -0,0 +1,117 @@
+using GuardianClient.Options.Search;
+
+namespace GuardianClient.Internal;
+
+internal static class UrlParameterBuilder
+{
+ internal static void AddParameterIfNotEmpty(List parameters, string parameterName, string? value)
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ parameters.Add($"{parameterName}={Uri.EscapeDataString(value)}");
+ }
+ }
+
+ internal static void AddParameterIfHasValue(List parameters, string parameterName, T? value) where T : struct
+ {
+ if (value.HasValue)
+ {
+ parameters.Add($"{parameterName}={value.Value}");
+ }
+ }
+
+ internal static void AddParameterIfAny(List parameters, string parameterName, string[]? values)
+ {
+ if (values is { Length: > 0 })
+ {
+ parameters.Add($"{parameterName}={string.Join(",", values.Select(Uri.EscapeDataString))}");
+ }
+ }
+
+ internal static void AddQueryParameters(GuardianApiContentSearchOptions options, List parameters)
+ {
+ AddParameterIfNotEmpty(parameters, "q", options.Query);
+ AddParameterIfAny(parameters, "query-fields", options.QueryFields);
+ }
+
+ internal static void AddFilterParameters(GuardianApiContentFilterOptions filterOptions, List parameters)
+ {
+ AddParameterIfNotEmpty(parameters, "section", filterOptions.Section);
+ AddParameterIfNotEmpty(parameters, "reference", filterOptions.Reference);
+ AddParameterIfNotEmpty(parameters, "reference-type", filterOptions.ReferenceType);
+ AddParameterIfNotEmpty(parameters, "tag", filterOptions.Tag);
+ AddParameterIfNotEmpty(parameters, "rights", filterOptions.Rights);
+ AddParameterIfNotEmpty(parameters, "ids", filterOptions.Ids);
+ AddParameterIfNotEmpty(parameters, "production-office", filterOptions.ProductionOffice);
+ AddParameterIfNotEmpty(parameters, "lang", filterOptions.Language);
+
+ AddParameterIfHasValue(parameters, "star-rating", filterOptions.StarRating);
+ }
+
+ internal static void AddDateParameters(GuardianApiContentDateOptions dateOptions, List parameters)
+ {
+ if (dateOptions.FromDate != default)
+ {
+ parameters.Add($"from-date={dateOptions.FromDate:yyyy-MM-dd}");
+ }
+
+ if (dateOptions.ToDate != default)
+ {
+ parameters.Add($"to-date={dateOptions.ToDate:yyyy-MM-dd}");
+ }
+
+ AddParameterIfNotEmpty(parameters, "use-date", dateOptions.UseDate);
+ }
+
+ internal static void AddPageParameters(GuardianApiContentPageOptions pageOptions, List parameters)
+ {
+ if (pageOptions.Page > 0)
+ {
+ parameters.Add($"page={pageOptions.Page}");
+ }
+
+ if (pageOptions.PageSize > 0)
+ {
+ parameters.Add($"page-size={pageOptions.PageSize}");
+ }
+ }
+
+ internal static void AddOrderParameters(GuardianApiContentOrderOptions orderOptions, List parameters)
+ {
+ if (orderOptions.OrderBy.HasValue)
+ {
+ var orderByValue = orderOptions.OrderBy.Value switch
+ {
+ GuardianApiOrderBy.Newest => "newest",
+ GuardianApiOrderBy.Oldest => "oldest",
+ GuardianApiOrderBy.Relevance => "relevance",
+ _ => "newest"
+ };
+ parameters.Add($"order-by={orderByValue}");
+ }
+
+ if (orderOptions.OrderDate.HasValue)
+ {
+ var orderDateValue = orderOptions.OrderDate.Value switch
+ {
+ GuardianApiOrderDate.Published => "published",
+ GuardianApiOrderDate.NewspaperEdition => "newspaper-edition",
+ GuardianApiOrderDate.LastModified => "last-modified",
+ _ => "published"
+ };
+ parameters.Add($"order-date={orderDateValue}");
+ }
+ }
+
+ internal static void AddAdditionalInformationParameters(
+ GuardianApiContentAdditionalInformationOptions additionalOptions,
+ List parameters
+ )
+ {
+ AddParameterIfAny(parameters, "show-fields", additionalOptions.ShowFields);
+ AddParameterIfAny(parameters, "show-tags", additionalOptions.ShowTags);
+ AddParameterIfAny(parameters, "show-elements", additionalOptions.ShowElements);
+ AddParameterIfAny(parameters, "show-references", additionalOptions.ShowReferences);
+ AddParameterIfAny(parameters, "show-blocks", additionalOptions.ShowBlocks);
+ }
+}
\ No newline at end of file
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
index 09e960c..584fc64 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
@@ -13,5 +13,5 @@ public class GuardianApiContentOrderOptions
///
/// Changes which type of date is used to order the results. Defaults to Published.
///
- public GuardianApiOrderDate OrderDate { get; set; }
+ public GuardianApiOrderDate? OrderDate { get; set; }
}
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs
index 49c7768..4892bbc 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs
@@ -3,7 +3,8 @@ namespace GuardianClient.Options.Search;
///
/// Options for searching content using the Guardian API.
///
-public class GuardianApiContentSearchOptions
+public class
+ GuardianApiContentSearchOptions
{
///
/// Request content containing this free text. Supports AND, OR and NOT operators, and exact phrase queries using double quotes.
From dea7d46ead8d1b99adeb39e62306d073567c449e Mon Sep 17 00:00:00 2001
From: Andrew Tarr
Date: Mon, 11 Aug 2025 19:49:03 -0500
Subject: [PATCH 3/7] Throw the client behind an interface to help people out
with DI
---
.../GuardianClient/GuardianApiClient.cs | 34 +-----------
.../GuardianClient/IGuardianApiClient.cs | 52 +++++++++++++++++++
.../Search/GuardianApiContentFilterOptions.cs | 3 ++
.../Search/GuardianApiContentOrderOptions.cs | 3 ++
.../Search/GuardianApiContentPageOptions.cs | 3 ++
.../Search/GuardianApiContentSearchOptions.cs | 6 ++-
6 files changed, 66 insertions(+), 35 deletions(-)
create mode 100644 GuardianClient/GuardianClient/IGuardianApiClient.cs
diff --git a/GuardianClient/GuardianClient/GuardianApiClient.cs b/GuardianClient/GuardianClient/GuardianApiClient.cs
index 0ef4181..b845290 100644
--- a/GuardianClient/GuardianClient/GuardianApiClient.cs
+++ b/GuardianClient/GuardianClient/GuardianApiClient.cs
@@ -5,7 +5,7 @@
namespace GuardianClient;
-public class GuardianApiClient : IDisposable
+public class GuardianApiClient : IGuardianApiClient, IDisposable
{
private readonly HttpClient _httpClient;
private readonly string _apiKey;
@@ -52,31 +52,6 @@ public GuardianApiClient(string apiKey)
ConfigureHttpClient();
}
- ///
- /// Search for Guardian content using comprehensive search options.
- ///
- ///
- /// Search options including query terms, filters, pagination, ordering, date ranges, and additional information to include.
- /// If null, returns all content with default settings (newest first, page 1, 10 items per page).
- ///
- /// Cancellation token to cancel the HTTP request
- ///
- /// A containing the search results, pagination info, and metadata.
- /// Returns null if the API response cannot be deserialized.
- ///
- /// Thrown when the API request fails or returns a non-success status code
- /// Thrown when the request is cancelled via the cancellation token
- ///
- /// This method provides access to the full Guardian Content API search functionality, including:
- ///
- /// - Free text search with boolean operators (AND, OR, NOT)
- /// - Filtering by section, tags, references, production office, language, star rating, and more
- /// - Date range filtering with different date types (published, first-publication, newspaper-edition, last-modified)
- /// - Pagination and result ordering options
- /// - Additional fields, tags, elements, references, and blocks in responses
- ///
- /// For simple searches, you can create basic options: new GuardianApiContentSearchOptions { Query = "your search terms" }
- ///
public async Task SearchAsync(
GuardianApiContentSearchOptions? options = null,
CancellationToken cancellationToken = default
@@ -104,13 +79,6 @@ public GuardianApiClient(string apiKey)
return wrapper?.Response;
}
- ///
- /// Get a single content item by its ID/path
- ///
- /// The content item ID (path from Guardian API)
- /// API options for including additional response data
- /// Cancellation token
- /// Single item response with content details
public async Task GetItemAsync(
string itemId,
GuardianApiContentAdditionalInformationOptions? options = null,
diff --git a/GuardianClient/GuardianClient/IGuardianApiClient.cs b/GuardianClient/GuardianClient/IGuardianApiClient.cs
new file mode 100644
index 0000000..db1787f
--- /dev/null
+++ b/GuardianClient/GuardianClient/IGuardianApiClient.cs
@@ -0,0 +1,52 @@
+using GuardianClient.Models;
+using GuardianClient.Options.Search;
+
+namespace GuardianClient;
+
+///
+/// Interface for the Guardian API client, providing access to Guardian content search and retrieval.
+///
+public interface IGuardianApiClient
+{
+ ///
+ /// Search for Guardian content using comprehensive search options.
+ ///
+ ///
+ /// Search options including query terms, filters, pagination, ordering, date ranges, and additional information to include.
+ /// If null, returns all content with default settings (newest first, page 1, 10 items per page).
+ ///
+ /// Cancellation token to cancel the HTTP request
+ ///
+ /// A containing the search results, pagination info, and metadata.
+ /// Returns null if the API response cannot be deserialized.
+ ///
+ /// Thrown when the API request fails or returns a non-success status code
+ /// Thrown when the request is cancelled via the cancellation token
+ ///
+ /// This method provides access to the full Guardian Content API search functionality, including:
+ ///
+ /// - Free text search with boolean operators (AND, OR, NOT)
+ /// - Filtering by section, tags, references, production office, language, star rating, and more
+ /// - Date range filtering with different date types (published, first-publication, newspaper-edition, last-modified)
+ /// - Pagination and result ordering options
+ /// - Additional fields, tags, elements, references, and blocks in responses
+ ///
+ /// For simple searches, you can create basic options: new GuardianApiContentSearchOptions { Query = "your search terms" }
+ ///
+ Task SearchAsync(
+ GuardianApiContentSearchOptions? options = null,
+ CancellationToken cancellationToken = default
+ );
+
+ ///
+ /// Get a single content item by its ID/path
+ ///
+ /// The content item ID (path from Guardian API)
+ /// API options for including additional response data
+ /// Cancellation token
+ /// Single item response with content details
+ Task GetItemAsync(
+ string itemId,
+ GuardianApiContentAdditionalInformationOptions? options = null,
+ CancellationToken cancellationToken = default);
+}
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs
index 220b3e9..e838e60 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentFilterOptions.cs
@@ -1,8 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace GuardianClient.Options.Search;
///
/// Options for filtering content search results by various criteria.
///
+[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public class GuardianApiContentFilterOptions
{
///
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
index 584fc64..2fd0143 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
@@ -1,8 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace GuardianClient.Options.Search;
///
/// Options for controlling the ordering of search results.
///
+[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public class GuardianApiContentOrderOptions
{
///
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs
index df86b78..04ba1ee 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentPageOptions.cs
@@ -1,8 +1,11 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace GuardianClient.Options.Search;
///
/// Options for controlling pagination of search results.
///
+[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public class GuardianApiContentPageOptions
{
///
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs
index 4892bbc..01fbf4d 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentSearchOptions.cs
@@ -1,10 +1,12 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace GuardianClient.Options.Search;
///
/// Options for searching content using the Guardian API.
///
-public class
- GuardianApiContentSearchOptions
+[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
+public class GuardianApiContentSearchOptions
{
///
/// Request content containing this free text. Supports AND, OR and NOT operators, and exact phrase queries using double quotes.
From 4129d79eddebb767ffb2f5e338eab25085e01e6b Mon Sep 17 00:00:00 2001
From: Andrew Tarr
Date: Mon, 11 Aug 2025 20:06:32 -0500
Subject: [PATCH 4/7] Adding more enums and verifying tests pass
---
.../GuardianApiClientTests.cs | 20 +-
.../GuardianClient.Tests/TestBase.cs | 2 +-
.../GuardianClient/GuardianApiClient.cs | 36 +++-
.../GuardianClient/IGuardianApiClient.cs | 3 +-
.../Internal/UrlParameterBuilder.cs | 16 +-
.../Search/AdditionalInformationEnums.cs | 189 ++++++++++++++++++
...nApiContentAdditionalInformationOptions.cs | 43 ++--
7 files changed, 282 insertions(+), 27 deletions(-)
create mode 100644 GuardianClient/GuardianClient/Options/Search/AdditionalInformationEnums.cs
diff --git a/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs b/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs
index fadc4d7..3a1a820 100644
--- a/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs
+++ b/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs
@@ -10,7 +10,11 @@ public class GuardianApiClientTests : TestBase
[TestMethod]
public async Task SearchAsyncSmokeTest()
{
- var result = await ApiClient.SearchAsync("climate change", pageSize: 5);
+ var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "climate change",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 5 }
+ });
result.ShouldNotBeNull("Search result should not be null");
result.Status.ShouldBe("ok", "API response status should be 'ok'");
@@ -31,7 +35,10 @@ public async Task SearchAsyncSmokeTest()
[TestMethod]
public async Task SearchAsyncWithNoResults()
{
- var result = await ApiClient.SearchAsync("xyzabc123nonexistentquery456");
+ var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "xyzabc123nonexistentquery456"
+ });
result.ShouldNotBeNull("Search result should not be null even with no matches");
result.Status.ShouldBe("ok", "API response status should be 'ok'");
@@ -41,13 +48,18 @@ public async Task SearchAsyncWithNoResults()
[TestMethod]
public async Task GetItemAsyncSmokeTest()
{
- var searchResult = await ApiClient.SearchAsync("technology", pageSize: 1);
+ var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "technology",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }
+ });
searchResult.ShouldNotBeNull("Search should return results");
searchResult.Results.Count.ShouldBe(1, "Should return exactly one result");
var contentItem = searchResult.Results.First();
var itemId = contentItem.Id;
- var singleItemResult = await ApiClient.GetItemAsync(itemId, new GuardianApiContentAdditionalInformationOptions { ShowFields = ["body"] });
+ var singleItemResult = await ApiClient.GetItemAsync(itemId,
+ new GuardianApiContentAdditionalInformationOptions { ShowFields = [ShowFieldsOption.Body] });
singleItemResult.ShouldNotBeNull("GetItem result should not be null");
singleItemResult.Status.ShouldBe("ok", "API response status should be 'ok'");
diff --git a/GuardianClient/GuardianClient.Tests/TestBase.cs b/GuardianClient/GuardianClient.Tests/TestBase.cs
index 5960da5..45d2a50 100644
--- a/GuardianClient/GuardianClient.Tests/TestBase.cs
+++ b/GuardianClient/GuardianClient.Tests/TestBase.cs
@@ -6,7 +6,7 @@ namespace GuardianClient.Tests;
public abstract class TestBase
{
- protected static GuardianApiClient ApiClient { get; }
+ protected static IGuardianApiClient ApiClient { get; }
static TestBase()
{
diff --git a/GuardianClient/GuardianClient/GuardianApiClient.cs b/GuardianClient/GuardianClient/GuardianApiClient.cs
index b845290..92272ff 100644
--- a/GuardianClient/GuardianClient/GuardianApiClient.cs
+++ b/GuardianClient/GuardianClient/GuardianApiClient.cs
@@ -88,11 +88,37 @@ public GuardianApiClient(string apiKey)
var parameters = new List { $"api-key={Uri.EscapeDataString(_apiKey)}" };
- UrlParameterBuilder.AddParameterIfAny(parameters, "show-fields", options?.ShowFields);
- UrlParameterBuilder.AddParameterIfAny(parameters, "show-tags", options?.ShowTags);
- UrlParameterBuilder.AddParameterIfAny(parameters, "show-elements", options?.ShowElements);
- UrlParameterBuilder.AddParameterIfAny(parameters, "show-references", options?.ShowReferences);
- UrlParameterBuilder.AddParameterIfAny(parameters, "show-blocks", options?.ShowBlocks);
+ UrlParameterBuilder.AddParameterIfAny(
+ parameters,
+ "show-fields",
+ options?.ShowFields,
+ option => option.ToApiString()
+ );
+
+ UrlParameterBuilder.AddParameterIfAny(
+ parameters,
+ "show-tags",
+ options?.ShowTags,
+ option => option.ToApiString()
+ );
+ UrlParameterBuilder.AddParameterIfAny(
+ parameters,
+ "show-elements",
+ options?.ShowElements,
+ option => option.ToApiString()
+ );
+ UrlParameterBuilder.AddParameterIfAny(
+ parameters,
+ "show-references",
+ options?.ShowReferences,
+ option => option.ToApiString()
+ );
+
+ UrlParameterBuilder.AddParameterIfAny(
+ parameters,
+ "show-blocks",
+ options?.ShowBlocks
+ );
var url = $"/{itemId}?{string.Join("&", parameters)}";
var response = await _httpClient.GetAsync(url, cancellationToken);
diff --git a/GuardianClient/GuardianClient/IGuardianApiClient.cs b/GuardianClient/GuardianClient/IGuardianApiClient.cs
index db1787f..d3a3f6b 100644
--- a/GuardianClient/GuardianClient/IGuardianApiClient.cs
+++ b/GuardianClient/GuardianClient/IGuardianApiClient.cs
@@ -48,5 +48,6 @@ public interface IGuardianApiClient
Task GetItemAsync(
string itemId,
GuardianApiContentAdditionalInformationOptions? options = null,
- CancellationToken cancellationToken = default);
+ CancellationToken cancellationToken = default
+ );
}
diff --git a/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs b/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs
index f7ad8ac..fef571f 100644
--- a/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs
+++ b/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs
@@ -28,6 +28,14 @@ internal static void AddParameterIfAny(List parameters, string parameter
}
}
+ internal static void AddParameterIfAny(List parameters, string parameterName, T[]? values, Func converter)
+ {
+ if (values is { Length: > 0 })
+ {
+ parameters.Add($"{parameterName}={string.Join(",", values.Select(v => Uri.EscapeDataString(converter(v))))}");
+ }
+ }
+
internal static void AddQueryParameters(GuardianApiContentSearchOptions options, List parameters)
{
AddParameterIfNotEmpty(parameters, "q", options.Query);
@@ -108,10 +116,10 @@ internal static void AddAdditionalInformationParameters(
List parameters
)
{
- AddParameterIfAny(parameters, "show-fields", additionalOptions.ShowFields);
- AddParameterIfAny(parameters, "show-tags", additionalOptions.ShowTags);
- AddParameterIfAny(parameters, "show-elements", additionalOptions.ShowElements);
- AddParameterIfAny(parameters, "show-references", additionalOptions.ShowReferences);
+ AddParameterIfAny(parameters, "show-fields", additionalOptions.ShowFields, f => f.ToApiString());
+ AddParameterIfAny(parameters, "show-tags", additionalOptions.ShowTags, t => t.ToApiString());
+ AddParameterIfAny(parameters, "show-elements", additionalOptions.ShowElements, e => e.ToApiString());
+ AddParameterIfAny(parameters, "show-references", additionalOptions.ShowReferences, r => r.ToApiString());
AddParameterIfAny(parameters, "show-blocks", additionalOptions.ShowBlocks);
}
}
\ No newline at end of file
diff --git a/GuardianClient/GuardianClient/Options/Search/AdditionalInformationEnums.cs b/GuardianClient/GuardianClient/Options/Search/AdditionalInformationEnums.cs
new file mode 100644
index 0000000..b3dc492
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/AdditionalInformationEnums.cs
@@ -0,0 +1,189 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Fields that can be included with content responses.
+///
+public enum ShowFieldsOption
+{
+ TrailText,
+ Headline,
+ ShowInRelatedContent,
+ Body,
+ LastModified,
+ HasStoryPackage,
+ Score,
+ Standfirst,
+ ShortUrl,
+ Thumbnail,
+ Wordcount,
+ Commentable,
+ IsPremoderated,
+ AllowUgc,
+ Byline,
+ Publication,
+ InternalPageCode,
+ ProductionOffice,
+ ShouldHideAdverts,
+ LiveBloggingNow,
+ CommentCloseDate,
+ StarRating,
+ All
+}
+
+///
+/// Tag types that can be included with content responses.
+///
+public enum ShowTagsOption
+{
+ Blog,
+ Contributor,
+ Keyword,
+ NewspaperBook,
+ NewspaperBookSection,
+ Publication,
+ Series,
+ Tone,
+ Type,
+ All
+}
+
+///
+/// Media element types that can be included with content responses.
+///
+public enum ShowElementsOption
+{
+ Audio,
+ Image,
+ Video,
+ All
+}
+
+///
+/// Reference types that can be included with content responses.
+///
+public enum ShowReferencesOption
+{
+ Author,
+ BisacPrefix,
+ EsaCricketMatch,
+ EsaFootballMatch,
+ EsaFootballTeam,
+ EsaFootballTournament,
+ Isbn,
+ Imdb,
+ Musicbrainz,
+ MusicbrainzGenre,
+ OptaCricketMatch,
+ OptaFootballMatch,
+ OptaFootballTeam,
+ OptaFootballTournament,
+ PaFootballCompetition,
+ PaFootballMatch,
+ PaFootballTeam,
+ R1Film,
+ ReutersIndexRic,
+ ReutersStockRic,
+ WitnessAssignment
+}
+
+///
+/// Rights types that can be included with content responses.
+///
+public enum ShowRightsOption
+{
+ Syndicatable,
+ SubscriptionDatabases,
+ All
+}
+
+///
+/// Extension methods for converting additional information enums to their API string values.
+///
+internal static class AdditionalInformationEnumExtensions
+{
+ internal static string ToApiString(this ShowFieldsOption option) => option switch
+ {
+ ShowFieldsOption.TrailText => "trailText",
+ ShowFieldsOption.Headline => "headline",
+ ShowFieldsOption.ShowInRelatedContent => "showInRelatedContent",
+ ShowFieldsOption.Body => "body",
+ ShowFieldsOption.LastModified => "lastModified",
+ ShowFieldsOption.HasStoryPackage => "hasStoryPackage",
+ ShowFieldsOption.Score => "score",
+ ShowFieldsOption.Standfirst => "standfirst",
+ ShowFieldsOption.ShortUrl => "shortUrl",
+ ShowFieldsOption.Thumbnail => "thumbnail",
+ ShowFieldsOption.Wordcount => "wordcount",
+ ShowFieldsOption.Commentable => "commentable",
+ ShowFieldsOption.IsPremoderated => "isPremoderated",
+ ShowFieldsOption.AllowUgc => "allowUgc",
+ ShowFieldsOption.Byline => "byline",
+ ShowFieldsOption.Publication => "publication",
+ ShowFieldsOption.InternalPageCode => "internalPageCode",
+ ShowFieldsOption.ProductionOffice => "productionOffice",
+ ShowFieldsOption.ShouldHideAdverts => "shouldHideAdverts",
+ ShowFieldsOption.LiveBloggingNow => "liveBloggingNow",
+ ShowFieldsOption.CommentCloseDate => "commentCloseDate",
+ ShowFieldsOption.StarRating => "starRating",
+ ShowFieldsOption.All => "all",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+
+ internal static string ToApiString(this ShowTagsOption option) => option switch
+ {
+ ShowTagsOption.Blog => "blog",
+ ShowTagsOption.Contributor => "contributor",
+ ShowTagsOption.Keyword => "keyword",
+ ShowTagsOption.NewspaperBook => "newspaper-book",
+ ShowTagsOption.NewspaperBookSection => "newspaper-book-section",
+ ShowTagsOption.Publication => "publication",
+ ShowTagsOption.Series => "series",
+ ShowTagsOption.Tone => "tone",
+ ShowTagsOption.Type => "type",
+ ShowTagsOption.All => "all",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+
+ internal static string ToApiString(this ShowElementsOption option) => option switch
+ {
+ ShowElementsOption.Audio => "audio",
+ ShowElementsOption.Image => "image",
+ ShowElementsOption.Video => "video",
+ ShowElementsOption.All => "all",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+
+ internal static string ToApiString(this ShowReferencesOption option) => option switch
+ {
+ ShowReferencesOption.Author => "author",
+ ShowReferencesOption.BisacPrefix => "bisac-prefix",
+ ShowReferencesOption.EsaCricketMatch => "esa-cricket-match",
+ ShowReferencesOption.EsaFootballMatch => "esa-football-match",
+ ShowReferencesOption.EsaFootballTeam => "esa-football-team",
+ ShowReferencesOption.EsaFootballTournament => "esa-football-tournament",
+ ShowReferencesOption.Isbn => "isbn",
+ ShowReferencesOption.Imdb => "imdb",
+ ShowReferencesOption.Musicbrainz => "musicbrainz",
+ ShowReferencesOption.MusicbrainzGenre => "musicbrainzgenre",
+ ShowReferencesOption.OptaCricketMatch => "opta-cricket-match",
+ ShowReferencesOption.OptaFootballMatch => "opta-football-match",
+ ShowReferencesOption.OptaFootballTeam => "opta-football-team",
+ ShowReferencesOption.OptaFootballTournament => "opta-football-tournament",
+ ShowReferencesOption.PaFootballCompetition => "pa-football-competition",
+ ShowReferencesOption.PaFootballMatch => "pa-football-match",
+ ShowReferencesOption.PaFootballTeam => "pa-football-team",
+ ShowReferencesOption.R1Film => "r1-film",
+ ShowReferencesOption.ReutersIndexRic => "reuters-index-ric",
+ ShowReferencesOption.ReutersStockRic => "reuters-stock-ric",
+ ShowReferencesOption.WitnessAssignment => "witness-assignment",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+
+ internal static string ToApiString(this ShowRightsOption option) => option switch
+ {
+ ShowRightsOption.Syndicatable => "syndicatable",
+ ShowRightsOption.SubscriptionDatabases => "subscription-databases",
+ ShowRightsOption.All => "all",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+}
\ No newline at end of file
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs
index 8f37c27..22373f8 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs
@@ -1,32 +1,51 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace GuardianClient.Options.Search;
+///
+/// Options for requesting additional information to be included with search results.
+///
+[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public class GuardianApiContentAdditionalInformationOptions
{
///
- ///
+ /// Add fields associated with the content such as headline, body, thumbnail, etc.
///
- public string[]? ShowFields { get; set; }
+ public ShowFieldsOption[]? ShowFields { get; set; }
///
- ///
+ /// Add associated metadata tags such as contributor, keyword, tone, etc.
///
- public string[]? ShowTags { get; set; }
+ public ShowTagsOption[]? ShowTags { get; set; }
///
- ///
+ /// Add associated media elements such as images, audio, and video.
///
- public string[]? ShowElements { get; set; }
+ public ShowElementsOption[]? ShowElements { get; set; }
///
- ///
+ /// Add associated reference data such as ISBNs, IMDB IDs, author references, etc.
///
- public string[]? ShowReferences { get; set; }
+ public ShowReferencesOption[]? ShowReferences { get; set; }
+
///
- ///
+ /// Add associated blocks (single block for content, one or more for liveblogs).
+ /// Supported values:
+ ///
+ /// - "main" - Main content block
+ /// - "body" - Body content blocks
+ /// - "all" - All blocks
+ /// - "body:latest" - Latest body blocks (default limit: 20)
+ /// - "body:latest:10" - Latest 10 body blocks
+ /// - "body:oldest" - Oldest body blocks
+ /// - "body:oldest:10" - Oldest 10 body blocks
+ /// - "body:<block-id>" - Specific block by ID
+ /// - "body:around:<block-id>" - Block and 20 blocks around it
+ /// - "body:around:<block-id>:10" - Block and 10 blocks around it
+ /// - "body:key-events" - Key event blocks
+ /// - "body:published-since:<timestamp>" - Blocks since timestamp (e.g., "body:published-since:1556529318000")
+ ///
///
public string[]? ShowBlocks { get; set; }
}
-
-// TODO additional information table below shows all the options that are available in the Show* methods, so
-// we could strongly type those.
From bdbf87dfbe2aecf624b24d358e4545ae1e6085a8 Mon Sep 17 00:00:00 2001
From: Andrew Tarr
Date: Mon, 11 Aug 2025 20:14:32 -0500
Subject: [PATCH 5/7] Test separation and cleanup
---
.../GuardianClient.Tests/GetItemAsyncTests.cs | 245 ++++++++++++++++++
.../GuardianApiClientIntegrationTests.cs | 136 ++++++++++
.../GuardianApiClientTests.cs | 82 ------
.../GuardianClient.Tests/SearchAsyncTests.cs | 144 ++++++++++
.../GuardianClient/GuardianClient.csproj | 4 +-
.../Internal/UrlParameterBuilder.cs | 12 +-
...rderBy.cs => GuardianApiContentOrderBy.cs} | 2 +-
...Date.cs => GuardianApiContentOrderDate.cs} | 2 +-
.../Search/GuardianApiContentOrderOptions.cs | 4 +-
9 files changed, 537 insertions(+), 94 deletions(-)
create mode 100644 GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs
create mode 100644 GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs
delete mode 100644 GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs
create mode 100644 GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs
rename GuardianClient/GuardianClient/Options/Search/{GuardianApiOrderBy.cs => GuardianApiContentOrderBy.cs} (92%)
rename GuardianClient/GuardianClient/Options/Search/{GuardianApiOrderDate.cs => GuardianApiContentOrderDate.cs} (91%)
diff --git a/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs b/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs
new file mode 100644
index 0000000..a0dcbe5
--- /dev/null
+++ b/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs
@@ -0,0 +1,245 @@
+using GuardianClient.Options.Search;
+using Shouldly;
+
+namespace GuardianClient.Tests;
+
+[TestClass]
+public class GetItemAsyncTests : TestBase
+{
+ [TestMethod]
+ public async Task GetItemAsync_WithValidId_ReturnsItem()
+ {
+ // First get a valid item ID from search
+ var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "technology",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }
+ });
+ searchResult.ShouldNotBeNull("Search should return results");
+ searchResult.Results.Count.ShouldBe(1, "Should return exactly one result");
+
+ var contentItem = searchResult.Results.First();
+ var itemId = contentItem.Id;
+
+ // Now get the specific item
+ var singleItemResult = await ApiClient.GetItemAsync(itemId,
+ new GuardianApiContentAdditionalInformationOptions { ShowFields = [ShowFieldsOption.Body] });
+
+ singleItemResult.ShouldNotBeNull("GetItem result should not be null");
+ singleItemResult.Status.ShouldBe("ok", "API response status should be 'ok'");
+ singleItemResult.Content.ShouldNotBeNull("Content should not be null");
+ singleItemResult.Content.Id.ShouldBe(itemId, "Returned content ID should match requested ID");
+ singleItemResult.Content.WebTitle.ShouldNotBeNullOrEmpty("Content should have a title");
+ singleItemResult.Content.Fields.ShouldNotBeNull("Fields should be populated when ShowFields is specified");
+
+ Console.WriteLine($"Retrieved item: {singleItemResult.Content.WebTitle}");
+ Console.WriteLine($"Item ID: {singleItemResult.Content.Id}");
+ Console.WriteLine($"Published: {singleItemResult.Content.WebPublicationDate}");
+
+ if (!string.IsNullOrEmpty(singleItemResult.Content.Fields.Body))
+ {
+ Console.WriteLine($"Body length: {singleItemResult.Content.Fields.Body.Length} characters");
+ Console.WriteLine(
+ $"Body preview: {singleItemResult.Content.Fields.Body[..Math.Min(200, singleItemResult.Content.Fields.Body.Length)]}...");
+ }
+ }
+
+ [TestMethod]
+ public async Task GetItemAsync_WithInvalidId_ThrowsException()
+ {
+ var invalidId = "invalid/article/id/that/does/not/exist";
+
+ var exception = await Should.ThrowAsync(async () =>
+ {
+ await ApiClient.GetItemAsync(invalidId);
+ });
+
+ Console.WriteLine($"Expected exception for invalid ID: {exception.Message}");
+ }
+
+ [TestMethod]
+ public async Task GetItemAsync_WithNullId_ThrowsArgumentException()
+ {
+ var exception = await Should.ThrowAsync(async () =>
+ {
+ await ApiClient.GetItemAsync(null!);
+ });
+
+ Console.WriteLine($"Expected exception for null ID: {exception.Message}");
+ }
+
+ [TestMethod]
+ public async Task GetItemAsync_WithEmptyId_ThrowsArgumentException()
+ {
+ var exception = await Should.ThrowAsync(async () =>
+ {
+ await ApiClient.GetItemAsync("");
+ });
+
+ Console.WriteLine($"Expected exception for empty ID: {exception.Message}");
+ }
+
+ [TestMethod]
+ public async Task GetItemAsync_WithShowFields_ReturnsEnhancedContent()
+ {
+ // Get a valid item ID
+ var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "politics",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }
+ });
+
+ var itemId = searchResult.Results.First().Id;
+
+ // Request with multiple fields
+ var result = await ApiClient.GetItemAsync(itemId,
+ new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowFields =
+ [
+ ShowFieldsOption.Headline,
+ ShowFieldsOption.Body,
+ ShowFieldsOption.Byline,
+ ShowFieldsOption.Thumbnail
+ ]
+ });
+
+ result.ShouldNotBeNull();
+ result.Content.Fields.ShouldNotBeNull("Fields should be populated");
+ result.Content.Fields.Headline.ShouldNotBeNullOrEmpty("Headline should be populated");
+
+ Console.WriteLine($"Enhanced content for: {result.Content.WebTitle}");
+ Console.WriteLine($"Headline: {result.Content.Fields.Headline}");
+ Console.WriteLine($"Has body: {!string.IsNullOrEmpty(result.Content.Fields.Body)}");
+ Console.WriteLine($"Has byline: {!string.IsNullOrEmpty(result.Content.Fields.Byline)}");
+ }
+
+ [TestMethod]
+ public async Task GetItemAsync_WithShowTags_ReturnsContentWithTags()
+ {
+ // Get a valid item ID
+ var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "sport",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }
+ });
+
+ var itemId = searchResult.Results.First().Id;
+
+ // Request with tags
+ var result = await ApiClient.GetItemAsync(itemId,
+ new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowTags = [ShowTagsOption.Keyword, ShowTagsOption.Tone, ShowTagsOption.Type]
+ });
+
+ result.ShouldNotBeNull();
+ result.Content.Tags.ShouldNotBeNull("Tags should be populated");
+ result.Content.Tags.Count.ShouldBeGreaterThan(0, "Should have at least one tag");
+
+ Console.WriteLine($"Content '{result.Content.WebTitle}' has {result.Content.Tags.Count} tags:");
+ foreach (var tag in result.Content.Tags.Take(5)) // Show first 5 tags
+ {
+ Console.WriteLine($" - {tag.WebTitle} ({tag.Type})");
+ }
+ }
+
+ [TestMethod]
+ public async Task GetItemAsync_WithShowElements_ReturnsContentWithElements()
+ {
+ // Get a valid item ID
+ var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "music",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }
+ });
+
+ var itemId = searchResult.Results.First().Id;
+
+ // Request with elements
+ var result = await ApiClient.GetItemAsync(itemId,
+ new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowElements = [ShowElementsOption.Image, ShowElementsOption.Video]
+ });
+
+ result.ShouldNotBeNull();
+ // Elements might be null if the article has no media, so we don't assert their presence
+
+ Console.WriteLine($"Content '{result.Content.WebTitle}' elements:");
+ if (result.Content.Elements != null)
+ {
+ Console.WriteLine($" Has {result.Content.Elements.Count} media elements");
+ }
+ else
+ {
+ Console.WriteLine(" No media elements found");
+ }
+ }
+
+ [TestMethod]
+ public async Task GetItemAsync_WithShowBlocks_ReturnsContentWithBlocks()
+ {
+ // Get a valid item ID
+ var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "news",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }
+ });
+
+ var itemId = searchResult.Results.First().Id;
+
+ // Request with blocks using string array (as it's still string-based)
+ var result = await ApiClient.GetItemAsync(itemId,
+ new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowBlocks = ["main", "body"]
+ });
+
+ result.ShouldNotBeNull();
+ // Blocks might be null depending on content type
+
+ Console.WriteLine($"Content '{result.Content.WebTitle}' blocks:");
+ if (result.Content.Blocks != null)
+ {
+ Console.WriteLine($" Requested main and body blocks");
+ }
+ else
+ {
+ Console.WriteLine(" No blocks found");
+ }
+ }
+
+ [TestMethod]
+ public async Task GetItemAsync_WithAllOptions_ReturnsFullyEnhancedContent()
+ {
+ // Get a valid item ID
+ var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "culture",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }
+ });
+
+ var itemId = searchResult.Results.First().Id;
+
+ // Request with all enhancement options
+ var result = await ApiClient.GetItemAsync(itemId,
+ new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowFields = [ShowFieldsOption.All],
+ ShowTags = [ShowTagsOption.All],
+ ShowElements = [ShowElementsOption.All],
+ ShowBlocks = ["all"]
+ });
+
+ result.ShouldNotBeNull();
+ result.Content.Fields.ShouldNotBeNull("All fields should be populated");
+ result.Content.Tags.ShouldNotBeNull("All tags should be populated");
+
+ Console.WriteLine($"Fully enhanced content: {result.Content.WebTitle}");
+ Console.WriteLine($" Fields populated: Yes");
+ Console.WriteLine($" Tags count: {result.Content.Tags.Count}");
+ Console.WriteLine($" Elements: {(result.Content.Elements?.Count ?? 0)}");
+ Console.WriteLine($" Has blocks: {result.Content.Blocks != null}");
+ }
+}
diff --git a/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs b/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs
new file mode 100644
index 0000000..30cef0b
--- /dev/null
+++ b/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs
@@ -0,0 +1,136 @@
+using GuardianClient.Options.Search;
+using Shouldly;
+
+namespace GuardianClient.Tests;
+
+[TestClass]
+public class GuardianApiClientIntegrationTests : TestBase
+{
+ [TestMethod]
+ public void ApiClient_ShouldNotBeNull()
+ {
+ ApiClient.ShouldNotBeNull("API client should be properly initialized");
+ ApiClient.ShouldBeAssignableTo("API client should implement the interface");
+ }
+
+ [TestMethod]
+ public async Task EndToEnd_SearchAndGetItem_WorksTogether()
+ {
+ // Search for content
+ var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "artificial intelligence",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 1 },
+ FilterOptions = new GuardianApiContentFilterOptions
+ {
+ Section = "technology"
+ }
+ });
+
+ searchResult.ShouldNotBeNull("Search should return results");
+ searchResult.Results.Count.ShouldBe(1, "Should return exactly one result");
+
+ // Get the specific item with enhanced information
+ var contentItem = searchResult.Results.First();
+ var detailedResult = await ApiClient.GetItemAsync(contentItem.Id,
+ new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowFields = [ShowFieldsOption.Headline, ShowFieldsOption.Body],
+ ShowTags = [ShowTagsOption.Keyword]
+ });
+
+ detailedResult.ShouldNotBeNull("Detailed result should not be null");
+ detailedResult.Content!.Id.ShouldBe(contentItem.Id, "IDs should match");
+ detailedResult.Content.WebTitle.ShouldBe(contentItem.WebTitle, "Titles should match");
+ detailedResult.Content.Fields.ShouldNotBeNull("Detailed fields should be populated");
+ detailedResult.Content.Tags.ShouldNotBeNull("Tags should be populated");
+
+ Console.WriteLine($"End-to-end test successful:");
+ Console.WriteLine($" Search found: {contentItem.WebTitle}");
+ Console.WriteLine($" Retrieved same item with {detailedResult.Content.Tags.Count} tags");
+ Console.WriteLine($" Body content length: {detailedResult.Content.Fields.Body?.Length ?? 0} characters");
+ }
+
+ [TestMethod]
+ public async Task SearchWithComplexOptions_ReturnsExpectedResults()
+ {
+ // Test a complex search with multiple option types
+ var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "climate change",
+ QueryFields = ["body", "headline"],
+ FilterOptions = new GuardianApiContentFilterOptions
+ {
+ Section = "environment"
+ },
+ DateOptions = new GuardianApiContentDateOptions
+ {
+ FromDate = new DateOnly(2023, 1, 1)
+ },
+ PageOptions = new GuardianApiContentPageOptions
+ {
+ Page = 1,
+ PageSize = 5
+ },
+ OrderOptions = new GuardianApiContentOrderOptions
+ {
+ OrderBy = GuardianApiContentOrderBy.Relevance
+ },
+ AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowFields = [ShowFieldsOption.Headline, ShowFieldsOption.Score],
+ ShowTags = [ShowTagsOption.Tone]
+ }
+ });
+
+ result.ShouldNotBeNull("Complex search should return results");
+ result.Status.ShouldBe("ok", "API should respond successfully");
+ result.Results.Count.ShouldBeLessThanOrEqualTo(5, "Should respect page size");
+
+ // Verify enhanced data is present
+ if (result.Results.Any())
+ {
+ var firstItem = result.Results.First();
+ firstItem.Fields.ShouldNotBeNull("Enhanced fields should be present");
+ firstItem.Tags.ShouldNotBeNull("Tags should be present");
+
+ Console.WriteLine($"Complex search returned {result.Results.Count} results");
+ Console.WriteLine($"First result: {firstItem.WebTitle}");
+ Console.WriteLine($"Relevance score: {firstItem.Fields.Score ?? "N/A"}");
+ }
+ }
+
+ [TestMethod]
+ public async Task TypeSafetyTest_EnumsMapToCorrectApiValues()
+ {
+ // This test ensures our enums map to the correct API values
+ var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "test",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 1 },
+ AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowFields =
+ [
+ ShowFieldsOption.Headline,
+ ShowFieldsOption.TrailText,
+ ShowFieldsOption.ShowInRelatedContent
+ ],
+ ShowTags = [ShowTagsOption.Tone, ShowTagsOption.Type],
+ ShowElements = [ShowElementsOption.Image]
+ }
+ });
+
+ result.ShouldNotBeNull("Type safety test should work");
+ result.Status.ShouldBe("ok", "API should accept enum-mapped values");
+
+ if (result.Results.Any())
+ {
+ var item = result.Results.First();
+ Console.WriteLine($"Type safety test passed for: {item.WebTitle}");
+ Console.WriteLine($" Fields populated: {item.Fields != null}");
+ Console.WriteLine($" Tags populated: {item.Tags != null}");
+ Console.WriteLine($" Elements populated: {item.Elements != null}");
+ }
+ }
+}
diff --git a/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs b/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs
deleted file mode 100644
index 3a1a820..0000000
--- a/GuardianClient/GuardianClient.Tests/GuardianApiClientTests.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-using GuardianClient.Options.Search;
-
-namespace GuardianClient.Tests;
-
-using Shouldly;
-
-[TestClass]
-public class GuardianApiClientTests : TestBase
-{
- [TestMethod]
- public async Task SearchAsyncSmokeTest()
- {
- var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
- {
- Query = "climate change",
- PageOptions = new GuardianApiContentPageOptions { PageSize = 5 }
- });
-
- result.ShouldNotBeNull("Search result should not be null");
- result.Status.ShouldBe("ok", "API response status should be 'ok'");
- result.Results.Count.ShouldBeGreaterThan(0, "Should return at least one result");
- result.Results.Count.ShouldBeLessThanOrEqualTo(5, "Should not return more than requested page size");
-
- var firstItem = result.Results.First();
- firstItem.Id.ShouldNotBeNullOrEmpty("Content item should have an ID");
- firstItem.WebTitle.ShouldNotBeNullOrEmpty("Content item should have a title");
- firstItem.WebUrl.ShouldNotBeNullOrEmpty("Content item should have a web URL");
- firstItem.ApiUrl.ShouldNotBeNullOrEmpty("Content item should have an API URL");
-
- Console.WriteLine($"Found {result.Results.Count} articles about climate change");
- Console.WriteLine($"First article: {firstItem.WebTitle}");
- Console.WriteLine($"Published: {firstItem.WebPublicationDate}");
- }
-
- [TestMethod]
- public async Task SearchAsyncWithNoResults()
- {
- var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
- {
- Query = "xyzabc123nonexistentquery456"
- });
-
- result.ShouldNotBeNull("Search result should not be null even with no matches");
- result.Status.ShouldBe("ok", "API response status should be 'ok'");
- result.Results.Count.ShouldBe(0, "Should return zero results for non-existent query");
- }
-
- [TestMethod]
- public async Task GetItemAsyncSmokeTest()
- {
- var searchResult = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
- {
- Query = "technology",
- PageOptions = new GuardianApiContentPageOptions { PageSize = 1 }
- });
- searchResult.ShouldNotBeNull("Search should return results");
- searchResult.Results.Count.ShouldBe(1, "Should return exactly one result");
-
- var contentItem = searchResult.Results.First();
- var itemId = contentItem.Id;
- var singleItemResult = await ApiClient.GetItemAsync(itemId,
- new GuardianApiContentAdditionalInformationOptions { ShowFields = [ShowFieldsOption.Body] });
-
- singleItemResult.ShouldNotBeNull("GetItem result should not be null");
- singleItemResult.Status.ShouldBe("ok", "API response status should be 'ok'");
- singleItemResult.Content.ShouldNotBeNull("Content should not be null");
- singleItemResult.Content.Id.ShouldBe(itemId, "Returned content ID should match requested ID");
- singleItemResult.Content.WebTitle.ShouldNotBeNullOrEmpty("Content should have a title");
- singleItemResult.Content.Fields.ShouldNotBeNull("Fields should be populated when ShowFields is specified");
-
- Console.WriteLine($"Retrieved item: {singleItemResult.Content.WebTitle}");
- Console.WriteLine($"Item ID: {singleItemResult.Content.Id}");
- Console.WriteLine($"Published: {singleItemResult.Content.WebPublicationDate}");
-
- if (!string.IsNullOrEmpty(singleItemResult.Content.Fields.Body))
- {
- Console.WriteLine($"Body length: {singleItemResult.Content.Fields.Body.Length} characters");
- Console.WriteLine(
- $"Body preview: {singleItemResult.Content.Fields.Body[..Math.Min(200, singleItemResult.Content.Fields.Body.Length)]}...");
- }
- }
-}
diff --git a/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs b/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs
new file mode 100644
index 0000000..a346136
--- /dev/null
+++ b/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs
@@ -0,0 +1,144 @@
+using GuardianClient.Options.Search;
+using Shouldly;
+
+namespace GuardianClient.Tests;
+
+[TestClass]
+public class SearchAsyncTests : TestBase
+{
+ [TestMethod]
+ public async Task SearchAsync_WithQuery_ReturnsResults()
+ {
+ var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "climate change",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 5 }
+ });
+
+ result.ShouldNotBeNull("Search result should not be null");
+ result.Status.ShouldBe("ok", "API response status should be 'ok'");
+ result.Results.Count.ShouldBeGreaterThan(0, "Should return at least one result");
+ result.Results.Count.ShouldBeLessThanOrEqualTo(5, "Should not return more than requested page size");
+
+ var firstItem = result.Results.First();
+ firstItem.Id.ShouldNotBeNullOrEmpty("Content item should have an ID");
+ firstItem.WebTitle.ShouldNotBeNullOrEmpty("Content item should have a title");
+ firstItem.WebUrl.ShouldNotBeNullOrEmpty("Content item should have a web URL");
+ firstItem.ApiUrl.ShouldNotBeNullOrEmpty("Content item should have an API URL");
+
+ Console.WriteLine($"Found {result.Results.Count} articles about climate change");
+ Console.WriteLine($"First article: {firstItem.WebTitle}");
+ Console.WriteLine($"Published: {firstItem.WebPublicationDate}");
+ }
+
+ [TestMethod]
+ public async Task SearchAsync_WithNonExistentQuery_ReturnsNoResults()
+ {
+ var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "xyzabc123nonexistentquery456"
+ });
+
+ result.ShouldNotBeNull("Search result should not be null even with no matches");
+ result.Status.ShouldBe("ok", "API response status should be 'ok'");
+ result.Results.Count.ShouldBe(0, "Should return zero results for non-existent query");
+ }
+
+ [TestMethod]
+ public async Task SearchAsync_WithNoOptions_ReturnsDefaultResults()
+ {
+ var result = await ApiClient.SearchAsync();
+
+ result.ShouldNotBeNull("Search result should not be null");
+ result.Status.ShouldBe("ok", "API response status should be 'ok'");
+ result.Results.Count.ShouldBeGreaterThan(0, "Should return results with default options");
+ result.Results.Count.ShouldBeLessThanOrEqualTo(10, "Default page size should be 10 or less");
+
+ Console.WriteLine($"Default search returned {result.Results.Count} results");
+ }
+
+ [TestMethod]
+ public async Task SearchAsync_WithFilterOptions_ReturnsFilteredResults()
+ {
+ var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "technology",
+ FilterOptions = new GuardianApiContentFilterOptions
+ {
+ Section = "technology"
+ },
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 3 }
+ });
+
+ result.ShouldNotBeNull("Search result should not be null");
+ result.Status.ShouldBe("ok", "API response status should be 'ok'");
+ result.Results.Count.ShouldBeGreaterThan(0, "Should return technology results");
+
+ // Check that results are from technology section
+ foreach (var item in result.Results)
+ {
+ item.SectionId.ShouldBe("technology", "All results should be from technology section");
+ }
+
+ Console.WriteLine($"Found {result.Results.Count} technology articles");
+ }
+
+ [TestMethod]
+ public async Task SearchAsync_WithOrderOptions_ReturnsOrderedResults()
+ {
+ var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "sports",
+ OrderOptions = new GuardianApiContentOrderOptions
+ {
+ OrderBy = GuardianApiContentOrderBy.Oldest
+ },
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 2 }
+ });
+
+ result.ShouldNotBeNull("Search result should not be null");
+ result.Status.ShouldBe("ok", "API response status should be 'ok'");
+ result.Results.Count.ShouldBeGreaterThan(0, "Should return sports results");
+
+ // Oldest first means first result should be older than or equal to second
+ if (result.Results.Count >= 2)
+ {
+ var first = result.Results.First();
+ var second = result.Results.Skip(1).First();
+
+ if (first.WebPublicationDate.HasValue && second.WebPublicationDate.HasValue)
+ {
+ first.WebPublicationDate.Value.ShouldBeLessThanOrEqualTo(second.WebPublicationDate.Value,
+ "Results should be ordered oldest first");
+ }
+ }
+
+ Console.WriteLine($"Found {result.Results.Count} sports articles (oldest first)");
+ }
+
+ [TestMethod]
+ public async Task SearchAsync_WithAdditionalInformation_ReturnsEnhancedResults()
+ {
+ var result = await ApiClient.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "environment",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 2 },
+ AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowFields = [ShowFieldsOption.Headline, ShowFieldsOption.Thumbnail],
+ ShowTags = [ShowTagsOption.Keyword, ShowTagsOption.Tone]
+ }
+ });
+
+ result.ShouldNotBeNull("Search result should not be null");
+ result.Status.ShouldBe("ok", "API response status should be 'ok'");
+ result.Results.Count.ShouldBeGreaterThan(0, "Should return environment results");
+
+ var firstItem = result.Results.First();
+ firstItem.Fields.ShouldNotBeNull("Fields should be populated");
+ firstItem.Tags.ShouldNotBeNull("Tags should be populated");
+
+ Console.WriteLine($"Enhanced search returned {result.Results.Count} environment articles");
+ Console.WriteLine($"First article has {firstItem.Tags.Count} tags");
+ }
+}
diff --git a/GuardianClient/GuardianClient/GuardianClient.csproj b/GuardianClient/GuardianClient/GuardianClient.csproj
index e09c9ec..b385d91 100644
--- a/GuardianClient/GuardianClient/GuardianClient.csproj
+++ b/GuardianClient/GuardianClient/GuardianClient.csproj
@@ -7,13 +7,13 @@
GuardianClient.NET
- 0.3.0-alpha
+ 0.4.0-alpha
Andrew Tarr
A .NET API wrapper for Guardian services
guardian;api;client;wrapper
MIT
https://github.com/tarrball/GuardianClient.NET
- https://github.com/tarrball/GuardianClient.NET.git
+ https://github.com/tarrball/GuardianClient.NET
git
main
README.md
diff --git a/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs b/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs
index fef571f..73981d1 100644
--- a/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs
+++ b/GuardianClient/GuardianClient/Internal/UrlParameterBuilder.cs
@@ -90,9 +90,9 @@ internal static void AddOrderParameters(GuardianApiContentOrderOptions orderOpti
{
var orderByValue = orderOptions.OrderBy.Value switch
{
- GuardianApiOrderBy.Newest => "newest",
- GuardianApiOrderBy.Oldest => "oldest",
- GuardianApiOrderBy.Relevance => "relevance",
+ GuardianApiContentOrderBy.Newest => "newest",
+ GuardianApiContentOrderBy.Oldest => "oldest",
+ GuardianApiContentOrderBy.Relevance => "relevance",
_ => "newest"
};
parameters.Add($"order-by={orderByValue}");
@@ -102,9 +102,9 @@ internal static void AddOrderParameters(GuardianApiContentOrderOptions orderOpti
{
var orderDateValue = orderOptions.OrderDate.Value switch
{
- GuardianApiOrderDate.Published => "published",
- GuardianApiOrderDate.NewspaperEdition => "newspaper-edition",
- GuardianApiOrderDate.LastModified => "last-modified",
+ GuardianApiContentOrderDate.Published => "published",
+ GuardianApiContentOrderDate.NewspaperEdition => "newspaper-edition",
+ GuardianApiContentOrderDate.LastModified => "last-modified",
_ => "published"
};
parameters.Add($"order-date={orderDateValue}");
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderBy.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderBy.cs
similarity index 92%
rename from GuardianClient/GuardianClient/Options/Search/GuardianApiOrderBy.cs
rename to GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderBy.cs
index f5ee5a8..b75c022 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderBy.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderBy.cs
@@ -3,7 +3,7 @@ namespace GuardianClient.Options.Search;
///
/// Specifies the order in which search results should be returned.
///
-public enum GuardianApiOrderBy
+public enum GuardianApiContentOrderBy
{
///
/// Order by newest content first. Default in all other cases.
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderDate.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderDate.cs
similarity index 91%
rename from GuardianClient/GuardianClient/Options/Search/GuardianApiOrderDate.cs
rename to GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderDate.cs
index 31d89e6..10962a8 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiOrderDate.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderDate.cs
@@ -3,7 +3,7 @@ namespace GuardianClient.Options.Search;
///
/// Specifies which type of date is used to order the results.
///
-public enum GuardianApiOrderDate
+public enum GuardianApiContentOrderDate
{
///
/// The date the content appeared on the web. Default.
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
index 2fd0143..606a557 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentOrderOptions.cs
@@ -11,10 +11,10 @@ public class GuardianApiContentOrderOptions
///
/// Returns results in the specified order. Defaults to Newest in most cases, or Relevance when a query parameter is specified.
///
- public GuardianApiOrderBy? OrderBy { get; set; }
+ public GuardianApiContentOrderBy? OrderBy { get; set; }
///
/// Changes which type of date is used to order the results. Defaults to Published.
///
- public GuardianApiOrderDate? OrderDate { get; set; }
+ public GuardianApiContentOrderDate? OrderDate { get; set; }
}
From 3ab1f62eb4cc9582e5d65c0f930669ded2e1be12 Mon Sep 17 00:00:00 2001
From: Andrew Tarr
Date: Mon, 11 Aug 2025 20:48:49 -0500
Subject: [PATCH 6/7] naming and test updates
---
.../GuardianClient.Tests/GetItemAsyncTests.cs | 20 +-
.../GuardianApiClientIntegrationTests.cs | 62 ++++--
.../GuardianClient.Tests/SearchAsyncTests.cs | 4 +-
.../AdditionalInformationExtensions.cs | 95 +++++++++
.../Search/AdditionalInformationEnums.cs | 189 ------------------
...nApiContentAdditionalInformationOptions.cs | 8 +-
.../GuardianApiContentShowElementsOption.cs | 12 ++
.../GuardianApiContentShowFieldsOption.cs | 31 +++
.../GuardianApiContentShowReferencesOption.cs | 29 +++
.../GuardianApiContentShowRightsOption.cs | 11 +
.../GuardianApiContentShowTagsOption.cs | 18 ++
11 files changed, 261 insertions(+), 218 deletions(-)
create mode 100644 GuardianClient/GuardianClient/Internal/AdditionalInformationExtensions.cs
delete mode 100644 GuardianClient/GuardianClient/Options/Search/AdditionalInformationEnums.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowElementsOption.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowFieldsOption.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowReferencesOption.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowRightsOption.cs
create mode 100644 GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowTagsOption.cs
diff --git a/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs b/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs
index a0dcbe5..d34547c 100644
--- a/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs
+++ b/GuardianClient/GuardianClient.Tests/GetItemAsyncTests.cs
@@ -23,7 +23,7 @@ public async Task GetItemAsync_WithValidId_ReturnsItem()
// Now get the specific item
var singleItemResult = await ApiClient.GetItemAsync(itemId,
- new GuardianApiContentAdditionalInformationOptions { ShowFields = [ShowFieldsOption.Body] });
+ new GuardianApiContentAdditionalInformationOptions { ShowFields = [GuardianApiContentShowFieldsOption.Body] });
singleItemResult.ShouldNotBeNull("GetItem result should not be null");
singleItemResult.Status.ShouldBe("ok", "API response status should be 'ok'");
@@ -97,10 +97,10 @@ public async Task GetItemAsync_WithShowFields_ReturnsEnhancedContent()
{
ShowFields =
[
- ShowFieldsOption.Headline,
- ShowFieldsOption.Body,
- ShowFieldsOption.Byline,
- ShowFieldsOption.Thumbnail
+ GuardianApiContentShowFieldsOption.Headline,
+ GuardianApiContentShowFieldsOption.Body,
+ GuardianApiContentShowFieldsOption.Byline,
+ GuardianApiContentShowFieldsOption.Thumbnail
]
});
@@ -130,7 +130,7 @@ public async Task GetItemAsync_WithShowTags_ReturnsContentWithTags()
var result = await ApiClient.GetItemAsync(itemId,
new GuardianApiContentAdditionalInformationOptions
{
- ShowTags = [ShowTagsOption.Keyword, ShowTagsOption.Tone, ShowTagsOption.Type]
+ ShowTags = [GuardianApiContentShowTagsOption.Keyword, GuardianApiContentShowTagsOption.Tone, GuardianApiContentShowTagsOption.Type]
});
result.ShouldNotBeNull();
@@ -160,7 +160,7 @@ public async Task GetItemAsync_WithShowElements_ReturnsContentWithElements()
var result = await ApiClient.GetItemAsync(itemId,
new GuardianApiContentAdditionalInformationOptions
{
- ShowElements = [ShowElementsOption.Image, ShowElementsOption.Video]
+ ShowElements = [GuardianApiContentShowElementsOption.Image, GuardianApiContentShowElementsOption.Video]
});
result.ShouldNotBeNull();
@@ -226,9 +226,9 @@ public async Task GetItemAsync_WithAllOptions_ReturnsFullyEnhancedContent()
var result = await ApiClient.GetItemAsync(itemId,
new GuardianApiContentAdditionalInformationOptions
{
- ShowFields = [ShowFieldsOption.All],
- ShowTags = [ShowTagsOption.All],
- ShowElements = [ShowElementsOption.All],
+ ShowFields = [GuardianApiContentShowFieldsOption.All],
+ ShowTags = [GuardianApiContentShowTagsOption.All],
+ ShowElements = [GuardianApiContentShowElementsOption.All],
ShowBlocks = ["all"]
});
diff --git a/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs b/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs
index 30cef0b..22bcba9 100644
--- a/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs
+++ b/GuardianClient/GuardianClient.Tests/GuardianApiClientIntegrationTests.cs
@@ -35,8 +35,8 @@ public async Task EndToEnd_SearchAndGetItem_WorksTogether()
var detailedResult = await ApiClient.GetItemAsync(contentItem.Id,
new GuardianApiContentAdditionalInformationOptions
{
- ShowFields = [ShowFieldsOption.Headline, ShowFieldsOption.Body],
- ShowTags = [ShowTagsOption.Keyword]
+ ShowFields = [GuardianApiContentShowFieldsOption.Headline, GuardianApiContentShowFieldsOption.Body],
+ ShowTags = [GuardianApiContentShowTagsOption.Keyword]
});
detailedResult.ShouldNotBeNull("Detailed result should not be null");
@@ -45,10 +45,46 @@ public async Task EndToEnd_SearchAndGetItem_WorksTogether()
detailedResult.Content.Fields.ShouldNotBeNull("Detailed fields should be populated");
detailedResult.Content.Tags.ShouldNotBeNull("Tags should be populated");
- Console.WriteLine($"End-to-end test successful:");
- Console.WriteLine($" Search found: {contentItem.WebTitle}");
- Console.WriteLine($" Retrieved same item with {detailedResult.Content.Tags.Count} tags");
- Console.WriteLine($" Body content length: {detailedResult.Content.Fields.Body?.Length ?? 0} characters");
+ Console.WriteLine("=== END-TO-END TEST RESULTS ===");
+ Console.WriteLine();
+ Console.WriteLine($"SEARCH RESULT:");
+ Console.WriteLine($" Title: {contentItem.WebTitle}");
+ Console.WriteLine($" Section: {contentItem.SectionName} ({contentItem.SectionId})");
+ Console.WriteLine($" Published: {contentItem.WebPublicationDate}");
+ Console.WriteLine($" URL: {contentItem.WebUrl}");
+ Console.WriteLine();
+
+ Console.WriteLine($"DETAILED CONTENT:");
+ Console.WriteLine($" ID: {detailedResult.Content.Id}");
+ Console.WriteLine($" Headline: {detailedResult.Content.Fields.Headline ?? "N/A"}");
+ Console.WriteLine($" Tags: {detailedResult.Content.Tags.Count} tags");
+
+ if (detailedResult.Content.Tags.Any())
+ {
+ Console.WriteLine($" Tag List:");
+ foreach (var tag in detailedResult.Content.Tags.Take(10)) // Show first 10 tags
+ {
+ Console.WriteLine($" - {tag.WebTitle} ({tag.Type})");
+ }
+ if (detailedResult.Content.Tags.Count > 10)
+ {
+ Console.WriteLine($" ... and {detailedResult.Content.Tags.Count - 10} more tags");
+ }
+ }
+
+ Console.WriteLine();
+ Console.WriteLine($"FULL ARTICLE BODY:");
+ Console.WriteLine("==================");
+ if (!string.IsNullOrEmpty(detailedResult.Content.Fields.Body))
+ {
+ Console.WriteLine(detailedResult.Content.Fields.Body);
+ }
+ else
+ {
+ Console.WriteLine("No body content available");
+ }
+ Console.WriteLine("==================");
+ Console.WriteLine();
}
[TestMethod]
@@ -78,8 +114,8 @@ public async Task SearchWithComplexOptions_ReturnsExpectedResults()
},
AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions
{
- ShowFields = [ShowFieldsOption.Headline, ShowFieldsOption.Score],
- ShowTags = [ShowTagsOption.Tone]
+ ShowFields = [GuardianApiContentShowFieldsOption.Headline, GuardianApiContentShowFieldsOption.Score],
+ ShowTags = [GuardianApiContentShowTagsOption.Tone]
}
});
@@ -112,12 +148,12 @@ public async Task TypeSafetyTest_EnumsMapToCorrectApiValues()
{
ShowFields =
[
- ShowFieldsOption.Headline,
- ShowFieldsOption.TrailText,
- ShowFieldsOption.ShowInRelatedContent
+ GuardianApiContentShowFieldsOption.Headline,
+ GuardianApiContentShowFieldsOption.TrailText,
+ GuardianApiContentShowFieldsOption.ShowInRelatedContent
],
- ShowTags = [ShowTagsOption.Tone, ShowTagsOption.Type],
- ShowElements = [ShowElementsOption.Image]
+ ShowTags = [GuardianApiContentShowTagsOption.Tone, GuardianApiContentShowTagsOption.Type],
+ ShowElements = [GuardianApiContentShowElementsOption.Image]
}
});
diff --git a/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs b/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs
index a346136..02bfbb4 100644
--- a/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs
+++ b/GuardianClient/GuardianClient.Tests/SearchAsyncTests.cs
@@ -125,8 +125,8 @@ public async Task SearchAsync_WithAdditionalInformation_ReturnsEnhancedResults()
PageOptions = new GuardianApiContentPageOptions { PageSize = 2 },
AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions
{
- ShowFields = [ShowFieldsOption.Headline, ShowFieldsOption.Thumbnail],
- ShowTags = [ShowTagsOption.Keyword, ShowTagsOption.Tone]
+ ShowFields = [GuardianApiContentShowFieldsOption.Headline, GuardianApiContentShowFieldsOption.Thumbnail],
+ ShowTags = [GuardianApiContentShowTagsOption.Keyword, GuardianApiContentShowTagsOption.Tone]
}
});
diff --git a/GuardianClient/GuardianClient/Internal/AdditionalInformationExtensions.cs b/GuardianClient/GuardianClient/Internal/AdditionalInformationExtensions.cs
new file mode 100644
index 0000000..0327232
--- /dev/null
+++ b/GuardianClient/GuardianClient/Internal/AdditionalInformationExtensions.cs
@@ -0,0 +1,95 @@
+using GuardianClient.Options.Search;
+
+namespace GuardianClient.Internal;
+
+///
+/// Extension methods for converting additional information enums to their API string values.
+///
+internal static class AdditionalInformationExtensions
+{
+ internal static string ToApiString(this GuardianApiContentShowFieldsOption option) => option switch
+ {
+ GuardianApiContentShowFieldsOption.TrailText => "trailText",
+ GuardianApiContentShowFieldsOption.Headline => "headline",
+ GuardianApiContentShowFieldsOption.ShowInRelatedContent => "showInRelatedContent",
+ GuardianApiContentShowFieldsOption.Body => "body",
+ GuardianApiContentShowFieldsOption.LastModified => "lastModified",
+ GuardianApiContentShowFieldsOption.HasStoryPackage => "hasStoryPackage",
+ GuardianApiContentShowFieldsOption.Score => "score",
+ GuardianApiContentShowFieldsOption.Standfirst => "standfirst",
+ GuardianApiContentShowFieldsOption.ShortUrl => "shortUrl",
+ GuardianApiContentShowFieldsOption.Thumbnail => "thumbnail",
+ GuardianApiContentShowFieldsOption.Wordcount => "wordcount",
+ GuardianApiContentShowFieldsOption.Commentable => "commentable",
+ GuardianApiContentShowFieldsOption.IsPremoderated => "isPremoderated",
+ GuardianApiContentShowFieldsOption.AllowUgc => "allowUgc",
+ GuardianApiContentShowFieldsOption.Byline => "byline",
+ GuardianApiContentShowFieldsOption.Publication => "publication",
+ GuardianApiContentShowFieldsOption.InternalPageCode => "internalPageCode",
+ GuardianApiContentShowFieldsOption.ProductionOffice => "productionOffice",
+ GuardianApiContentShowFieldsOption.ShouldHideAdverts => "shouldHideAdverts",
+ GuardianApiContentShowFieldsOption.LiveBloggingNow => "liveBloggingNow",
+ GuardianApiContentShowFieldsOption.CommentCloseDate => "commentCloseDate",
+ GuardianApiContentShowFieldsOption.StarRating => "starRating",
+ GuardianApiContentShowFieldsOption.All => "all",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+
+ internal static string ToApiString(this GuardianApiContentShowTagsOption option) => option switch
+ {
+ GuardianApiContentShowTagsOption.Blog => "blog",
+ GuardianApiContentShowTagsOption.Contributor => "contributor",
+ GuardianApiContentShowTagsOption.Keyword => "keyword",
+ GuardianApiContentShowTagsOption.NewspaperBook => "newspaper-book",
+ GuardianApiContentShowTagsOption.NewspaperBookSection => "newspaper-book-section",
+ GuardianApiContentShowTagsOption.Publication => "publication",
+ GuardianApiContentShowTagsOption.Series => "series",
+ GuardianApiContentShowTagsOption.Tone => "tone",
+ GuardianApiContentShowTagsOption.Type => "type",
+ GuardianApiContentShowTagsOption.All => "all",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+
+ internal static string ToApiString(this GuardianApiContentShowElementsOption option) => option switch
+ {
+ GuardianApiContentShowElementsOption.Audio => "audio",
+ GuardianApiContentShowElementsOption.Image => "image",
+ GuardianApiContentShowElementsOption.Video => "video",
+ GuardianApiContentShowElementsOption.All => "all",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+
+ internal static string ToApiString(this GuardianApiContentShowReferencesOption option) => option switch
+ {
+ GuardianApiContentShowReferencesOption.Author => "author",
+ GuardianApiContentShowReferencesOption.BisacPrefix => "bisac-prefix",
+ GuardianApiContentShowReferencesOption.EsaCricketMatch => "esa-cricket-match",
+ GuardianApiContentShowReferencesOption.EsaFootballMatch => "esa-football-match",
+ GuardianApiContentShowReferencesOption.EsaFootballTeam => "esa-football-team",
+ GuardianApiContentShowReferencesOption.EsaFootballTournament => "esa-football-tournament",
+ GuardianApiContentShowReferencesOption.Isbn => "isbn",
+ GuardianApiContentShowReferencesOption.Imdb => "imdb",
+ GuardianApiContentShowReferencesOption.Musicbrainz => "musicbrainz",
+ GuardianApiContentShowReferencesOption.MusicbrainzGenre => "musicbrainzgenre",
+ GuardianApiContentShowReferencesOption.OptaCricketMatch => "opta-cricket-match",
+ GuardianApiContentShowReferencesOption.OptaFootballMatch => "opta-football-match",
+ GuardianApiContentShowReferencesOption.OptaFootballTeam => "opta-football-team",
+ GuardianApiContentShowReferencesOption.OptaFootballTournament => "opta-football-tournament",
+ GuardianApiContentShowReferencesOption.PaFootballCompetition => "pa-football-competition",
+ GuardianApiContentShowReferencesOption.PaFootballMatch => "pa-football-match",
+ GuardianApiContentShowReferencesOption.PaFootballTeam => "pa-football-team",
+ GuardianApiContentShowReferencesOption.R1Film => "r1-film",
+ GuardianApiContentShowReferencesOption.ReutersIndexRic => "reuters-index-ric",
+ GuardianApiContentShowReferencesOption.ReutersStockRic => "reuters-stock-ric",
+ GuardianApiContentShowReferencesOption.WitnessAssignment => "witness-assignment",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+
+ internal static string ToApiString(this GuardianApiContentShowRightsOption option) => option switch
+ {
+ GuardianApiContentShowRightsOption.Syndicatable => "syndicatable",
+ GuardianApiContentShowRightsOption.SubscriptionDatabases => "subscription-databases",
+ GuardianApiContentShowRightsOption.All => "all",
+ _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
+ };
+}
diff --git a/GuardianClient/GuardianClient/Options/Search/AdditionalInformationEnums.cs b/GuardianClient/GuardianClient/Options/Search/AdditionalInformationEnums.cs
deleted file mode 100644
index b3dc492..0000000
--- a/GuardianClient/GuardianClient/Options/Search/AdditionalInformationEnums.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-namespace GuardianClient.Options.Search;
-
-///
-/// Fields that can be included with content responses.
-///
-public enum ShowFieldsOption
-{
- TrailText,
- Headline,
- ShowInRelatedContent,
- Body,
- LastModified,
- HasStoryPackage,
- Score,
- Standfirst,
- ShortUrl,
- Thumbnail,
- Wordcount,
- Commentable,
- IsPremoderated,
- AllowUgc,
- Byline,
- Publication,
- InternalPageCode,
- ProductionOffice,
- ShouldHideAdverts,
- LiveBloggingNow,
- CommentCloseDate,
- StarRating,
- All
-}
-
-///
-/// Tag types that can be included with content responses.
-///
-public enum ShowTagsOption
-{
- Blog,
- Contributor,
- Keyword,
- NewspaperBook,
- NewspaperBookSection,
- Publication,
- Series,
- Tone,
- Type,
- All
-}
-
-///
-/// Media element types that can be included with content responses.
-///
-public enum ShowElementsOption
-{
- Audio,
- Image,
- Video,
- All
-}
-
-///
-/// Reference types that can be included with content responses.
-///
-public enum ShowReferencesOption
-{
- Author,
- BisacPrefix,
- EsaCricketMatch,
- EsaFootballMatch,
- EsaFootballTeam,
- EsaFootballTournament,
- Isbn,
- Imdb,
- Musicbrainz,
- MusicbrainzGenre,
- OptaCricketMatch,
- OptaFootballMatch,
- OptaFootballTeam,
- OptaFootballTournament,
- PaFootballCompetition,
- PaFootballMatch,
- PaFootballTeam,
- R1Film,
- ReutersIndexRic,
- ReutersStockRic,
- WitnessAssignment
-}
-
-///
-/// Rights types that can be included with content responses.
-///
-public enum ShowRightsOption
-{
- Syndicatable,
- SubscriptionDatabases,
- All
-}
-
-///
-/// Extension methods for converting additional information enums to their API string values.
-///
-internal static class AdditionalInformationEnumExtensions
-{
- internal static string ToApiString(this ShowFieldsOption option) => option switch
- {
- ShowFieldsOption.TrailText => "trailText",
- ShowFieldsOption.Headline => "headline",
- ShowFieldsOption.ShowInRelatedContent => "showInRelatedContent",
- ShowFieldsOption.Body => "body",
- ShowFieldsOption.LastModified => "lastModified",
- ShowFieldsOption.HasStoryPackage => "hasStoryPackage",
- ShowFieldsOption.Score => "score",
- ShowFieldsOption.Standfirst => "standfirst",
- ShowFieldsOption.ShortUrl => "shortUrl",
- ShowFieldsOption.Thumbnail => "thumbnail",
- ShowFieldsOption.Wordcount => "wordcount",
- ShowFieldsOption.Commentable => "commentable",
- ShowFieldsOption.IsPremoderated => "isPremoderated",
- ShowFieldsOption.AllowUgc => "allowUgc",
- ShowFieldsOption.Byline => "byline",
- ShowFieldsOption.Publication => "publication",
- ShowFieldsOption.InternalPageCode => "internalPageCode",
- ShowFieldsOption.ProductionOffice => "productionOffice",
- ShowFieldsOption.ShouldHideAdverts => "shouldHideAdverts",
- ShowFieldsOption.LiveBloggingNow => "liveBloggingNow",
- ShowFieldsOption.CommentCloseDate => "commentCloseDate",
- ShowFieldsOption.StarRating => "starRating",
- ShowFieldsOption.All => "all",
- _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
- };
-
- internal static string ToApiString(this ShowTagsOption option) => option switch
- {
- ShowTagsOption.Blog => "blog",
- ShowTagsOption.Contributor => "contributor",
- ShowTagsOption.Keyword => "keyword",
- ShowTagsOption.NewspaperBook => "newspaper-book",
- ShowTagsOption.NewspaperBookSection => "newspaper-book-section",
- ShowTagsOption.Publication => "publication",
- ShowTagsOption.Series => "series",
- ShowTagsOption.Tone => "tone",
- ShowTagsOption.Type => "type",
- ShowTagsOption.All => "all",
- _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
- };
-
- internal static string ToApiString(this ShowElementsOption option) => option switch
- {
- ShowElementsOption.Audio => "audio",
- ShowElementsOption.Image => "image",
- ShowElementsOption.Video => "video",
- ShowElementsOption.All => "all",
- _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
- };
-
- internal static string ToApiString(this ShowReferencesOption option) => option switch
- {
- ShowReferencesOption.Author => "author",
- ShowReferencesOption.BisacPrefix => "bisac-prefix",
- ShowReferencesOption.EsaCricketMatch => "esa-cricket-match",
- ShowReferencesOption.EsaFootballMatch => "esa-football-match",
- ShowReferencesOption.EsaFootballTeam => "esa-football-team",
- ShowReferencesOption.EsaFootballTournament => "esa-football-tournament",
- ShowReferencesOption.Isbn => "isbn",
- ShowReferencesOption.Imdb => "imdb",
- ShowReferencesOption.Musicbrainz => "musicbrainz",
- ShowReferencesOption.MusicbrainzGenre => "musicbrainzgenre",
- ShowReferencesOption.OptaCricketMatch => "opta-cricket-match",
- ShowReferencesOption.OptaFootballMatch => "opta-football-match",
- ShowReferencesOption.OptaFootballTeam => "opta-football-team",
- ShowReferencesOption.OptaFootballTournament => "opta-football-tournament",
- ShowReferencesOption.PaFootballCompetition => "pa-football-competition",
- ShowReferencesOption.PaFootballMatch => "pa-football-match",
- ShowReferencesOption.PaFootballTeam => "pa-football-team",
- ShowReferencesOption.R1Film => "r1-film",
- ShowReferencesOption.ReutersIndexRic => "reuters-index-ric",
- ShowReferencesOption.ReutersStockRic => "reuters-stock-ric",
- ShowReferencesOption.WitnessAssignment => "witness-assignment",
- _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
- };
-
- internal static string ToApiString(this ShowRightsOption option) => option switch
- {
- ShowRightsOption.Syndicatable => "syndicatable",
- ShowRightsOption.SubscriptionDatabases => "subscription-databases",
- ShowRightsOption.All => "all",
- _ => throw new ArgumentOutOfRangeException(nameof(option), option, null)
- };
-}
\ No newline at end of file
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs
index 22373f8..f0a4f81 100644
--- a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentAdditionalInformationOptions.cs
@@ -11,22 +11,22 @@ public class GuardianApiContentAdditionalInformationOptions
///
/// Add fields associated with the content such as headline, body, thumbnail, etc.
///
- public ShowFieldsOption[]? ShowFields { get; set; }
+ public GuardianApiContentShowFieldsOption[]? ShowFields { get; set; }
///
/// Add associated metadata tags such as contributor, keyword, tone, etc.
///
- public ShowTagsOption[]? ShowTags { get; set; }
+ public GuardianApiContentShowTagsOption[]? ShowTags { get; set; }
///
/// Add associated media elements such as images, audio, and video.
///
- public ShowElementsOption[]? ShowElements { get; set; }
+ public GuardianApiContentShowElementsOption[]? ShowElements { get; set; }
///
/// Add associated reference data such as ISBNs, IMDB IDs, author references, etc.
///
- public ShowReferencesOption[]? ShowReferences { get; set; }
+ public GuardianApiContentShowReferencesOption[]? ShowReferences { get; set; }
///
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowElementsOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowElementsOption.cs
new file mode 100644
index 0000000..275fe03
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowElementsOption.cs
@@ -0,0 +1,12 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Media element types that can be included with content responses.
+///
+public enum GuardianApiContentShowElementsOption
+{
+ Audio,
+ Image,
+ Video,
+ All
+}
\ No newline at end of file
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowFieldsOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowFieldsOption.cs
new file mode 100644
index 0000000..32e0251
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowFieldsOption.cs
@@ -0,0 +1,31 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Fields that can be included with content responses.
+///
+public enum GuardianApiContentShowFieldsOption
+{
+ TrailText,
+ Headline,
+ ShowInRelatedContent,
+ Body,
+ LastModified,
+ HasStoryPackage,
+ Score,
+ Standfirst,
+ ShortUrl,
+ Thumbnail,
+ Wordcount,
+ Commentable,
+ IsPremoderated,
+ AllowUgc,
+ Byline,
+ Publication,
+ InternalPageCode,
+ ProductionOffice,
+ ShouldHideAdverts,
+ LiveBloggingNow,
+ CommentCloseDate,
+ StarRating,
+ All
+}
\ No newline at end of file
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowReferencesOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowReferencesOption.cs
new file mode 100644
index 0000000..663bb09
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowReferencesOption.cs
@@ -0,0 +1,29 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Reference types that can be included with content responses.
+///
+public enum GuardianApiContentShowReferencesOption
+{
+ Author,
+ BisacPrefix,
+ EsaCricketMatch,
+ EsaFootballMatch,
+ EsaFootballTeam,
+ EsaFootballTournament,
+ Isbn,
+ Imdb,
+ Musicbrainz,
+ MusicbrainzGenre,
+ OptaCricketMatch,
+ OptaFootballMatch,
+ OptaFootballTeam,
+ OptaFootballTournament,
+ PaFootballCompetition,
+ PaFootballMatch,
+ PaFootballTeam,
+ R1Film,
+ ReutersIndexRic,
+ ReutersStockRic,
+ WitnessAssignment
+}
\ No newline at end of file
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowRightsOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowRightsOption.cs
new file mode 100644
index 0000000..0a57c62
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowRightsOption.cs
@@ -0,0 +1,11 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Rights types that can be included with content responses.
+///
+public enum GuardianApiContentShowRightsOption
+{
+ Syndicatable,
+ SubscriptionDatabases,
+ All
+}
\ No newline at end of file
diff --git a/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowTagsOption.cs b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowTagsOption.cs
new file mode 100644
index 0000000..d186193
--- /dev/null
+++ b/GuardianClient/GuardianClient/Options/Search/GuardianApiContentShowTagsOption.cs
@@ -0,0 +1,18 @@
+namespace GuardianClient.Options.Search;
+
+///
+/// Tag types that can be included with content responses.
+///
+public enum GuardianApiContentShowTagsOption
+{
+ Blog,
+ Contributor,
+ Keyword,
+ NewspaperBook,
+ NewspaperBookSection,
+ Publication,
+ Series,
+ Tone,
+ Type,
+ All
+}
\ No newline at end of file
From 7a6249256fc4902bd6cd985a74858a3f6f403764 Mon Sep 17 00:00:00 2001
From: Andrew Tarr
Date: Mon, 11 Aug 2025 20:53:18 -0500
Subject: [PATCH 7/7] README.md updates
---
README.md | 190 ++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 163 insertions(+), 27 deletions(-)
diff --git a/README.md b/README.md
index b8a74d2..8f78dc4 100644
--- a/README.md
+++ b/README.md
@@ -6,19 +6,21 @@
-A modern .NET client library for The Guardian's Content API. Provides strongly-typed models and simple methods for
-searching Guardian articles, tags, sections, and more.
+A modern, comprehensive .NET client library for The Guardian's Content API. Provides strongly-typed models, flexible search options, and simple methods for searching Guardian articles with full API feature support.
[](https://www.nuget.org/packages/GuardianClient.NET)
[](https://github.com/tarrball/GuardianClient.NET/actions)
## โจ Features
-- ๐ **Content Search** - Search Guardian articles with full query support
-- ๐ท๏ธ **Strongly Typed** - Complete C# models for all API responses
-- ๐ง **Dependency Injection** - Easy setup with `services.AddGuardianApiClient()`
-- ๐ฆ **HttpClientFactory** - Proper HttpClient lifecycle management
-- โก **Async/Await** - Modern async patterns with cancellation support
+- ๐ **Complete Content Search** - Full Guardian Content API search with all parameters
+- ๐ท๏ธ **Strongly Typed** - Type-safe enums for fields, tags, elements, and references
+- ๐ฏ **Comprehensive Filtering** - Search by section, tags, date ranges, production office, and more
+- ๐ **Rich Content Options** - Include fields, tags, media elements, blocks, and references
+- ๐ง **Interface-Based Design** - Easy mocking and dependency injection with `IGuardianApiClient`
+- ๐ฆ **HttpClientFactory Ready** - Proper HttpClient lifecycle management
+- โก **Modern Async** - Full async/await patterns with cancellation support
+- ๐จ **Clean API** - Organized option classes for maintainable code
## ๐ Quick Start
@@ -28,37 +30,129 @@ searching Guardian articles, tags, sections, and more.
dotnet add package GuardianClient.NET
```
-### Setup with Dependency Injection
+### Setup with Dependency Injection (Recommended)
```csharp
// Program.cs or Startup.cs
builder.Services.AddGuardianApiClient("your-api-key");
```
-### Usage
+### Basic Usage
```csharp
public class NewsService
{
- private readonly GuardianApiClient _client;
+ private readonly IGuardianApiClient _client;
- public NewsService(GuardianApiClient client)
+ public NewsService(IGuardianApiClient client)
{
_client = client;
}
- public async Task GetLatestNews()
+ public async Task GetLatestTechNews()
{
- return await _client.SearchAsync("politics", pageSize: 10);
+ return await _client.SearchAsync(new GuardianApiContentSearchOptions
+ {
+ Query = "artificial intelligence",
+ FilterOptions = new GuardianApiContentFilterOptions
+ {
+ Section = "technology"
+ },
+ PageOptions = new GuardianApiContentPageOptions
+ {
+ PageSize = 10
+ }
+ });
}
}
```
+## ๐ง Advanced Usage
+
+### Comprehensive Search with All Options
+
+```csharp
+var results = await client.SearchAsync(new GuardianApiContentSearchOptions
+{
+ // Search terms with boolean operators
+ Query = "climate change AND (policy OR legislation)",
+ QueryFields = ["body", "headline"],
+
+ // Filtering options
+ FilterOptions = new GuardianApiContentFilterOptions
+ {
+ Section = "environment",
+ Tag = "climate-change",
+ Language = "en"
+ },
+
+ // Date filtering
+ DateOptions = new GuardianApiContentDateOptions
+ {
+ FromDate = new DateOnly(2023, 1, 1),
+ UseDate = "published"
+ },
+
+ // Pagination
+ PageOptions = new GuardianApiContentPageOptions
+ {
+ Page = 1,
+ PageSize = 20
+ },
+
+ // Ordering
+ OrderOptions = new GuardianApiContentOrderOptions
+ {
+ OrderBy = GuardianApiContentOrderBy.Relevance,
+ OrderDate = GuardianApiContentOrderDate.Published
+ },
+
+ // Additional content
+ AdditionalInformationOptions = new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowFields = [
+ GuardianApiContentShowFieldsOption.Headline,
+ GuardianApiContentShowFieldsOption.Body,
+ GuardianApiContentShowFieldsOption.Thumbnail
+ ],
+ ShowTags = [
+ GuardianApiContentShowTagsOption.Keyword,
+ GuardianApiContentShowTagsOption.Tone
+ ],
+ ShowElements = [GuardianApiContentShowElementsOption.Image]
+ }
+});
+```
+
+### Getting Individual Articles
+
+```csharp
+// Get a specific article by ID
+var article = await client.GetItemAsync("world/2023/oct/15/climate-summit-agreement",
+ new GuardianApiContentAdditionalInformationOptions
+ {
+ ShowFields = [
+ GuardianApiContentShowFieldsOption.Body,
+ GuardianApiContentShowFieldsOption.Byline
+ ],
+ ShowTags = [GuardianApiContentShowTagsOption.All]
+ });
+
+// Access the rich content
+Console.WriteLine(article.Content.WebTitle);
+Console.WriteLine(article.Content.Fields.Body); // Full HTML content
+Console.WriteLine($"Author: {article.Content.Fields.Byline}");
+```
+
### Manual Setup (without DI)
```csharp
using var client = new GuardianApiClient("your-api-key");
-var results = await client.SearchAsync("climate change", pageSize: 5);
+var results = await client.SearchAsync(new GuardianApiContentSearchOptions
+{
+ Query = "sports",
+ PageOptions = new GuardianApiContentPageOptions { PageSize = 5 }
+});
foreach (var article in results.Results)
{
@@ -69,27 +163,69 @@ foreach (var article in results.Results)
## ๐ Getting an API Key
1. Visit [The Guardian Open Platform](https://open-platform.theguardian.com/access/)
-2. Sign up for a free developer account
+2. Sign up for a free developer account
3. Generate your API key
+4. Store it securely (use User Secrets for development)
+
+## ๐๏ธ Available Options
+
+### Filter Options
+- **Section**: Filter by Guardian sections (e.g., "politics", "sport", "culture")
+- **Tags**: Filter by content tags with boolean operators
+- **Date Range**: Filter by publication date with flexible date types
+- **Language**: Filter by content language (ISO codes)
+- **Production Office**: Filter by Guardian production offices
+- **Star Rating**: Filter by review ratings (1-5)
+
+### Additional Content Options
+- **ShowFields**: Include extra fields like body content, thumbnails, bylines
+- **ShowTags**: Include metadata tags (keywords, contributors, tone)
+- **ShowElements**: Include media elements (images, videos, audio)
+- **ShowReferences**: Include reference data (ISBNs, IMDB IDs, etc.)
+- **ShowBlocks**: Include content blocks (useful for live blogs)
+
+### Ordering Options
+- **OrderBy**: Sort by newest, oldest, or relevance
+- **OrderDate**: Choose which date to use for sorting
-## ๐ Current Status
+## ๐ Current Status
-**โ
Implemented:**
+**โ
Fully Implemented:**
+- Complete Content API search with all parameters
+- Individual article retrieval
+- Strongly-typed models and enums for all options
+- Advanced filtering, pagination, and sorting
+- Rich content enhancement options
+- Interface-based design for easy testing
+- Comprehensive documentation and examples
-- Content search with basic parameters
-- Strongly-typed response models
-- Dependency injection support
-- HttpClientFactory integration
+**๐ฏ Feature Complete:** This library now supports the full Guardian Content API specification.
-**๐ In Development:**
+## ๐งช Testing
-- Additional endpoints (tags, sections, editions)
-- Advanced search parameters
-- Deep pagination support
+The library includes comprehensive test coverage:
+
+```bash
+# Run all tests
+dotnet test
+
+# Run with detailed output
+dotnet test --logger "console;verbosity=detailed"
+```
+
+Tests require a Guardian API key stored in user secrets:
+```bash
+dotnet user-secrets set "GuardianApiKey" "your-api-key-here"
+```
## ๐ค Contributing
-This is an early-stage project. Issues and pull requests are welcome!
+Contributions are welcome! Please:
+1. Fork the repository
+2. Create a feature branch
+3. Add tests for new functionality
+4. Ensure all tests pass
+5. Submit a pull request
## ๐ License
@@ -97,4 +233,4 @@ MIT License - see [LICENSE](LICENSE) for details.
---
-*This is an unofficial library and is not affiliated with The Guardian.*
+*This is an unofficial library and is not affiliated with The Guardian.*
\ No newline at end of file