Quick start guide: C# and .NET
Learn how to create a GraphQL client for Optimizely Content Management System (SaaS) using .NET 8 and StrawberryShake.
Build a .NET 8 client that queries Optimizely Content Management System (SaaS) content through Optimizely Graph. This guide covers two approaches: a manual GraphQL client, and a strongly-typed client generated by the StrawberryShake code generator. The StrawberryShake approach ensures type safety, simplifies API calls, and improves developer productivity when fetching and manipulating CMS content. StrawberryShake delivers the following advantages.
- Full IntelliSense support for all GraphQL operations.
- Compile-time type checking.
- Automatic serialization and deserialization.
- Generated documentation from the schema.
Prerequisites
- .NET 8 SDK or later.
- Optimizely CMS (SaaS) instance with published content.
- Optimizely GraphQL endpoint URL and authentication token.
- Basic knowledge of GraphQL queries.
Create the project
Run the following commands to scaffold a new ASP.NET Core Web API project that hosts the GraphQL client.
dotnet new webapi -n MySaasCmsApp
cd MySaasCmsAppAdd the Optimizely NuGet source if your NuGet config does not already include it.
dotnet nuget add source https://api.nuget.optimizely.com/v3/index.json -n OptimizelyInstall dependencies
Install the core GraphQL client packages that every approach in this guide uses.
dotnet add package GraphQL.Client
dotnet add package GraphQL.Client.Serializer.Newtonsoft
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensionsAdd StrawberryShake packages
To use the strongly-typed StrawberryShake client, install the following dependencies.
# Install StrawberryShake tools
dotnet new tool-manifest
dotnet tool install StrawberryShake.Tools
# Add StrawberryShake packages
dotnet add package StrawberryShake.CodeGeneration.CSharp.Analyzers
dotnet add package StrawberryShake.ServerConfigure StrawberryShake
Update the .csproj file to register the StrawberryShake packages and include all .graphql files so the code generator picks them up at build time.
<!-- MySaasCmsApp.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StrawberryShake.AspNetCore" Version="13.9.0" />
<PackageReference Include="StrawberryShake.CodeGeneration.CSharp.Analyzers" Version="13.9.0" />
</ItemGroup>
<ItemGroup>
<GraphQL Include="**/*.graphql" />
</ItemGroup>
</Project>StrawberryShake (GraphQL Codegen) delivers the following benefits.
- Type safety – Compile-time checks, IntelliSense, and no runtime schema surprises.
- Performance – Optimized queries, caching, and efficient serialization.
- Developer experience – Auto-completion, schema documentation, and refactoring support.
- CMS (SaaS) integration – Handles
_metadata, strong typing for URLs, locales, and types, and version-safe development.
Create GraphQL schema and queries
Download the schema from your CMS (SaaS) instance and define the queries the client compiles into strongly-typed methods.
# Download the schema from the Mosey Bank CMS (SaaS) sample
dotnet graphql download https://beta.cg.optimizely.com/content/v2?auth=YOUR_ACCESS_KEY -f ./schema.graphqlCreate the Queries/SaasCmsQueries.graphql file with the following content.
# Queries/SaasCmsQueries.graphql
query GetAllContent($locale: [Locales], $limit: Int = 10, $skip: Int = 0) {
_Content(
locale: $locale
limit: $limit
skip: $skip
orderBy: { _metadata: { lastModified: DESC } }
) {
items {
_metadata {
displayName
key
locale
types
url {
default
hierarchical
}
published
lastModified
}
}
total
}
}
query GetLandingPages($locale: [Locales]) {
LandingPage(locale: $locale) {
item {
_metadata {
displayName
url {
default
}
published
}
}
total
}
}
query GetContentByKey($key: String!, $locale: [Locales]) {
_Content(
where: { _metadata: { key: { eq: $key } } }
locale: $locale
) {
items {
_metadata {
displayName
key
locale
types
url {
default
hierarchical
}
published
lastModified
}
}
}
}Configure the GraphQL client
Initialize the StrawberryShake client against your Optimizely Graph endpoint.
dotnet graphql init "https://beta.cg.optimizely.com/content/v2?auth=YOUR_ACCESS_KEY&stored=true" -n SaasCmsGraphClient -p .Update the generated .graphqlrc.json to set the namespace, expose the client publicly with accessModifier, and point documents at the Queries/*.graphql glob.
# .graphqlrc.json
{
"schema": "schema.graphql",
"documents": "Queries/*.graphql",
"extensions": {
"strawberryShake": {
"name": "SaasCmsGraphClient",
"namespace": "MySaasCmsApp.GraphQL",
"accessModifier": "public",
"url": "https://beta.cg.optimizely.com/content/v2?auth=YOUR_ACCESS_KEY&stored=true"
}
}
}Configure services with cached templates
Register the GraphQL client in dependency injection and enable cached templates so repeat requests reuse the parsed query. Choose the manual approach or the StrawberryShake client.
Manual GraphQL client (basic approach)
Configure a GraphQLHttpClient directly and add the cg-stored-query header to enable cached templates.
// Program.cs
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;
var builder = WebApplication.CreateBuilder(args);
// Configure GraphQL client for Mosey Bank template
builder.Services.AddSingleton<GraphQLHttpClient>(provider =>
{
var client = new GraphQLHttpClient(
"https://beta.cg.optimizely.com/content/v2?auth=YOUR_ACCESS_KEY&stored=true",
new NewtonsoftJsonSerializer()
);
// Enable cached templates for better performance
client.HttpClient.DefaultRequestHeaders.Add("cg-stored-query", "template");
return client;
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseRouting();
app.MapControllers();
app.Run();StrawberryShake client (recommended)
Register the generated SaasCmsGraphClient and apply the cached-templates header on the underlying HTTP client.
// Program.cs
using MySaasCmsApp.GraphQL;
var builder = WebApplication.CreateBuilder(args);
// Add StrawberryShake GraphQL client
builder.Services
.AddSaasCmsGraphClient()
.ConfigureHttpClient(client =>
{
client.BaseAddress = new Uri("https://beta.cg.optimizely.com/content/v2?auth=YOUR_ACCESS_KEY&stored=true");
// Enable cached templates for better performance
client.DefaultRequestHeaders.Add("cg-stored-query", "template");
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseRouting();
app.MapControllers();
app.Run();Configure settings
Move the Optimizely Graph endpoint into appsettings.json so it stays out of source code and is straightforward to swap per environment.
// appsettings.json
{
"OptimizelyGraph": {
"Endpoint": "https://beta.cg.optimizely.com/content/v2?auth=YOUR_ACCESS_KEY&stored=true"
}
}Create models for CMS (SaaS)
Define the C# types that map to the GraphQL response payload. Choose the manual approach or rely on the types generated by StrawberryShake.
Manual models (basic approach)
Manually define C# classes to represent CMS (SaaS) content and metadata for basic GraphQL queries and mutations.
// Models/SaasCmsModels.cs
using System.Text.Json.Serialization;
public class ContentMetadata
{
public string DisplayName { get; set; }
public string Key { get; set; }
public string Locale { get; set; }
public List<string> Types { get; set; } = new();
public UrlInfo Url { get; set; }
public DateTime Published { get; set; }
public DateTime LastModified { get; set; }
}
public class UrlInfo
{
public string Default { get; set; }
public string Hierarchical { get; set; }
public string Base { get; set; }
}
public class ContentItem
{
[JsonPropertyName("_metadata")]
public ContentMetadata Metadata { get; set; }
}
public class ContentResponse
{
public List<ContentItem> Items { get; set; } = new();
public int Total { get; set; }
}
public class GraphQLResponse<T>
{
[JsonPropertyName("_Content")]
public T Content { get; set; }
public T LandingPage { get; set; }
}Auto-generated types (recommended with StrawberryShake)
StrawberryShake generates types from your GraphQL queries during the build.
dotnet buildThe generated types live in the MySaasCmsApp.GraphQL namespace.
IGetAllContentResult– Result type for aGetAllContentquery.IGetLandingPagesResult– Result type for aGetLandingPagesquery (returns a single item).IGetContentByKeyResult– Result type for aGetContentByKeyquery.- Strongly-typed models for all GraphQL types used in queries.
Create content service
Centralize content access behind an injectable service so controllers and other consumers depend on an interface rather than a raw GraphQL client.
Manual GraphQL client
Wrap the manual GraphQLHttpClient in a service that issues queries and surfaces typed responses.
// Services/ContentService.cs
using GraphQL;
using GraphQL.Client.Http;
public interface IContentService
{
Task<ContentResponse> GetAllContentAsync(int limit = 10);
Task<ContentResponse> GetLandingPagesAsync();
}
public class ContentService : IContentService
{
private readonly GraphQLHttpClient _graphQLClient;
private readonly ILogger<ContentService> _logger;
public ContentService(GraphQLHttpClient graphQLClient, ILogger<ContentService> logger)
{
_graphQLClient = graphQLClient;
_logger = logger;
}
public async Task<ContentResponse> GetAllContentAsync(int limit = 10)
{
var query = @"
query GetAllContent($locale: [Locales] = en, $limit: Int!) {
_Content(locale:$locale, limit: $limit, orderBy: { _metadata: { lastModified: DESC } }) {
items {
_metadata {
displayName
key
locale
types
url {
default
hierarchical
}
published
lastModified
}
}
total
}
}";
var request = new GraphQLRequest
{
Query = query,
Variables = new { limit }
};
try
{
var response = await _graphQLClient.SendQueryAsync<GraphQLResponse<ContentResponse>>(request);
if (response.Errors?.Any() == true)
{
_logger.LogError("GraphQL errors: {Errors}",
string.Join(", ", response.Errors.Select(e => e.Message)));
return new ContentResponse();
}
return response.Data.Content ?? new ContentResponse();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching content");
return new ContentResponse();
}
}
public async Task<ContentResponse> GetLandingPagesAsync()
{
var query = @"
query GetLandingPages($locale:[Locales] = en) {
LandingPage(locale: $locale) {
item {
_metadata {
displayName
url {
default
}
published
}
}
total
}
}";
var request = new GraphQLRequest
{
Query = query,
Variables = new { }
};
try
{
var response = await _graphQLClient.SendQueryAsync<GraphQLResponse<ContentResponse>>(request);
if (response.Errors?.Any() == true)
{
_logger.LogError("GraphQL errors: {Errors}",
string.Join(", ", response.Errors.Select(e => e.Message)));
return new ContentResponse();
}
// Handle single item result - convert to array for consistent response format
var landingPageData = response.Data.LandingPage;
if (landingPageData?.Items?.Any() == true)
{
// Take only the first item since we're expecting a single landing page
return new ContentResponse
{
Items = new List<ContentItem> { landingPageData.Items.First() },
Total = landingPageData.Total
};
}
return new ContentResponse();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching landing pages");
return new ContentResponse();
}
}
}StrawberryShake service (strongly-typed)
Wrap the generated client in a service that exposes the operations as Task<IOperationResult<T>> methods for safer and more efficient GraphQL operations.
// Services/SaasCmsService.cs
using MySaasCmsApp.GraphQL;
using StrawberryShake;
public interface ISaasCmsService
{
Task<IOperationResult<IGetAllContentResult>> GetAllContentAsync(int limit = 10, int skip = 0);
Task<IOperationResult<IGetLandingPagesResult>> GetLandingPagesAsync();
Task<IOperationResult<IGetContentByKeyResult>> GetContentByKeyAsync(string key);
}
public class SaasCmsService : ISaasCmsService
{
private readonly ISaasCmsGraphClient _graphQLClient;
private readonly ILogger<SaasCmsService> _logger;
public SaasCmsService(ISaasCmsGraphClient graphQLClient, ILogger<SaasCmsService> logger)
{
_graphQLClient = graphQLClient;
_logger = logger;
}
public async Task<IOperationResult<IGetAllContentResult>> GetAllContentAsync(int limit = 10, int skip = 0)
{
try
{
var result = await _graphQLClient.GetAllContent.ExecuteAsync([Locales.En], limit, skip);
if (result.IsErrorResult())
{
_logger.LogError("GraphQL errors: {Errors}",
string.Join(", ", result.Errors?.Select(e => e.Message) ?? new string[0]));
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching content");
throw;
}
}
public async Task<IOperationResult<IGetLandingPagesResult>> GetLandingPagesAsync()
{
try
{
var result = await _graphQLClient.GetLandingPages.ExecuteAsync([Locales.En]);
if (result.IsErrorResult())
{
_logger.LogError("GraphQL errors: {Errors}",
string.Join(", ", result.Errors?.Select(e => e.Message) ?? new string[0]));
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching landing page");
throw;
}
}
public async Task<IOperationResult<IGetContentByKeyResult>> GetContentByKeyAsync(string key)
{
try
{
var result = await _graphQLClient.GetContentByKey.ExecuteAsync(key, [Locales.En]);
if (result.IsErrorResult())
{
_logger.LogError("GraphQL errors: {Errors}",
string.Join(", ", result.Errors?.Select(e => e.Message) ?? new string[0]));
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching content by key");
throw;
}
}
}Create controller
Expose the content service through an HTTP API by adding ASP.NET Core controllers. Use the controller that matches the client you registered.
Manual GraphQL client
The following controller surfaces the manual IContentService over /api/content.
// Controllers/ContentController.cs
using Microsoft.AspNetCore.Mvc;
using StrawberryShake;
[ApiController]
[Route("api/[controller]")]
public class ContentController : ControllerBase
{
private readonly IContentService _contentService;
public ContentController(IContentService contentService)
{
_contentService = contentService;
}
[HttpGet]
public async Task<ActionResult<ContentResponse>> GetAllContent([FromQuery] int limit = 10)
{
var content = await _contentService.GetAllContentAsync(limit);
return Ok(content);
}
[HttpGet("landing-pages")]
public async Task<ActionResult<ContentResponse>> GetLandingPages()
{
var pages = await _contentService.GetLandingPagesAsync();
return Ok(pages);
}
}StrawberryShake controller (strongly-typed)
The following controller surfaces the StrawberryShake-backed ISaasCmsService over /api/saascms.
// Controllers/SaasCmsController.cs
using Microsoft.AspNetCore.Mvc;
using MySaasCmsApp.GraphQL;
[ApiController]
[Route("api/[controller]")]
public class SaasCmsController : ControllerBase
{
private readonly ISaasCmsService _saasCmsService;
public SaasCmsController(ISaasCmsService saasCmsService)
{
_saasCmsService = saasCmsService;
}
[HttpGet("content")]
public async Task<ActionResult> GetAllContent([FromQuery] int limit = 10, [FromQuery] int skip = 0)
{
var result = await _saasCmsService.GetAllContentAsync(limit, skip);
if (result.IsErrorResult())
{
return BadRequest(result.Errors);
}
return Ok(new
{
items = result.Data?._Content?.Items?.Select(item => new
{
displayName = item?._metadata?.DisplayName,
key = item?._metadata?.Key,
locale = item?._metadata?.Locale,
types = item?._metadata?.Types,
url = new
{
@default = item?._metadata?.Url?.Default,
hierarchical = item?._metadata?.Url?.Hierarchical
},
published = item?._metadata?.Published,
lastModified = item?._metadata?.LastModified
}),
total = result.Data?._Content?.Total
});
}
[HttpGet("landing-pages")]
public async Task<ActionResult> GetLandingPages()
{
var result = await _saasCmsService.GetLandingPagesAsync();
if (result.IsErrorResult())
{
return BadRequest(result.Errors);
}
// Handle single item result
var landingPage = result.Data?.LandingPage?.Item;
if (landingPage == null)
{
return NotFound("No landing page found");
}
return Ok(new
{
item = new
{
displayName = landingPage._metadata?.DisplayName,
url = landingPage._metadata?.Url?.Default,
published = landingPage._metadata?.Published
},
total = result.Data?.LandingPage?.Total ?? 0
});
}
[HttpGet("content/{key}")]
public async Task<ActionResult> GetContentByKey(string key)
{
var result = await _saasCmsService.GetContentByKeyAsync(key);
if (result.IsErrorResult())
{
return BadRequest(result.Errors);
}
var content = result.Data?._Content?.Items?.FirstOrDefault();
if (content == null)
{
return NotFound();
}
return Ok(new
{
displayName = content._metadata?.DisplayName,
key = content._metadata?.Key,
locale = content._metadata?.Locale,
types = content._metadata?.Types,
url = new
{
@default = content._metadata?.Url?.Default,
hierarchical = content._metadata?.Url?.Hierarchical
},
published = content._metadata?.Published,
lastModified = content._metadata?.LastModified
});
}
}Register services
Register the content service in Program.cs so dependency injection resolves it for controllers. Pick the registration that matches your client choice.
Manual services
// Program.cs (add to existing code)
builder.Services.AddScoped<IContentService, ContentService>();StrawberryShake services
// Program.cs (add to existing code)
builder.Services.AddScoped<ISaasCmsService, SaasCmsService>();Test the API
Build and run the application, then call the endpoints to confirm that content flows from Optimizely Graph through the service into the API response.
dotnet build # This generates types if using StrawberryShake
dotnet runReplace PORT with the port number displayed in the console.
Manual GraphQL client endpoints
http://localhost:PORT/api/content– All contenthttps://localhost:PORT/api/content/landing-pages– Landing pages
StrawberryShake endpoints
http://localhost:PORT/api/saascms/content– All content with paginationhttp://localhost:PORT/api/saascms/landing-pages– Landing pageshttp://localhost:PORT/api/saascms/content/{key}– Content by key
Example generated types for CMS (SaaS)
The following snippet shows the shape of the interfaces StrawberryShake generates for the GetAllContent query so you know what to expect in IntelliSense.
// Auto-generated by StrawberryShake for CMS (SaaS)
namespace MySaasCmsApp.GraphQL
{
public partial interface IGetAllContentResult
{
public IGetAllContent__Content? _Content { get; }
}
public partial interface IGetAllContent__Content
{
public IReadOnlyList<IGetAllContent__Content_Items?>? Items { get; }
public int Total { get; }
}
public partial interface IGetAllContent__Content_Items
{
public IGetAllContent__Content_Items__Metadata? _Metadata { get; }
}
public partial interface IGetAllContent__Content_Items__Metadata
{
public string? DisplayName { get; }
public string? Key { get; }
public string? Locale { get; }
public IReadOnlyList<string?>? Types { get; }
public IGetAllContent__Content_Items__Metadata_Url? Url { get; }
public DateTimeOffset Published { get; }
public DateTimeOffset LastModified { get; }
}
}Updated 9 days ago
