When working with Contentful’s Management API, efficiently retrieving entries by content type is a fundamental skill that can make or break your content management workflows. Whether you’re building content migration tools, analytics dashboards, or automated content processing systems, having robust querying capabilities is essential.

In this guide, I will walk through building a comprehensive C# utility that demonstrates best practices for querying Contentful entries by content type, complete with pagination, filtering, and error handling.

The Challenge with Large Content Sets

Contentful’s API returns paginated results with a maximum of 1,000 items per request. For content types with thousands of entries, you need to implement proper pagination handling to retrieve all items efficiently. Additionally, you’ll often need to filter results based on specific field values or metadata.

Building a Robust Content Type Query Utility

Let’s examine a complete solution that addresses these challenges:

using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json.Linq;
namespace ContentfulUtilities
{
    public class GetItemsByContentType
    {
        private readonly HttpClient _httpClient;
        private readonly string _spaceId;
        private readonly string _environmentId;
        private readonly string _managementToken;
        public GetItemsByContentType(string spaceId, string environmentId, string managementToken)
        {
            _spaceId = spaceId;
            _environmentId = environmentId;
            _managementToken = managementToken;
            _httpClient = new HttpClient();
        }
        public async Task<List<JObject>> GetEntriesAsync(string contentType, Dictionary<string, string>? query = null)
        {
            try
            {
                Console.WriteLine($"🔍 Fetching entries for content type: {contentType}");
                var allItems = new List<JObject>();
                int skip = 0;
                const int limit = 500; // Optimal batch size for performance
                bool more = true;
                while (more)
                {
                    var url = $"https://api.contentful.com/spaces/{_spaceId}/environments/{_environmentId}/entries?content_type={contentType}&limit={limit}&skip={skip}";
                    // Add query parameters if provided
                    if (query != null)
                    {
                        var queryString = string.Join("&", query.Select(kvp =>
                            $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value)}"));
                        url += $"&{queryString}";
                    }
                    var request = new HttpRequestMessage(HttpMethod.Get, url);
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _managementToken);
                    var response = await _httpClient.SendAsync(request);
                    var content = await response.Content.ReadAsStringAsync();
                    if (!response.IsSuccessStatusCode)
                    {
                        Console.WriteLine($"❌ Failed to fetch entries: {response.StatusCode}\n{content}");
                        break;
                    }
                    var json = JObject.Parse(content);
                    var items = json["items"] as JArray ?? new JArray();
                    
                    if (items.Count == 0)
                    {
                        more = false;
                        break;
                    }
                    allItems.AddRange(items.Select(i => (JObject)i));
                    skip += limit;
                    var total = json["total"]?.ToObject<int>() ?? 0;
                    more = skip < total;
                    var currentCount = Math.Min(skip, total);
                    Console.WriteLine($"📥 Loaded {currentCount}/{total} entries (batch size: {items.Count})");
                }
                Console.WriteLine($"✅ Successfully fetched {allItems.Count} entries for content type: {contentType}");
                return allItems;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"❌ Error fetching entries for content type {contentType}: {ex.Message}");
                return new List<JObject>();
            }
        }
    }
}

Key Design Decisions Explained

Optimal Batch Size

I use a limit of 500 entries per request rather than the maximum 1,000. This strikes a balance between minimising API calls and avoiding timeouts with large response payloads.

Comprehensive Error Handling

The utility gracefully handles API errors and continues processing, ensuring partial results aren’t lost due to transient network issues.

Flexible Query Parameters

The optional query parameter allows for dynamic filtering without modifying the core pagination logic.

Advanced Filtering Methods

Building on the core functionality, let’s add specialised methods for common filtering scenarios:

public async Task<List<JObject>> GetEntriesWithFieldFilterAsync(string contentType, string fieldName, string fieldValue, string matchType = "match")
{
    var query = new Dictionary<string, string>
    {
        [$"fields.{fieldName}[{matchType}]"] = fieldValue
    };
    return await GetEntriesAsync(contentType, query);
}
public async Task<List<JObject>> GetEntriesBySourceAsync(string contentType, string sourceValue)
{
    return await GetEntriesWithFieldFilterAsync(contentType, "source", sourceValue, "match");
}
public async Task<List<JObject>> GetEntriesCreatedAfterAsync(string contentType, DateTime date)
{
    var query = new Dictionary<string, string>
    {
        ["sys.createdAt[gte]"] = date.ToString("yyyy-MM-ddTHH:mm:ssZ")
    };
    return await GetEntriesAsync(contentType, query);
}

