In my last post I wrote about a small tweak to the built-in PropertyList type. This time I am sharing a little-known trick that can replace a lot of repeated JSON deserialization. I know, because I have seen it happen in so many website solutions.
Sometimes we need to store complex data that cannot easily be stored nicely in a content model class. It could be a big chunk of product data from a PIM system. It could be some complex data about an event on a CMS page. Or something else that would require a lot of different fields on the page, a deep hierarchy of content items, or it could be that we just want to store it as a JSON string. I will not be the judge of this.
It could be as simple as this:
public record ComplexDataObject
{
public string Key { get; set; }
public string Color { get; set; }
public Dictionary<string, string> Data { get; set; }
}
public class ArticlePage : PageData
{
public string RawJsonData { get; set; }
}
Then we can deserialize it in a controller, like this:
var complexDataObject = JsonSerializer.Deserialize<ComplexDataObject>(currentContent.RawJsonData);
But is this wise? When will the JSON string be deserialized?
On every request. This is not good for rendering performance.
Solution
Instead, we can utilize a little-known method that ensures the JSON is deserialized lazily and only on first use (until the parent content is removed from cache).
By making our own property type that takes care of deserializing the JSON to an object, we can get and set the typed object to the property, without prior serializing or deserializing.
[PropertyDefinitionTypePlugIn]
// Use this line in Commerce Connect.
// public class PropertyComplexDataObject : PropertyLongString, ILazyProperty
public class PropertyComplexDataObject: PropertyJson, ILazyProperty {
private ComplexDataObject _complexDataObject = null;
private Func _lazyValueFactory;
public override Type PropertyValueType => typeof (ComplexDataObject);
public virtual ComplexDataObject Data {
get => _complexDataObject;
set {
ThrowIfReadOnly();
if (value == null) {
ClearNoCheck();
}
if (_complexDataObject.Equals(value)) {
return;
}
// Check whether the object is actually different from the existing.
_lazyValueFactory = null;
_complexDataObject = value;
ModifiedNoCheck();
}
}
public override object Value {
get => Data;
set {
ThrowIfReadOnly();
SetPropertyValue(value, delegate {
_complexDataObject = value as ComplexDataObject;
});
}
}
public override void ParseToSelf(string value) {
ThrowIfReadOnly();
if (string.IsNullOrWhiteSpace(value)) {
ClearNoCheck();
return;
}
Data = JsonSerializer.Deserialize < ComplexDataObject > (value);
}
// In Commerce Connect, uncomment this line.
// public override object SaveData(PropertyDataCollection properties) => JsonSerializer.Serialize(Data);
public override bool IsNull => _lazyValueFactory == null && _complexDataObject == null;
protected override void SetDefaultValue() {
_complexDataObject = null;
_lazyValueFactory = null;
}
private void ClearNoCheck() {
if (IsNull) {
return;
}
ModifiedNoCheck();
SetDefaultValue();
}
bool ILazyProperty.HasLazyValue => _lazyValueFactory != null;
void ILazyProperty.AssignValueFactory(Func < object > valueFactory) {
ThrowIfReadOnly();
if (IsNull && valueFactory != null) {
ModifiedNoCheck();
}
_complexDataObject = null;
if (_lazyValueFactory != valueFactory) {
_lazyValueFactory = valueFactory;
ModifiedNoCheck();
}
}
}
We can then change the property on the ArticlePage, like this.
public class ArticlePage : PageData
{
[BackingType(typeof(PropertyComplexDataObject))]
[CultureSpecific]
[Editable(false)]
[UIHint(UIHint.Textarea)]
public virtual ComplexDataObject RawProductData { get; set; }
}
This works fine in CMS, because it has built-in support for lazy-loading JSON properties. For Commerce Connect we need some adjustments, because it does not have a native MetaDataType for JSON. So, we fall back to LongString, as shown in the code comments.
Bonus tip
As mentioned in the previous blog post, we should take care of the IsModified flag. This is a little harder in this case. But here is one way to do it with a custom Equals method.
public record ComplexDataObject
{
public string Key { get; set; }
public string Color { get; set; }
public Dictionary Data { get; set; }
// Even though records come with an Equals method, we override this to check the dictionary for equality.
public bool Equals(ComplexDataObject x, ComplexDataObject y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
if (!string.Equals(x.Key, y.Key, StringComparison.Ordinal)) return false;
if (!string.Equals(x.Color, y.Color, StringComparison.Ordinal)) return false;
if (x.Data.Count != y.Data.Count) return false;
foreach (var kvp in x.Data)
if (!y.Data.TryGetValue(kvp.Key, out var value) ||
!string.Equals(value, kvp.Value, StringComparison.Ordinal))
return false;
return true;
}
}