Use .NET Attributes to Specify Entry Model Classes for CMS Content Types

This blog post explains how you can use .NET attributes to indicate entry model class(es) to represent entries based on different content types in the Contentstack SaaS headless CMS, although this technique should work with any headless CMS.

When you need to retrieve an instance of an entry model (an object that represents an entry from the CMS), something needs to determine which class to use to represent that entry. There are many possible strategies for this logic, such as conventions based on matching content types to class names. I mark entry model classes with an attribute that identifies the content type(s) for which it is relevant. The logic for retrieving entry models consults these attributes to determine which class to instantiate.

Because other attributes could need to scan assemblies to determine types to which they apply, I put that logic in a base class for attributes.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;

public abstract class AttributeBase : Attribute
{
    // map the type of the attribute to a list of types that have that attribute.
    // static and cached to optimize performance.
    private static ConcurrentDictionary<Type, List<Type>> _types =
        new ConcurrentDictionary<Type, List<Type>>();

    public static Type[] GetTypesWithAttribute(Type attribute)
    {
        // Have assemblies already been scanned for this attribute.
        if (!_types.ContainsKey(attribute))
        {
            // no, create a new entry in the dictionary mapping
            // the attribute to the list of types that have that attribute.
            List<Type> result = new List<Type>();

            // for each assembly available
            foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                try
                {
                    // for each type in the assembly
                    foreach (Type type in assembly.GetTypes())
                    {
                        // if the type has the attribute
                        if (type.GetCustomAttributes(attribute, true).Length > 0)
                        {
                            // then add it to the list of types for the attribute
                            result.Add(type);
                        }
                    }
                }
                catch (Exception ex)
                {
                    // ignore; unable to load; assembly may be obfuscated, etc. 
                }
            }

            // store a record mapping the attribute to the list of types with that attribute
            _types[attribute] = result;
        }

        // return the list of types with the attribute
        return _types[attribute].ToArray();
    }
}

A derived attribute class associates entry models with content types.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class ContentTypeIdentifier : AttributeBase
{

    public ContentTypeIdentifier(string value)
    {
        Value = value;
    }
    public string Value { get; }

    private static ConcurrentDictionary<string, Type[]> _contentTypeTypes =
        new ConcurrentDictionary<string, Type[]>();

    public static Type[] GetModelTypeForContentType(string contentType)
    {
        if (!_contentTypeTypes.ContainsKey(contentType))
        {
            List<Type> result = new List<Type>();

            foreach (Type t in ContentTypeIdentifier.GetTypesWithAttribute(
                typeof(ContentTypeIdentifier)))
            {
                foreach (var attr in t.GetCustomAttributes(
                    typeof(ContentTypeIdentifier), true))
                {
                    if (attr is ContentTypeIdentifier ct
                        && ct.Value.Equals(contentType, 
                            StringComparison.InvariantCultureIgnoreCase))
                    {
                        result.Add(t);
                    }
                }
            }

            _contentTypeTypes[contentType] = result.ToArray();
        }

        return _contentTypeTypes[contentType];
    }
}

Now I can attribute the entry models to associate them with content types.

[ContentTypeIdentifier("modularblocksexample")]
public class ExampleBlocksEntryModel : EntryModelBase

I need to add a signature to my typed client that retrieves entries from the CMS as well as its underlying IContentDeliveryClient interface.

public async Task<EntryModelBase> AsDefaultEntryModelAsync(EntryIdentifier entryId)
{
    Type type = typeof(EntryModelBase);
    Type[] types = ContentTypeIdentifier.GetModelTypeForContentType(entryId.ContentType);

    if (types.Length > 0)
    {
        type = types[0];
    }

    JObject json = await AsObjectAsync(entryId);
    EntryModelBase entryModel = (EntryModelBase) json.SelectToken("$.entry").ToObject(type);
    entryModel.Json = json;
    return entryModel;
}

One thought on “Use .NET Attributes to Specify Entry Model Classes for CMS Content Types

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: