Skip to content

Graph C# (.NET) SDK Reference

intermediate
📜CoreGraphcms

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.

Install the NuGet package
bash
dotnet add package Optimizely.ContentGraph.Client
xml
<PackageReference Include="Optimizely.ContentGraph.Client" Version="3.*" />

The GraphClient is the entry point for all queries. You need a Graph gateway URL and an authentication credential.

Create a Graph client
csharp
using Optimizely.ContentGraph.Client;

var client = new GraphClient(
    new Uri("https://cg.optimizely.com/content/v2"),
    new SingleKeyAuthHandler("your-single-key")
);
csharp
using Optimizely.ContentGraph.Client;

var client = new GraphClient(
    new Uri("https://cg.optimizely.com/content/v2"),
    new HmacAuthHandler("your-app-key", "your-secret")
);
csharp
// 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;
    }
}
ParameterRequiredDescription
gatewayUrlYesGraph endpoint URL. Typically https://cg.optimizely.com/content/v2
authHandlerYesAuthentication handler — SingleKeyAuthHandler or HmacAuthHandler
httpClientNoCustom HttpClient instance for proxy or logging scenarios
timeoutNoRequest timeout. Default: 30 seconds

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.

SingleKey setup
csharp
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.

HMAC setup
csharp
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 methodUse caseKey exposure risk
SingleKeyPublic websites, SSG buildsLow — stored server-side
HMACServer-to-server, secure APIsNone — secret never leaves server

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.

Query a content type
csharp
// 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}");
}
csharp
// 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

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.

Define content models
csharp
// 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; }
}

The SDK provides a fluent Where method with operators that mirror the GraphQL filtering syntax.

Filter content
csharp
// 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();
csharp
// 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();
SDK methodGraphQL equivalentDescription
.Eq(value)eqExact match
.NotEq(value)notEqNot equal
.Gt(value)gtGreater than
.Gte(value)gteGreater than or equal
.Lt(value)ltLess than
.Lte(value)lteLess than or equal
.In(values)inValue in list
.NotIn(values)notInValue not in list
.Like(pattern)likeWildcard match (% as wildcard)
.Contains(text)containsSubstring match
.StartsWith(text)startsWithPrefix match
.Exists(bool)existField has (or lacks) a value
Sort results
csharp
// 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 directionConstantDescription
AscendingOrderBy.ASCA to Z, oldest first, lowest first
DescendingOrderBy.DESCZ to A, newest first, highest first

Graph supports two pagination strategies. Cursor-based pagination is recommended for performance.

Paginate results
csharp
// 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();
}
csharp
// 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();
csharp
// 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);
StrategyBest forLimitation
CursorLarge datasets, sequential page traversalCannot jump to arbitrary page
SkipSmall datasets, random page accessSlower beyond 1000 items

The SDK provides a Search method that queries across all fulltext-indexed fields.

Full-text search
csharp
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})");
}
csharp
// 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();
ModeConstantDescription
RelevanceRanking.RELEVANCEText match quality scoring
SemanticRanking.SEMANTICAI-powered meaning-based ranking

Fragments let you query across multiple content types in a single request by querying a shared base type or interface.

Fragment queries
csharp
// 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}"
    );
}
Query by locale
csharp
// 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();

The SDK throws specific exception types for different failure modes. Wrap queries in try-catch blocks for production code.

Handle errors
csharp
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);
}
ExceptionCauseTypical fix
GraphAuthenticationExceptionInvalid credentials or expired tokenVerify API key or HMAC secret
GraphQueryExceptionMalformed query, unknown field, type mismatchCheck that your C# model matches the CMS content type
GraphTimeoutExceptionQuery exceeded 30-second limitAdd filters, reduce Limit, or simplify query
HttpRequestExceptionNetwork unreachable, DNS failureCheck connectivity to cg.optimizely.com

Every query returns a GraphResult<T> object with the following properties:

PropertyTypeDescription
ItemsIReadOnlyList<T>Content items matching the query
TotalintTotal number of matching items (across all pages)
Cursorstring?Cursor for fetching the next page. null when no more results
Full query pipeline
csharp
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}");
}