Disclaimer: This website requires Please enable JavaScript in your browser settings for the best experience.

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

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 MySaasCmsApp

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

2. 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 for URLs/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.graphql

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

Generated types will be available in the MySaasCmsApp.GraphQL namespace:

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