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
selectparameter 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