# Fetching Content

> Source: https://agilitycms.com/docs/dotNet/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

```bash
dotnet add package Agility.NET.FetchAPI
```

### Configure Services

```csharp
// 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

```csharp
// 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:

```csharp
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:

```csharp
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

```csharp
// 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

```csharp
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

```csharp
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) |

```csharp
// 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

```csharp
// 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

```csharp
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

```csharp
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

```csharp
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

```csharp
// 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

```csharp
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:

```csharp
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:

```csharp
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

```graphql
{
    # 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:

```csharp
// 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

```csharp
// 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:

```csharp
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

```csharp
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

```csharp
// 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

```csharp
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:

```csharp
// 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](/docs/dotNet/configuration) - API keys and caching settings
- [How Components Work](/docs/dotNet/how-components-work) - Use fetched content in components
- [Blazor Starter](/docs/dotNet/blazor-starter) - Blazor-specific patterns
- [MVC Starter](/docs/dotNet/mvc-starter) - MVC-specific patterns
