Why use the .NET SDK
Section titled “Why use the .NET SDK”Raw GraphQL queries work from any HTTP client, but they require you to manage string building, serialization, authentication headers, and error parsing yourself. The Graph .NET SDK gives you a strongly-typed, LINQ-style query builder that catches mistakes at compile time, handles authentication automatically, and deserializes responses into your own C# types.
Use the SDK when your delivery layer or backend service runs on .NET. Use raw GraphQL when your frontend is JavaScript-based or when you need maximum flexibility over the query shape.
Installation
Section titled “Installation”dotnet add package Optimizely.ContentGraph.Client <PackageReference Include="Optimizely.ContentGraph.Client" Version="3.*" /> Client initialization
Section titled “Client initialization”The GraphClient is the entry point for all queries. You need a Graph gateway URL and an authentication credential.
using Optimizely.ContentGraph.Client;
var client = new GraphClient(
new Uri("https://cg.optimizely.com/content/v2"),
new SingleKeyAuthHandler("your-single-key")
); using Optimizely.ContentGraph.Client;
var client = new GraphClient(
new Uri("https://cg.optimizely.com/content/v2"),
new HmacAuthHandler("your-app-key", "your-secret")
); // In Program.cs or Startup.cs
services.AddSingleton<GraphClient>(sp =>
new GraphClient(
new Uri(Configuration["Graph:GatewayUrl"]),
new SingleKeyAuthHandler(Configuration["Graph:SingleKey"])
)
);
// In a controller or service
public class ArticleService
{
private readonly GraphClient _graph;
public ArticleService(GraphClient graph)
{
_graph = graph;
}
} Configuration options
Section titled “Configuration options”| Parameter | Required | Description |
|---|---|---|
gatewayUrl | Yes | Graph endpoint URL. Typically https://cg.optimizely.com/content/v2 |
authHandler | Yes | Authentication handler — SingleKeyAuthHandler or HmacAuthHandler |
httpClient | No | Custom HttpClient instance for proxy or logging scenarios |
timeout | No | Request timeout. Default: 30 seconds |
Authentication
Section titled “Authentication”SingleKey
Section titled “SingleKey”SingleKey authentication uses a single API token passed as a bearer header. Suitable for public-facing websites and static site generation where the key can be stored in environment variables.
var auth = new SingleKeyAuthHandler("your-single-key");
// The SDK adds the header automatically:
// Authorization: Bearer your-single-key HMAC authentication uses a key pair (app key + secret) to sign each request. The SDK computes the HMAC-SHA256 signature automatically. Use this for server-to-server integrations where you cannot expose API keys to the browser.
var auth = new HmacAuthHandler("your-app-key", "your-secret");
// The SDK computes a per-request signature:
// Authorization: epi-hmac your-app-key:timestamp:nonce:signature | Auth method | Use case | Key exposure risk |
|---|---|---|
| SingleKey | Public websites, SSG builds | Low — stored server-side |
| HMAC | Server-to-server, secure APIs | None — secret never leaves server |
Querying content types
Section titled “Querying content types”Every content type defined in CMS becomes a queryable type in Graph. The SDK uses a generic ForType<T>() method to start building a typed query.
// Define a model matching your CMS content type
public class ArticlePage
{
public string Headline { get; set; }
public string Author { get; set; }
public DateTime PublishedDate { get; set; }
public string Body { get; set; }
}
// Query articles
var result = await client
.ForType<ArticlePage>()
.Fields(x => x.Headline, x => x.Author, x => x.PublishedDate)
.GetResultAsync();
foreach (var article in result.Items)
{
Console.WriteLine($"{article.Headline} by {article.Author}");
} // Only fetch the fields you need to reduce payload size
var result = await client
.ForType<ArticlePage>()
.Fields(x => x.Headline, x => x.PublishedDate)
.GetResultAsync();
// result.Items contains ArticlePage objects
// with only Headline and PublishedDate populated Content model classes
Section titled “Content model classes”Your C# model classes should have properties matching the CMS content type field names. The SDK maps GraphQL response fields to these properties by name.
// Page type
public class ProductPage
{
public string Name { get; set; }
public string Description { get; set; }
public double Price { get; set; }
public string Category { get; set; }
public string ImageUrl { get; set; }
public ContentMetadata _metadata { get; set; }
}
// Metadata (available on all content)
public class ContentMetadata
{
public string Key { get; set; }
public string Locale { get; set; }
public DateTime Published { get; set; }
public string Status { get; set; }
public ContentUrl Url { get; set; }
}
public class ContentUrl
{
public string Default { get; set; }
} Filtering
Section titled “Filtering”The SDK provides a fluent Where method with operators that mirror the GraphQL filtering syntax.
// Equals
var published = await client
.ForType<ArticlePage>()
.Where(x => x.Status.Eq("Published"))
.GetResultAsync();
// Greater than
var recent = await client
.ForType<ArticlePage>()
.Where(x => x.PublishedDate.Gte(DateTime.UtcNow.AddDays(-30)))
.GetResultAsync();
// In list
var filtered = await client
.ForType<ArticlePage>()
.Where(x => x.Category.In(new[] { "News", "Blog" }))
.GetResultAsync();
// Contains substring
var matching = await client
.ForType<ArticlePage>()
.Where(x => x.Headline.Contains("optimizely"))
.GetResultAsync(); // AND — chain multiple Where calls
var result = await client
.ForType<ArticlePage>()
.Where(x => x.Status.Eq("Published"))
.Where(x => x.Category.Eq("News"))
.GetResultAsync();
// OR — use the Or() combinator
var result = await client
.ForType<ArticlePage>()
.Where(x => x.Category.Eq("News").Or(x.Category.Eq("Blog")))
.GetResultAsync();
// Exists — check that a field has a value
var withImages = await client
.ForType<ArticlePage>()
.Where(x => x.HeroImage.Exists(true))
.GetResultAsync(); Filter operator reference
Section titled “Filter operator reference”| SDK method | GraphQL equivalent | Description |
|---|---|---|
.Eq(value) | eq | Exact match |
.NotEq(value) | notEq | Not equal |
.Gt(value) | gt | Greater than |
.Gte(value) | gte | Greater than or equal |
.Lt(value) | lt | Less than |
.Lte(value) | lte | Less than or equal |
.In(values) | in | Value in list |
.NotIn(values) | notIn | Value not in list |
.Like(pattern) | like | Wildcard match (% as wildcard) |
.Contains(text) | contains | Substring match |
.StartsWith(text) | startsWith | Prefix match |
.Exists(bool) | exist | Field has (or lacks) a value |
Sorting
Section titled “Sorting”// Single field, descending
var result = await client
.ForType<ArticlePage>()
.OrderBy(x => x.PublishedDate, OrderBy.DESC)
.GetResultAsync();
// Multiple fields
var result = await client
.ForType<ArticlePage>()
.OrderBy(x => x.Category, OrderBy.ASC)
.ThenBy(x => x.PublishedDate, OrderBy.DESC)
.GetResultAsync();
// Sort by relevance (when using full-text search)
var result = await client
.ForType<ArticlePage>()
.Search("content modeling")
.OrderByRanking(Ranking.RELEVANCE)
.GetResultAsync(); | Sort direction | Constant | Description |
|---|---|---|
| Ascending | OrderBy.ASC | A to Z, oldest first, lowest first |
| Descending | OrderBy.DESC | Z to A, newest first, highest first |
Pagination
Section titled “Pagination”Graph supports two pagination strategies. Cursor-based pagination is recommended for performance.
// First page
var page1 = await client
.ForType<ArticlePage>()
.Limit(10)
.GetResultAsync();
Console.WriteLine($"Total: {page1.Total}");
// Next page — pass the cursor from the previous result
if (page1.Cursor != null)
{
var page2 = await client
.ForType<ArticlePage>()
.Limit(10)
.Cursor(page1.Cursor)
.GetResultAsync();
} // Skip-based pagination (simpler but slower for deep pages)
int pageSize = 10;
int pageNumber = 3;
var result = await client
.ForType<ArticlePage>()
.Limit(pageSize)
.Skip(pageSize * (pageNumber - 1))
.GetResultAsync(); // Iterate through all pages using cursor
string cursor = null;
var allItems = new List<ArticlePage>();
do
{
var query = client
.ForType<ArticlePage>()
.Limit(100);
if (cursor != null)
query = query.Cursor(cursor);
var result = await query.GetResultAsync();
allItems.AddRange(result.Items);
cursor = result.Cursor;
}
while (cursor != null); | Strategy | Best for | Limitation |
|---|---|---|
| Cursor | Large datasets, sequential page traversal | Cannot jump to arbitrary page |
| Skip | Small datasets, random page access | Slower beyond 1000 items |
Full-text search
Section titled “Full-text search”The SDK provides a Search method that queries across all fulltext-indexed fields.
var result = await client
.ForType<ArticlePage>()
.Search("content modeling best practices")
.OrderByRanking(Ranking.RELEVANCE)
.Limit(20)
.GetResultAsync();
foreach (var item in result.Items)
{
Console.WriteLine($"{item.Headline} (score: {item._score})");
} // Combine search with filters
var result = await client
.ForType<ArticlePage>()
.Search("getting started")
.Where(x => x.Category.Eq("Tutorial"))
.Where(x => x.PublishedDate.Gte(DateTime.UtcNow.AddMonths(-6)))
.OrderByRanking(Ranking.RELEVANCE)
.Limit(10)
.GetResultAsync(); Ranking modes
Section titled “Ranking modes”| Mode | Constant | Description |
|---|---|---|
| Relevance | Ranking.RELEVANCE | Text match quality scoring |
| Semantic | Ranking.SEMANTIC | AI-powered meaning-based ranking |
Fragment queries
Section titled “Fragment queries”Fragments let you query across multiple content types in a single request by querying a shared base type or interface.
// Query all content types that inherit from a base
var result = await client
.ForType<IContent>()
.Fields(x => x._metadata)
.Search("optimizely")
.Limit(20)
.GetResultAsync();
// Process results — items may be different content types
foreach (var item in result.Items)
{
Console.WriteLine(
$"{item._metadata.Types.First()}: {item._metadata.Url.Default}"
);
} Locale selection
Section titled “Locale selection”// Single locale
var english = await client
.ForType<ArticlePage>()
.Locale("en")
.GetResultAsync();
// Multiple locales
var result = await client
.ForType<ArticlePage>()
.Locale("en", "sv")
.GetResultAsync();
// All locales
var all = await client
.ForType<ArticlePage>()
.LocaleAll()
.GetResultAsync(); Error handling
Section titled “Error handling”The SDK throws specific exception types for different failure modes. Wrap queries in try-catch blocks for production code.
try
{
var result = await client
.ForType<ArticlePage>()
.Where(x => x.Status.Eq("Published"))
.GetResultAsync();
}
catch (GraphAuthenticationException ex)
{
// Invalid or expired credentials
// Action: check your SingleKey or HMAC configuration
logger.LogError("Graph auth failed: {Message}", ex.Message);
}
catch (GraphQueryException ex)
{
// Malformed query or invalid field reference
// Action: verify your model matches the CMS content type
logger.LogError("Graph query error: {Message}", ex.Message);
foreach (var error in ex.Errors)
{
logger.LogError(" - {Path}: {Detail}", error.Path, error.Message);
}
}
catch (GraphTimeoutException ex)
{
// Query exceeded 30-second timeout
// Action: add more filters or reduce result size
logger.LogWarning("Graph query timed out: {Message}", ex.Message);
}
catch (HttpRequestException ex)
{
// Network-level failure
logger.LogError("Network error reaching Graph: {Message}", ex.Message);
} Exception types
Section titled “Exception types”| Exception | Cause | Typical fix |
|---|---|---|
GraphAuthenticationException | Invalid credentials or expired token | Verify API key or HMAC secret |
GraphQueryException | Malformed query, unknown field, type mismatch | Check that your C# model matches the CMS content type |
GraphTimeoutException | Query exceeded 30-second limit | Add filters, reduce Limit, or simplify query |
HttpRequestException | Network unreachable, DNS failure | Check connectivity to cg.optimizely.com |
Result structure
Section titled “Result structure”Every query returns a GraphResult<T> object with the following properties:
| Property | Type | Description |
|---|---|---|
Items | IReadOnlyList<T> | Content items matching the query |
Total | int | Total number of matching items (across all pages) |
Cursor | string? | Cursor for fetching the next page. null when no more results |
Complete example
Section titled “Complete example”using Optimizely.ContentGraph.Client;
// 1. Initialize client
var client = new GraphClient(
new Uri("https://cg.optimizely.com/content/v2"),
new SingleKeyAuthHandler(Environment.GetEnvironmentVariable("GRAPH_KEY"))
);
// 2. Build and execute a query
var result = await client
.ForType<ArticlePage>()
.Fields(x => x.Headline, x => x.Author, x => x.PublishedDate, x => x.Body)
.Where(x => x.Status.Eq("Published"))
.Where(x => x.PublishedDate.Gte(DateTime.UtcNow.AddMonths(-3)))
.Search("content delivery")
.OrderByRanking(Ranking.RELEVANCE)
.Locale("en")
.Limit(10)
.GetResultAsync();
// 3. Process results
Console.WriteLine($"Found {result.Total} articles");
foreach (var article in result.Items)
{
Console.WriteLine($" {article.Headline} — {article.Author}");
Console.WriteLine($" Published: {article.PublishedDate:yyyy-MM-dd}");
}