Quick start guide: C#/.NET
Learn how to create a GraphQL client for Optimizely Content Management System Software as a Service (CMS SaaS) using .NET 8 and StrawberryShake.
Interact with Optimizely SaaS CMS efficiently from a .NET application, you can generate a strongly-typed GraphQL client using either a manual setup or the StrawberryShake code generator. This approach ensures type safety, simplifies API calls, and improves developer productivity when fetching and manipulating CMS content. With StrawberryShake, you get the following:
- Full IntelliSense support for all GraphQL operations.
- Compile-time type checking.
- Automatic serialization/deserialization.
- Generated documentation from the schema.
Prerequisite
Ensure the following prerequisites are in place:
- .NET 8 SDK or later
- Optimizely Content Management System Software as a Service (CMS SaaS) instance with published content
- Optimizely Graph endpoint URL and authentication token
- Basic knowledge of GraphQL queries
1. Create the project
dotnet new webapi -n MySaasCmsApp
cd MySaasCmsAppAdd Optimizely NuGet source if you don't already have it in your NuGet config
dotnet nuget add source https://api.nuget.optimizely.com/v3/index.json -n Optimizely2. Install dependencies
Install dependencies of the StrawberryShake client.
2.a. Manual GraphQL code generation
# 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.Server
2.b. Configure StrawberryShake
Configure the project to use StrawberryShake by updating the .csproj file to include all .graphql files.
<!-- 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>Following are the benefits of Using GraphQL Codegen (StrawberryShake):
- 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 easy refactoring.
- SaaS CMS Integration – Handles
_metadata, strong typing forURLs/locales/types, and version-safe development.
2.c. Create GraphQL schema and queries
# Download the schema from Mosey Bank SaaS CMS
dotnet graphql download https://beta.cg.optimizely.com/content/v2?auth=<<INSERT_YOUR_KEY_HERE>> -f ./schema.graphqlAdd file Queries/SaasCmsQueries.graphql
# 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
}
}
}
}2.d. Configure the GraphQL client
dotnet graphql init "https://beta.cg.optimizely.com/content/v2?auth=<<INSERT_YOUR_KEY_HERE>>&stored=true" -n SaasCmsGraphClient -p .Add namespace to GraphQL client, add accessModifier property and change documents property to Queries/*.graphql
# .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=<<INSERT_YOUR_KEY_HERE>>&stored=true"
}
}
}3. Configure services with Cached Templates
You have the option of creating services using either the manual approach or the StrawberryShake client.
3.a. Manual GraphQL client (basic approach)
Add the packages which are required.
dotnet add package GraphQL.Client
dotnet add package GraphQL.Client.Serializer.Newtonsoft// 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=<<INSERT_YOUR_KEY_HERE>>&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();3.b. StrawberryShake client (recommended)
// 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=<<INSERT_YOUR_KEY_HERE>>&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();4. Configure settings
// appsettings.json
{
"OptimizelyGraph": {
"Endpoint": "https://beta.cg.optimizely.com/content/v2?auth=<<INSERT_YOUR_KEY_HERE>>&stored=true"
}
}5. Create models for SaaS CMS
You have the option of creating models using either the manual approach or the StrawberryShake client.
5.a. Manual models (basic approach)
Use manually defined C# classes to represent SaaS CMS content and metadata for basic GraphQL handling.
// 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; }
}5.b. Auto-generated types (recommended with StrawberryShake)
When using StrawberryShake, types are automatically generated based on your GraphQL queries. Build the project to generate types:
dotnet buildGenerated types will be available in the MySaasCmsApp.GraphQL namespace:
IGetAllContentResult- Result type forGetAllContentqueryIGetLandingPagesResult- Result type forGetLandingPagesquery (returns single item)IGetContentByKeyResult- Result type forGetContentByKeyquery- Strongly-typed models for all GraphQL types used in queries
6. Create content service
Create a centralized service to fetch and manage SaaS CMS content using your GraphQL client.
6.a. Manual GraphQL client
Use a basic GraphQL client to manually query the SaaS CMS and handle 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();
}
}
}6.b. StrawberryShake service (strongly-typed)
Leverage StrawberryShake to generate a strongly-typed service 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;
}
}
}7. Create controller
7.a. Manual GraphQL client
// 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);
}
}7.b. StrawberryShake controller (strongly-typed)
// 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
});
}
}8. Register services
8.a. Manual services
// Program.cs (add to existing code)
builder.Services.AddScoped<IContentService, ContentService>();8.b. StrawberryShake Services
// Program.cs (add to existing code)
builder.Services.AddScoped<ISaasCmsService, SaasCmsService>();9. Test the api
dotnet build # This generates types if using StrawberryShake
dotnet run
Use the following port number displayed in the console:
Manual GraphQL client endpoints:
http://localhost:<<PORT>>/api/content - All content
https://localhost:<<PORT>>/api/content/landing-pages - Landing pages
StrawberryShake endpoints:
http://localhost:<<PORT>>/api/saascms/content - All content with pagination
http://localhost:<<PORT>>/api/saascms/landing-pages - Landing pages
http://localhost:<<PORT>>/api/saascms/content/{key} - Content by key
Example Generated Types for SaaS CMS
// Auto-generated by StrawberryShake for SaaS CMS
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 16 days ago