These methods demonstrate how to construct field-level queries and system metadata filters. The matchType parameter supports Contentful’s various matching operators like match, in, exists, and ne.

Data Extraction Utilities

Often you need specific field values rather than complete entry objects. These utility methods extract commonly needed data:

public async Task<List<string>> GetFieldValuesAsync(string contentType, string fieldName, string locale = "en-US")
{
    var entries = await GetEntriesAsync(contentType);
    var values = new List<string>();
    foreach (var entry in entries)
    {
        var fieldValue = entry["fields"]?[fieldName]?[locale]?.ToString();
        if (!string.IsNullOrWhiteSpace(fieldValue))
        {
            values.Add(fieldValue);
        }
    }
    return values.Distinct().ToList();
}
public async Task<List<string>> GetEntryIdsAsync(string contentType)
{
    var entries = await GetEntriesAsync(contentType);
    return entries.Select(e => e["sys"]?["id"]?.ToString())
                .Where(id => !string.IsNullOrWhiteSpace(id))
                .ToList();
}

Content Analysis and Reporting

For content auditing and analysis, the utility includes a comprehensive summary method:

public async Task PrintContentTypeSummaryAsync(string contentType)
{
    Console.WriteLine($"\n📊 Content Type Summary: {contentType}");
    Console.WriteLine("=".PadRight(50, '='));
    var entries = await GetEntriesAsync(contentType);
    if (entries.Count == 0)
    {
        Console.WriteLine("❌ No entries found");
        return;
    }
    Console.WriteLine($"📈 Total Entries: {entries.Count}");
    // Get unique sources
    var sources = await GetEntrySourcesAsync(contentType);
    if (sources.Any())
    {
        Console.WriteLine($"🏷️  Unique Sources: {sources.Count}");
        foreach (var source in sources.Take(10))
        {
            var count = entries.Count(e => e["fields"]?["source"]?["en-US"]?.ToString() == source);
            Console.WriteLine($"   • {source}: {count} entries");
        }
    }
    // Check published status
    var publishedCount = entries.Count(e => e["sys"]?["publishedVersion"] != null);
    var draftCount = entries.Count - publishedCount;
    Console.WriteLine($"📋 Status: {publishedCount} published, {draftCount} draft");
}

Best Practices Demonstrated

1. Pagination Handling

Always implement pagination when working with large datasets. The utility automatically handles the skip and limit parameters to retrieve all available entries.

2. Rate Limiting Awareness

While not explicitly implemented in this example, consider adding delays between requests for very large datasets to respect Contentful’s rate limits.

3. Locale Handling

The utility defaults to “en-US” but accepts locale parameters, making it easy to work with internationalized content.

4. Null Safety

Extensive null checking prevents runtime exceptions when dealing with optional fields or missing data.

5. Memory Efficiency

By processing entries in batches, the utility can handle large datasets without excessive memory consumption.

Usage Examples

Here’s how to use the utility in practice:

var spaceId = "your-space-id";
var environmentId = "master";
var managementToken = "CFPAT-your-management-token";
var getItems = new GetItemsByContentType(spaceId, environmentId, managementToken);
// Get all entries for a content type
var allArticles = await getItems.GetEntriesAsync("article");
// Get entries with specific field values
var featuredArticles = await getItems.GetEntriesWithFieldFilterAsync("article", "featured", "true");
// Get entries created in the last 30 days
var recentArticles = await getItems.GetEntriesCreatedAfterAsync("article", DateTime.Now.AddDays(-30));
// Get just the entry IDs for bulk operations
var articleIds = await getItems.GetEntryIdsAsync("article");
// Generate a content summary report
await getItems.PrintContentTypeSummaryAsync("article");

Performance Considerations

When working with large content sets:

  • Batch Size: 500 entries per request provides optimal performance
  • Selective Fields: Use the select parameter to retrieve only needed fields
  • Caching: Consider caching results for frequently accessed, slowly changing content
  • Parallel Processing: For multiple content types, process them concurrently

Conclusion

Effective content type querying is fundamental to successful Contentful implementations. This utility demonstrates key patterns for robust, scalable content retrieval:

  • Proper pagination handling for large datasets
  • Flexible filtering capabilities
  • Comprehensive error handling
  • Memory-efficient processing
  • Rich reporting and analysis features

Whether you’re building content migration tools, analytics dashboards, or automated workflows, these patterns will help you build reliable, maintainable solutions that scale with your content needs.

Leave a Reply