Dev guideRecipesAPI ReferenceChangelog
Dev guideRecipesUser GuidesNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

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 MySaasCmsApp

Add 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 Optimizely

Install 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.ConfigurationExtensions

Add 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.Server

Configure 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.graphql

Create 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 build

The generated types live in the MySaasCmsApp.GraphQL namespace.

  • IGetAllContentResult – Result type for a GetAllContent query.
  • IGetLandingPagesResult – Result type for a GetLandingPages query (returns a single item).
  • IGetContentByKeyResult – Result type for a GetContentByKey query.
  • 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 run

Replace PORT with the 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 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; }
    }
}