Fetching Content

The Agility.NET.FetchAPI package provides multiple ways to retrieve content from Agility CMS. This guide covers the REST API, GraphQL, and typed responses.

Overview

Agility CMS content is accessed through the Fetch API, which provides:

  • Sitemap - Page structure and routing
  • Pages - Full page data with zones and modules
  • Content Items - Individual content pieces
  • Content Lists - Collections of content items
  • GraphQL - Flexible queries with field selection

Setup

Install the Package

dotnet add package Agility.NET.FetchAPI

Configure Services

// Program.cs or Startup.cs
builder.Services.AddSingleton<FetchApiService>(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    return new FetchApiService(
        instanceGuid: config["AppSettings:InstanceGUID"],
        fetchApiKey: config["AppSettings:FetchAPIKey"],
        previewApiKey: config["AppSettings:PreviewAPIKey"]
    );
});

Inject the Service

// In a controller, page model, or component
public class MyComponent
{
    private readonly FetchApiService _fetchApi;

    public MyComponent(FetchApiService fetchApi)
    {
        _fetchApi = fetchApi;
    }
}

Fetching the Sitemap

The sitemap contains all pages and their routing information.

Flat Sitemap

Returns a flat list of all pages:

var sitemap = await _fetchApi.GetSitemapFlat(
    channelName: "website",
    locale: "en-us"
);

// sitemap is List<SitemapPage>
foreach (var page in sitemap)
{
    Console.WriteLine($"{page.Path} -> Page ID: {page.PageID}");
}

Nested Sitemap

Returns a hierarchical structure:

var sitemap = await _fetchApi.GetSitemapNested(
    channelName: "website",
    locale: "en-us"
);

// sitemap includes parent/child relationships
void PrintPages(SitemapPage page, int depth = 0)
{
    Console.WriteLine($"{new string(' ', depth * 2)}{page.Path}");
    foreach (var child in page.Children ?? new List<SitemapPage>())
    {
        PrintPages(child, depth + 1);
    }
}

Typed Sitemap Methods

// Flat sitemap with automatic deserialization
var sitemap = await _fetchApi.GetTypedSitemapFlat(
    channelName: "website",
    locale: "en-us"
);

// Nested sitemap with automatic deserialization
var sitemap = await _fetchApi.GetTypedSitemapNested(
    channelName: "website",
    locale: "en-us"
);

Fetching Pages

Get Page by ID

var page = await _fetchApi.GetTypedPage(
    pageId: 123,
    locale: "en-us",
    contentLinkDepth: 2  // How deep to expand linked content
);

// Access page data
var title = page.Name;
var seo = page.Seo;
var zones = page.Zones;

Page Response Structure

public class PageResponse
{
    public int PageID { get; set; }
    public string Name { get; set; }
    public string Path { get; set; }
    public string TemplateName { get; set; }
    public SEO Seo { get; set; }
    public Dictionary<string, Zone> Zones { get; set; }
}

public class Zone
{
    public string Name { get; set; }
    public List<Module> Modules { get; set; }
}

public class Module
{
    public string ModuleName { get; set; }
    public int ContentID { get; set; }
    public dynamic Fields { get; set; }
}

The contentLinkDepth parameter controls how deeply linked content is expanded:

DepthBehavior
0No expansion - linked content returns only IDs
1Expand one level of linked content
2Expand two levels (recommended default)
3+Deeper expansion (use sparingly)
// Shallow - linked content not expanded
var page = await _fetchApi.GetTypedPage(123, "en-us", contentLinkDepth: 0);

// Deep - multiple levels expanded
var page = await _fetchApi.GetTypedPage(123, "en-us", contentLinkDepth: 3);

Fetching Content Items

Single Content Item

// Get a specific content item by ID
var item = await _fetchApi.GetTypedContentItem<BlogPost>(
    contentId: 456,
    locale: "en-us",
    contentLinkDepth: 1
);

// Access typed fields
Console.WriteLine(item.Fields.Title);
Console.WriteLine(item.Fields.Content);

Content Item Response

public class ContentItemResponse<T>
{
    public int ContentID { get; set; }
    public T Fields { get; set; }
    public ContentItemProperties Properties { get; set; }
}

public class ContentItemProperties
{
    public int ItemOrder { get; set; }
    public string ReferenceName { get; set; }
    public DateTime Modified { get; set; }
}

Fetching Content Lists

Basic List Fetch

var posts = await _fetchApi.GetTypedContentList<BlogPost>(
    referenceName: "posts",
    locale: "en-us",
    take: 10,
    skip: 0
);

// posts is List<ContentItemResponse<BlogPost>>
foreach (var post in posts)
{
    Console.WriteLine(post.Fields.Title);
}

With Filtering and Sorting

var posts = await _fetchApi.GetTypedContentList<BlogPost>(
    referenceName: "posts",
    locale: "en-us",
    take: 10,
    skip: 0,
    filter: "fields.category eq 'news'",
    sort: "fields.date",
    direction: "desc"
);

Filter Syntax

OperatorExampleDescription
eqfields.status eq 'published'Equals
nefields.status ne 'draft'Not equals
containsfields.title contains 'agility'Contains substring

Sort Options

// Sort by date descending
sort: "fields.date",
direction: "desc"

// Sort by title ascending
sort: "fields.title",
direction: "asc"

// Sort by item order
sort: "properties.itemOrder",
direction: "asc"

GraphQL Queries

GraphQL provides flexible queries with precise field selection. This is often more efficient than REST for complex data needs.

Basic GraphQL Query

var query = @"
{
    posts(take: 10, skip: 0, sort: ""fields.date"", direction: ""desc"") {
        contentID
        fields {
            title
            slug
            date
            excerpt
            image {
                url
                label
            }
            category {
                contentID
                fields {
                    name
                }
            }
        }
    }
}";

var posts = await _fetchApi.GetContentByGraphQL<Post>(
    query: query,
    objName: "posts",
    locale: "en-us"
);

GraphQL Response Types

Define types matching your GraphQL selection:

public class Post
{
    public int ContentID { get; set; }
    public PostFields Fields { get; set; }
}

public class PostFields
{
    public string Title { get; set; }
    public string Slug { get; set; }
    public DateTime Date { get; set; }
    public string Excerpt { get; set; }
    public ImageAttachment Image { get; set; }
    public CategoryReference Category { get; set; }
}

public class CategoryReference
{
    public int ContentID { get; set; }
    public CategoryFields Fields { get; set; }
}

public class CategoryFields
{
    public string Name { get; set; }
}

GraphQL with Variables

For dynamic queries, construct the query string:

public async Task<List<Post>> GetPostsByCategory(string categoryId, int take)
{
    var query = $@"
    {{
        posts(
            take: {take},
            filter: ""fields.category.contentID eq {categoryId}""
        ) {{
            contentID
            fields {{
                title
                slug
            }}
        }}
    }}";

    return await _fetchApi.GetContentByGraphQL<Post>(query, "posts", "en-us");
}

GraphQL Filtering

{
    # Filter by field value
    posts(filter: "fields.featured eq true") {
        contentID
        fields { title }
    }

    # Filter by date range
    events(filter: "fields.date ge '2024-01-01'") {
        contentID
        fields { title date }
    }

    # Multiple conditions
    posts(filter: "fields.category.contentID eq 5 AND fields.featured eq true") {
        contentID
        fields { title }
    }
}

Preview Mode

All fetch methods support preview mode to retrieve draft content:

// The FetchApiService automatically handles preview vs live
// based on which API key you use

// For explicit control in your service:
public async Task<List<Post>> GetPosts(bool isPreview)
{
    return await _fetchApi.GetTypedContentList<Post>(
        referenceName: "posts",
        locale: "en-us",
        take: 10,
        isPreview: isPreview  // Pass preview flag
    );
}

Detecting Preview Mode

// Blazor
@inject IHttpContextAccessor HttpContextAccessor

@code {
    private bool IsPreview => HttpContextAccessor.HttpContext?
        .Request.Query.ContainsKey("agilitypreviewkey") ?? false;
}

// MVC
public bool IsPreview => HttpContext.Request.Query
    .ContainsKey("agilitypreviewkey");

URL Redirects

Fetch URL redirects configured in Agility CMS:

var redirects = await _fetchApi.GetUrlRedirects(locale: "en-us");

// redirects is List<UrlRedirect>
foreach (var redirect in redirects)
{
    Console.WriteLine($"{redirect.OriginUrl} -> {redirect.DestinationUrl}");
}

Caching Best Practices

Implement Service-Level Caching

public class CachedAgilityService : IAgilityService
{
    private readonly FetchApiService _fetchApi;
    private readonly IMemoryCache _cache;
    private readonly int _cacheMinutes;

    public async Task<List<Post>> GetPostsAsync(string locale)
    {
        var cacheKey = $"posts_{locale}";

        return await _cache.GetOrCreateAsync(cacheKey, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow =
                TimeSpan.FromMinutes(_cacheMinutes);

            return await _fetchApi.GetTypedContentList<Post>(
                "posts", locale, take: 100);
        });
    }
}

Cache Invalidation via Webhooks

// Endpoint for Agility webhook
[HttpPost("/api/revalidate")]
public IActionResult Revalidate([FromBody] WebhookPayload payload)
{
    if (payload.State != "Published")
        return Ok();

    // Invalidate relevant cache entries
    switch (payload.Type)
    {
        case "content":
            _cache.Remove($"content_{payload.ReferenceName}_{payload.Locale}");
            break;
        case "page":
            _cache.Remove($"sitemap_{payload.Locale}");
            _cache.Remove($"page_{payload.PageID}_{payload.Locale}");
            break;
    }

    return Ok();
}

Error Handling

try
{
    var content = await _fetchApi.GetTypedContentItem<Post>(contentId, locale);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Content not found - handle gracefully
    _logger.LogWarning("Content {ContentId} not found", contentId);
    return null;
}
catch (HttpRequestException ex)
{
    // API error
    _logger.LogError(ex, "Error fetching content {ContentId}", contentId);
    throw;
}

Content Models

Define C# classes matching your Agility content models:

// Models/AgilityModels.cs

public class BlogPost
{
    public string? Title { get; set; }
    public string? Slug { get; set; }
    public DateTime? Date { get; set; }
    public string? Excerpt { get; set; }
    public string? Content { get; set; }
    public ImageAttachment? Image { get; set; }
    public ContentReference? Category { get; set; }
    public List<ContentReference>? Tags { get; set; }
}

public class ImageAttachment
{
    public string? Url { get; set; }
    public string? Label { get; set; }
    public int? Width { get; set; }
    public int? Height { get; set; }
}

public class ContentReference
{
    public int ContentID { get; set; }
    public dynamic? Fields { get; set; }
}

Next Steps