Tutorials
The Agility.NET.FetchAPI package provides multiple ways to retrieve content from Agility CMS. This guide covers the REST API, GraphQL, and typed responses.
The Agility.NET.FetchAPI package provides multiple ways to retrieve content from Agility CMS. This guide covers the REST API, GraphQL, and typed responses.
Agility CMS content is accessed through the Fetch API, which provides:
dotnet add package Agility.NET.FetchAPI
// 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"]
);
});
// In a controller, page model, or component
public class MyComponent
{
private readonly FetchApiService _fetchApi;
public MyComponent(FetchApiService fetchApi)
{
_fetchApi = fetchApi;
}
}
The sitemap contains all pages and their routing information.
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}");
}
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);
}
}
// 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"
);
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;
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:
| 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);
// 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);
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; }
}
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);
}
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"
);
| Operator | Example | Description |
|---|---|---|
eq | fields.status eq 'published' | Equals |
ne | fields.status ne 'draft' | Not equals |
contains | fields.title contains 'agility' | Contains substring |
// 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 provides flexible queries with precise field selection. This is often more efficient than REST for complex data needs.
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"
);
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; }
}
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");
}
{
# 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 }
}
}
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
);
}
// Blazor
@inject IHttpContextAccessor HttpContextAccessor
@code {
private bool IsPreview => HttpContextAccessor.HttpContext?
.Request.Query.ContainsKey("agilitypreviewkey") ?? false;
}
// MVC
public bool IsPreview => HttpContext.Request.Query
.ContainsKey("agilitypreviewkey");
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}");
}
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);
});
}
}
// 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();
}
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;
}
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; }
}