Getting Started with Contentful and .NET
The Content Delivery API (CDA) is a read-only API for retrieving content from Contentful. All content, both JSON and binary, is fetched from the server closest to a user's location by using our global CDN.
To make development easier for our users, we publish SDKs for various languages which make the task easier. This article details how to use the .NET CDA SDK.
Pre-requisites
This tutorial assumes you understand the basic Contentful data model as described in the developer center.
Contentful.net is built on .NET Core and targets .NET Standard 2.0. The SDK is cross-platform and runs on Linux, macOS and Windows.
Installation
Add the package to your .NET solution using the NuGet package manager by running the following command in your NuGet package manager console.
Install-Package contentful.csharp
Or running the following with the .NET CLI.
dotnet add package contentful.csharp
Installation via the package manager dialog
Right click your solution in Visual Studio and select Manage NuGet Packages for Solution....
Search in the package manager for "Contentful", select the "Contentful.csharp" package, check the projects you want to add the package to, and click install. The package will now download and install to your selected projects.
Your first request
To communicate with the CDA you use the ContentfulClient
class that requires four parameters:
- An
HttpClient
that makes the HTTP requests to the CDA. - An access token. You can create access tokens in the APIs tab of each space in the Contentful web app.
- An access token. This is the preview token used when communicating with the Contentful Preview API.
- A space id. This is the unique identifier of your space that you can find in the Contentful web app.
var httpClient = new HttpClient();
var client = new ContentfulClient(httpClient, "<content_delivery_api_key>", "<content_preview_api_key>", "<space_id>")
HttpClient
in .NET is special. It implements IDisposable
but is generally not supposed to be disposed for the lifetime of your application. This is because whenever you make a request with the HttpClient
and immediately dispose it you leave the connection open in a TIME_WAIT
state. It will remain in this state for 240 seconds by default. This means that if you make a lot of requests in a short period of time you might end up exhausting the connection pool, which would result in a SocketException
. To avoid this you should share a single instance of HttpClient
for the entire application, and exposing the underlying HttpClient
of the ContentfulClient
allows you to do this.
Once you have an ContentfulClient
you can start querying content. For example, to get a single entry:
var entry = await client.GetEntry<Product>("<entry_id>");
Console.WriteLine(entry.ProductName);
The GetEntry
method is generic and you can pass it any POCO class. It will then deserialize the JSON response from the API into that class. The example above passed it a Product
class as a generic type parameter.
public class Product {
public string ProductName { get; set; }
public string Price { get; set; }
public string Description { get; set; }
}
If you pass this class to the GetEntry
method you will get a response as a strongly typed Product
.
var product = await client.GetEntry<Product>("<entry_id");
Console.WriteLine(book.ProductName); // => How to manage content in a developer-friendly manner
Console.WriteLine(book.Price); // => Contentful
Console.WriteLine(book.Description); // => Make an API request, get JSON in return.
If you need the system meta data, add a SystemProperties
property to your class.
public class Product {
public SystemProperties Sys { get; set; }
public string ProductName { get; set; }
public string Price { get; set; }
public string Description { get; set; }
}
var productEntry = await client.GetEntry<Product>("<entry_id>");
Console.WriteLine(entry.ProductName); // => Contentful
Console.WriteLine(entry.Sys.Id); // => <entry_id>
Querying for content
There are a couple of methods on ContentfulClient
that allow you to query for content. Every example below assumes you have an ContentfulClient
with the name of client
initialized.
Get a single Entry
To get a single entry use the GetEntry<T>
method.
var entry = await client.GetEntry<Product>("<entry_id>");
This calls the CDA and returns JSON in the following format:
{
"sys": {
"space": {
"sys": {
"type": "Link",
"linkType": "Space",
"id": "<space_id>"
}
},
"id": "<entry_id>",
"type": "Entry",
"createdAt": "2016-11-03T10:50:05.033Z",
"updatedAt": "2016-11-08T14:30:23.857Z",
"revision": 4,
"contentType": {
"sys": {
"type": "Link",
"linkType": "<content_type>",
"id": "<content_type_id>"
}
},
"locale": "en-US"
},
"fields": {
"productName": "SoSo Wall Clock",
"slug": "soso-wall-clock",
"productDescription": "The newly released SoSo Clock from Lemnos marries simple...",
"sizetypecolor": "10\" x 2.2\"",
"image": [
{
"sys": {
"type": "Link",
"linkType": "Asset",
"id": "<asset_id>"
}
}
],
"tags": [
"home décor",
"clocks",
"interior design",
"yellow",
"gifts"
],
"categories": [
{
"sys": {
"type": "Link",
"linkType": "Entry",
"id": "<entry_id>"
}
}
],
"price": 120,
"brand": {
"sys": {
"type": "Link",
"linkType": "Entry",
"id": "<entry_id>"
}
},
"quantity": 3,
"sku": "B00MG4ULK2",
"website": "http://store.dwell.com/soso-wall-clock.html"
}
}
Which you can then map to your POCO object.
public class Product {
public SystemProperies Sys { get; set; }
public int Quantity { get; set; }
public string ProductName { get; set; }
public string Slug { get; set; }
}
var entry = await client.GetEntry<Product>("<entry_id>");
Console.WriteLine(entry.ProductName) // => SoSo Wall Clock
Console.WriteLine(entry.Quantity.ToString()) // => 3
Console.WriteLine(entry.Slug) // => soso-wall-clock
Console.WriteLine(entry.Sys.Id) // => <entry_id>
Console.WriteLine(entry.Sys.Revision?.ToString()) // => 4
Get multiple entries
There are several methods to retrieve multiple entries available in the SDK.
Get all entries of a space
var entries = await client.GetEntries<dynamic>();
// entries would be an IEnumerable ContentfulCollection of dynamic
Will return all entries in a space in an IEnumerable<dynamic>
. As with GetEntry<T>
you can choose to provide GetEntries<T>
with another implementation of T
, for example Product
.
var entries = await client.GetEntries<Product>();
// entries would be an IEnumerable ContentfulCollection of Product
Every collection returned by the CDA has this JSON structure:
{
"sys": {
"type": "Array"
},
"total": 2,
"skip": 0,
"limit": 100,
"items": [
{
//...items
}
]
}
This is useful if the response returns a large number of entries and you need to paginate the result. The maximum number of entries ever returned for a single result set is 1000 items, the default 100 items.
The ContentfulCollection<T>
response above corresponds to this structure. You can use the Skip
, Total
and Limit
properties directly on the returned collection.
ContentfulCollection
implements IEnumerable
and thus you can write normal LINQ syntax directly against the collection instead of against collection.Items
, e.g. entries.First()
as opposed to entries.Items.First()
which also works.
var entries = await client.GetEntries<Product>();
Console.WriteLine(entries.Total.ToString()); // => 2
Console.WriteLine(entries.Skip.ToString()); // => 0
Console.WriteLine(entries.Limit.ToString()); // => 100
Console.WriteLine(entries.First().ProductName) // => SoSo Wall Clock
Get and filter entries
Frequently you're not interested in every entry in a space but would like to filter the entries returned. The CDA exposes powerful filtering options that you can read more about in our api documentation.
When using the GetEntries
methods you can filter the query by using the QueryBuilder<T>
class.
var builder = QueryBuilder<Product>.New.ContentTypeIs("<content_type_id>").FieldEquals("fields.slug","soso-wall-clock");
var entries = await client.GetEntries(builder);
// entries would be an IEnumerable of Product
This would filter the entries returned to be of content type <content_type_id>
and the fields.slug
property equal to 'soso-wall-clock'.
Passing a string like fields.slug
can be hard to read, provides no IntelliSense and could easily be misspelled or faulty. The QueryBuilder<T>
class therefore provides a way to pass the fields as a strongly typed parameter.
var builder = QueryBuilder<Product>.New.ContentTypeIs("<content_type_id>").FieldEquals(f => f.Slug, "soso-wall-clock");
var entries = await client.GetEntries(builder);
// entries would be an IEnumerable of Product
// note that supplying a QueryBuilder infers the generic argument for the GentEntries method and we no longer need to supply the generic type
As filtering by content type id is a common scenario, the ContentfulClient
exposes a helpful method.
var entries = await client.GetEntriesByType<Product>("<content_type_id>");
// entries would be an IEnumerable of Product
This method can take an optional QueryBuilder<T>
for further filtering.
var builder = new QueryBuilder<Product>().FieldGreaterThan(f => f.Sys.UpdatedAt, DateTime.Now.AddDays(-7).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssK"));
var entries = await client.GetEntriesByType("<content_type_id>");
//entries would be an IEnumerable of Product
This would filter the entries returned to be of content type <content_type_id>
and that have been updated in the last week.
You can pass the query string directly to the GetEntries
methods.
var entries = await client.GetEntries<Product>("?content_Type=<content_type_id>&fields.productName[match]=Clock");
//entries would be an IEnumerable of Product
While this is possible, the recommended approach is to use the QueryBuilder<T>
as it will make sure your query string is correctly formatted.
Get entries of multiple types or by interface
Sometimes you want to fetch entries of several different content types. This can make it difficult to know what generic argument to use for your GetEntries<T>
call. You could always use dynamic
but a cleaner approach would be using an interface or base class.
Consider if we had the following types.
public class Person : IEntity {
public string Name { get; set; }
}
public class Product : IEntity {
public int Price { get; set; }
}
public interface IEntity {}
If we would like to fetch all the persons and products from Contentful it would be nice to be able to do this:
var entities = await client.GetEntries<IEntity>();
Unfortunately this does not work as we can't deserialize into an abstract type or an interface. We need concrete implementations and the deserializer has no way to know what to deserialize each entry into.
There is a way to specify how we want each content type deserialized. By implementing an IContentTypeResolver
we can give the client information how to deserialize into a specific type.
public class EntityResolver : IContentTypeResolver
{
public Dictionary<string, Type> _types = new Dictionary<string, Type>()
{
{ "person", typeof(Person) },
{ "product", typeof(Product) },
};
public Type Resolve(string contentTypeId)
{
return _types.TryGetValue(contentTypeId, out var type) ? type : null;
}
}
And then setting this resolver on the client before we call GetEntriesAsync
.
client.ContentTypeResolver = new EntityResolver();
var entities = await client.GetEntries<IEntity>();
var persons = entities.Where(p => p is Person);
var products = entities.Where(p => p is Product);
This now works and each entity will be of the expected type.
Setting an IContentTypeResolver
works for sub-properties as well. Consider the following class.
public class Person {
public string Name { get; set; }
public IProduct Product { get; set;}
}
The Product
property is of the interface type IProduct
and we therefore must use an IContentTypeResolver
to instruct the serialization engine how to resolve this interface into concrete implementations.
Get a single asset
To get a single asset use the GetAsset
method.
var asset = await client.GetAsset("<asset_id>");
This would return an asset as JSON:
{
"sys": {
"space": {
"sys": {
"type": "Link",
"linkType": "Space",
"id": "<space_id>"
}
},
"id": "<asset_id>",
"type": "Asset",
"createdAt": "2016-11-03T15:06:48.621Z",
"updatedAt": "2016-11-03T15:06:48.621Z",
"revision": 1,
"locale": "en-US"
},
"fields": {
"title": "ihavenoidea",
"file": {
"url": "//images.ctfassets.net/SpaceId/321/123/ihavenoidea.jpg",
"details": {
"size": 46022,
"image": {
"width": 600,
"height": 600
}
},
"fileName": "ihavenoidea.jpg",
"contentType": "image/jpeg"
}
}
}
That is then serialized into a strongly typed Asset
.
var asset = await client.GetAsset("<asset_id>");
Console.WriteLine(asset.SystemProperties.CreatedAt.ToShortDateString()); // => 2016-11-03
Console.WriteLine(asset.Title); // => ihavenoidea
Console.WriteLine(asset.File.Url); // => // images.ctfassets.net/SpaceId/321/123/ihavenoidea.jpg
Get multiple assets
Getting multiple assets is similar to getting multiple entries, but the methods are not generic but all return a variation of an IEnumerable<Asset>
.
Get all assets of a space
var assets = await client.GetAssets();
// assets would be an IEnumerable of Asset
Every collection returned by the Contentful API has this JSON structure:
{
"sys": {
"type": "Array"
},
"total": 7,
"skip": 0,
"limit": 100,
"items": [
{
// ...items
}
]
}
This is useful if the response returns a large number of entries and you need to paginate the result. The maximum number of entries ever returned for a single result set is 1000 items, the default 100 items.
The IEnumerable<Asset>
response above corresponds to this structure. It returns a ContentfulCollection<Asset>
which includes Skip
, Total
and Limit
properties.
ContentfulCollection
implements IEnumerable
and thus you can write normal LINQ syntax directly against the collection instead of against collection.Items
, e.g. assets.First()
as opposed to assets.Items.First()
which also works.
var assets = await client.GetAssets();
Console.WriteLine(assets.Total.ToString()); // => 7
Console.WriteLine(assets.Skip.ToString()); // => 0
Console.WriteLine(assets.Limit.ToString()); // => 100
Console.WriteLine(assets.First().Title) // => ihavenoidea
Get and filter assets
As with entries, you can filter assets by using your own query string or a QueryBuilder<T>
.
var builder = new QueryBuilder<Asset>().MimeTypeIs(MimeTypeRestriction.Image).OrderBy(a => a.SystemProperties.CreatedAt);
var assets = await client.GetAssets(builder);
// assets would be an IEnumerable of Asset
This returns all assets that are images, ordered by their creation date and would be equivalent to using the following query string.
var assets = await client.GetAssetsAsync("?mimetype_group=imageℴ=sys.createdAt");
// assets would be an IEnumerable of Asset
Including referenced assets and entries
When querying for entries it's common that the returned entries reference other assets and entries. For example, a blog post entry referencing its author entry and vice versa.
The CDA allows you to do this by specifying how many levels of references you wish to resolve and include in the API response.
Consider the following classes has two different properties that contain references to other assets and entries.
public class Product
{
public SystemProperties Sys { get; set; }
public string productName { get; set; }
public string Slug { get; set; }
public string productDescription { get; set; }
public List<Categories> Category { get; set; }
}
public class Category {
public SystemProperties Sys { get; set; }
public string Title { get; set; }
}
Specifying the number of levels to include
To specify the number of levels to include in a call add an include
query string parameter, manually or by using the QueryBuilder<T>
.
var builder = new QueryBuilder<Product>().ContentTypeIs("<content_type_id>").Include(3);
var entries = await client.GetEntries(builder);
This queries for entries of a specific content type id and tells the CDA to resolve up to three levels of referenced entries and assets. The default setting for the include parameter is 1. This means that omitting the query string parameter still resolves up to 1 level of referenced content. If you specifically do not want any referenced content included you need to set the include parameter to 0.
var builder = new QueryBuilder<Product>().ContentTypeIs("<content_type_id>").Include(0);
var entries = await client.GetEntries(builder);
// No referenced content would be included.
Including referenced content is only supported for the methods that return collections. Using GetEntry
will not resolve your references. Instead, you could query for a single entry using GetEntries
, but add a restriction to get an entry by a specific id.
var builder = new QueryBuilder<Product>().FieldEquals(f => f.Sys.Id, "<entry_id>").Include(2);
var entry = (await client.GetEntries(builder)).FirstOrDefault();
This fetches an entry with id "123" and includes up to two levels of referenced entries and assets.
Resolving included assets
To resolve assets when querying for content, add a property of type Asset
or IEnumerable<Asset>
and the deserialization will automatically fill up any referenced assets.
var builder = new QueryBuilder<Product>().ContentTypeIs("<content_type_id>").Include(1);
var entries = await client.GetEntries(builder);
Console.WriteLine(entries.First().FeaturedImage.Title); // => Alice in Wonderland
Console.WriteLine(entries.First().Images.Count.ToString()); // => 2
Resolving included entries
Entries are simple to resolve.
public List<Catgory> Categories { get; set; }
You can now deserialize the referenced categories and included them in the Product
.
var builder = new QueryBuilder<Product>().ContentTypeIs("<content_type_id>").Include(1);
var entries = await client.GetEntries(builder);
Console.WriteLine(entries.First().Categories[0].Fields.Title); // => Lewis Carroll
Console.WriteLine(entries.First().Categories[0].Sys.Id); // => 1234
Space and content types
You can retrieve information about your space and content types, useful for when you are working with your content model.
Get the space
var space = await _client.GetSpace();
Console.WriteLine(space.Name); // => SpaceName
Console.WriteLine(space.SystemProperties.Type); // => Space
Console.WriteLine(space.Locales.Count.ToString()); // => 2
Console.WriteLine(space.Locales[0].Code); // => en-US
Console.WriteLine(space.Locales[1].Name); // => de-DE
Get a content type
var contentType = await _client.GetContentType("<content_type_id>");
Console.WriteLine(contentType.Name); // => Product
Console.WriteLine(contentType.DisplayField); // => productName
Console.WriteLine(contentType.Fields.Count.ToString()); // => 12
Console.WriteLine(contentType.Fields[0].Localized.ToString()); // => true
Console.WriteLine(contentType.Fields[0].Required.ToString()); // => true
Console.WriteLine(contentType.Fields[2].Name); // => Description
Get all content types
var contentTypes = await _client.GetContentTypes();
Console.WriteLine(contentTypes.Count().ToString()); // => 3
Console.WriteLine(contentTypes.First().Name); // => Brand
Synchronization
They synchronization endpoints allow you to sync all your content to local storage. You can choose to sync everything, just assets, certain content types or deleted entries.
The initial sync
To start syncing content, call the SyncInitial
method. Calling it without any parameters will sync all content types and assets, but you can specify that you wish to sync specific content types, assets or deletions.
var result = await client.SyncInitial();
// Sync everything
var result = await client.SyncInitial(SyncType.Asset);
// Sync only assets
var result = await client.SyncInitial(SyncType.Entry)
// Sync only entries
var result = await client.SyncInitial(SyncType.Entry, "123")
// Sync only entries of the "123" content type
var result = await client.SyncInitial(SyncType.Deletions)
// Sync only deletions of assets and entries
var result = await client.SyncInitial(SyncType.DeletedAsset)
// Sync only deleted assets
var result = await client.SyncInitial(SyncType.DeletedEntry)
// Sync only deleted entries
Handling the result of a synchronization
All sync operations return a SyncResult
. This class contains all assets, entries, and deletions that the synchronization call resulted in and contains two URL properties, NextSyncUrl
and NextPageUrl
. NextPageUrl
is only present if there is more content for the current sync operation to receive, NextSyncUrl
will be null in this case. If there is no more content for the current sync, only NextSyncUrl
will be present.
var res = await client.SyncInitial();
// Sync everything
var syncedEntries = res.Entries;
// An IEnumerable of Entry<dynamic>
var syncedAssets = res.Assets;
// An IEnumerable of SyncedAsset
var deletedEntries = res.DeletedEntries;
// An IEnumerable of SystemProperty
var deletedEntries = res.DeletedAssets;
// An IEnumerable of SystemProperty
The synced entries are of the Entry<dynamic>
type. This is because the JSON returned by a sync must contain every locale of a space. The JSON structure of an entry looks something like this.
"fields": {
"productName": {
"en-US": "SoSo Wall Clock",
"sv": "SåSå Väggklocka"
},
"slug": {
"en-US": "soso-wall-clock"
},
"productDescription": {
"en-US": "The newly released SoSo Clock from Lemnos..."
},
"sizetypecolor": {
"en-US": "10\" x 2.2\""
}
}
This poses a problem when deserializing as you cannot have C# members that include hyphens (-) in their name, or classes that have a structure like this, i.e. a property ProductName
which is an object with two more properties, one for each language. To work around this for sync operations the Entry<dynamic>
is used. This means that you can access any property in any language using an indexer value.
var res = await client.SyncInitial();
Console.WriteLine(res.Entries.First().Fields.productName["en-US"].ToString()); // => SoSo Wall Clock
Console.WriteLine(res.Entries.First().Fields.productName.sv.ToString()); // => SåSå Väggklocka
This is also why you don't use the Asset
class directly for synced assets, but instead the SyncedAsset
class.
var res = await client.SyncInitial();
Console.WriteLine(res.Assets.First().Fields.title["en-US"].ToString()); // => SoSo Wall Clock
Console.WriteLine(res.Assets.First().Fields.title.sv.ToString()); // => SåSå Väggklocka
Syncing the next page or result
After a sync completes and you receive a SyncResult
you store the NextSyncUrl
value and call it when it's time to fetch changes by passing the full URL or the sync token to the SyncNextResult
method.
var res = await client.SyncInitial();
if(!string.IsNullOrEmpty(res.NextSyncUrl)){
//store the sync url somewhere
db.Store(res.NextSyncUrl);
}
// Later it's time to fetch the syncUrl from storage and fetch the next SyncResult
var syncUrl = db.RetrieveSyncUrl();
var res = await client.SyncNextResult(syncUrl);
// Process the result and again store the NextSyncUrl and sync again at the next interval
if(!string.IsNullOrEmpty(res.NextSyncUrl)){
//store the sync url somewhere
db.Store(res.NextSyncUrl);
}
Contentful makes no assumption on the interval of your sync operations, which is up to the individual application to decide. The sync result will contain all changes in your space since the last sync operation.
Recursively syncing all pages
If you want to ensure the initial sync retrieves all pages, you can use the SyncInitialRecursive
method.
var res = await client.SyncInitialRecursive();
// Sync everything and recursively process any NextPageUrls
var syncedEntries = res.Entries;
// An IEnumerable of Entry<dynamic> containing all entries for all pages aggregated into a single collection.
var syncedAssets = res.Assets;
// An IEnumerable of SyncedAsset containing all assets for all pages aggregated into a single collection.
var deletedEntries = res.DeletedEntries;
// An IEnumerable of SystemProperty containing all deleted entries for all pages aggregated into a single collection.
var deletedEntries = res.DeletedAssets;
// An IEnumerable of SystemProperty containing all deleted assets for all pages aggregated into a single collection.
This potentially results in multiple calls to the sync endpoint. If you have a large number of pages the initial collection of entries can be large and it might be better to manually process each page using the SyncInitial
and SyncNextResult
methods.
Next steps
Not what you’re looking for? Try our FAQ.