Block Models and JsonConverters without the Contentstack .NET SDK

This blog post explains how you can use block models with a generic base class to model Contentstack modular fields and to simplify JsonConverters serialization configuration without using the Contentstack SDK. This blog post depends on a refactored version of the base class for block models described in the following blog post. Basically, for each modular field, implement an enum to identify the types of blocks and a base class for block models, with inheriting classes to model each type of block.

Modeling Contentstack entries depends on entry models and block models. Entry model classes expose properties that correspond to the fields defined in content types.

Block models are like entry models, but instead of modeling fields in a content type, block models model groups of fields including each type of block that can appear in modular fields. To support flexible data structures, deserialization from JSON requires registering JsonConverters that create block models based on the data received. The base class described in the blog post helps to simplify all of this.

With this solution, each modular field requires two primary components and an additional component for each type of block. The first component is an enum that identifies the types of blocks, where the values in the enum map to block type identifiers that appear in the JSON. The second component is a base class for models of blocks that can appear in the field. Additional components are classes that inherit from this base class to represent the different types of blocks. Here is the current code.

namespace ContentstackRazorPages.Core.Models
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Linq;
    using System.Reflection;

    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;

    public class ModularBlockBase
    {
        private static List<Type> _implementations = null;
        private static object _sync = new object();

        public static Type[] Implementations
        {
            get
            {
                if (_implementations == null)
                {
                    lock (_sync)
                    {
                        _implementations = new List<Type>();

                        foreach (Assembly assembly 
                            in AppDomain.CurrentDomain.GetAssemblies())
                        {
                            try
                            {
                                foreach (Type type 
                                    in assembly.GetExportedTypes())
                                {
                                    if (type.BaseType != null
                                        && type.BaseType.BaseType != null
                                        && type.BaseType.BaseType 
                                            == typeof(ModularBlockBase))
                                    {
                                        _implementations.Add(type);
                                    }
                                }
                            }
                            catch (Exception)
                            {
                                //TODO: assembly is obfuscated, etc.
                            }
                        }
                    }
                }

                return _implementations.ToArray();
            }
        }
    };

    public abstract class 
        ModularBlockBase<TBlockBase, TBlockTypeEnum> : ModularBlockBase
        where TBlockBase : class
        where TBlockTypeEnum : struct, IConvertible
    {
        public class ModularBlocksJsonConverter : JsonConverter<TBlockBase>
        {

            public override TBlockBase ReadJson(
                JsonReader reader,
                Type objectType,
                TBlockBase existingValue,
                bool hasExistingValue,
                JsonSerializer serializer)
            {
                TBlockTypeEnum parsed;
                JObject jObject = JObject.Load(reader);

                if (Enum.TryParse(
                    jObject.Children().First().Children().First().Path,
                    true,
                    out parsed))
                {
                    foreach (Type t in Assembly.GetAssembly(
                        typeof(TBlockBase)).GetTypes().Where(
                            myType => myType.IsClass
                                && !myType.IsAbstract
                                && myType.IsSubclassOf(typeof(TBlockBase))))
                    {
                        TBlockBase obj = (TBlockBase)Activator.CreateInstance(t);

                        foreach (PropertyInfo propertyInfo 
                            in obj.GetType().GetRuntimeProperties())
                        {
                            if (propertyInfo.PropertyType == typeof(TBlockTypeEnum))
                            {
                                if (((int)propertyInfo.GetValue(obj))
                                    == parsed.ToInt32(CultureInfo.InvariantCulture))
                                {
                                    serializer.Populate(jObject.GetValue(
                                        parsed.ToString().ToLower()).CreateReader(), 
                                        obj);
                                    return obj;
                                }
                            }
                        }
                    }
                }

                Trace.Assert(
                    false,
                    "Unable to locate " + typeof(TBlockTypeEnum) + " property or matching "
                    + parsed + " in classes of " + Assembly.GetAssembly(typeof(TBlockBase))
                    + " that derive from " + typeof(TBlockBase));
                return null;
            }

            public override void WriteJson(
                JsonWriter writer,
                [AllowNull] TBlockBase value,
                JsonSerializer serializer)
            {
                throw new NotImplementedException();
            }
        }

        private static JsonConverter _converter = null;

        public JsonConverter GetJsonConverter()
        {
            if (_converter == null)
            {
                _converter = new ModularBlocksJsonConverter();
            }

            return _converter;
        }
    }
}

Luckily, that is incredibly reusable. For this new version that does not depend on the Contentstack .NET SDK, I updated the ConfigureServices() method in Startup.cs to register JsonConverters for all types that inherit directly from ModularBlockBase, which are the base classes for the types of blocks that can appear in each modular field.

JsonConvert.DefaultSettings = () =>
{
    JsonSerializerSettings settings = new JsonSerializerSettings();

    foreach (Type blockType in ModularBlockBase.Implementations)
    {
        MethodInfo methodInfo = blockType.GetRuntimeMethod("GetJsonConverter", new Type[0]);
        object obj = Activator.CreateInstance(blockType);
        JsonConverter jsonConverter = methodInfo.Invoke(obj, new object[0]) as JsonConverter;
        settings.Converters.Add(jsonConverter);
    }

    return settings;
};

As a contrived example, consider a modular field that allows the user to create any number of blocks of two types: date and text. The user can create any number of date and text blocks in any sequence, where each block has one value or the other. The user can select which ASP.NET Razor Pages partial view to apply to each block.

Modeling the blocks requires four components. The enum is relatively straightforward: one value for any undefined conditions and a value for each type of block.

public enum ExampleBlockTypeEnum
{
    Unknown,
    Date,
    Text
}

All blocks will allow the CMS user to select a view, so the PartialView property to model that value can appear in the ExampleBlockBase class from which both ImageBlock and TextBlock derive.

public class ExampleBlockBase : ModularBlockBase<ExampleBlockBase, ExampleBlockTypeEnum>
{
    public virtual ExampleBlockTypeEnum BlockType { get; set; }
    public virtual string PartialView { get; set; }
}

Each block model has a single property to store the value of the field.

using System;

public class DateBlock : ExampleBlockBase
{
    public override string PartialView { get; set; } = "__date";
    public new ExampleBlockTypeEnum BlockType { get; set; } = ExampleBlockTypeEnum.Date;
    public DateTimeOffset When { get; set; }
}
...
public class TextBlock : ExampleBlockBase
{
    public override string PartialView { get; set; } = "__text";
    public new ExampleBlockTypeEnum BlockType { get; set; } = ExampleBlockTypeEnum.Text;
    public string Text { get; set; }
}

To represent the modular field in entry models, expose a list property of the base class.

public List<ExampleBlockBase> ExampleBlocks { get; set; }

The JsonConverter infrastructure described here will populate the list with TextBlock and ImageBlock instances based on what appears in the JSON.

One thought on “Block Models and JsonConverters without the Contentstack .NET SDK

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 )

Facebook photo

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

Connecting to %s

%d bloggers like this: