See Agility CMS in action. Watch a product demo
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; }
}
Content Link Depth
The contentLinkDepth parameter controls how deeply linked content is expanded:
| Depth | Behavior |
|---|---|
| 0 | No expansion - linked content returns only IDs |
| 1 | Expand one level of linked content |
| 2 | Expand 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
| Operator | Example | Description |
|---|---|---|
eq | fields.status eq 'published' | Equals |
ne | fields.status ne 'draft' | Not equals |
contains | fields.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
- Configuration - API keys and caching settings
- How Components Work - Use fetched content in components
- Blazor Starter - Blazor-specific patterns
- MVC Starter - MVC-specific patterns