question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

A DataListItemAttribute with custom value can sometime not deserialize correctly

See original GitHub issue

Which Contentment version are you using?

3.0.1

Which Umbraco version are you using? For example: 8.14.1 - don’t just write v8

9.1.2

Bug summary

When I have an enum with DataListItem attributes, if one of the attributes is a number it will use the enum value whose zero-based position in the enum matches that number rather than whose DataListItem attribute matches that number, when I access it as IPublishedContent.

Steps to reproduce

  1. Create an Enum, such as this:
public enum CustomEnum
{
   [DataListItem(Value = "1")]
   First,
   [DataListItem(Value = "2")]
   Second
}
  1. Create a datatype with a Contentment dropdown using the CustomEnum as the source
  2. Add that datatype to a document type
  3. Display the selected enum value in the template for the doc type
  4. Create a document using that document type, and select First as the value from the dropdown list
  5. Navigate to the page for that document, and see that the selected value is displayed as Second

Expected result / actual result

I would expect that when I select First from the Umbraco back-office, it would display First when I show the value on the front-end.

This appears to result from the IPublishedContent.Value call executing the property value converter, which ends up at https://github.com/leekelleher/umbraco-contentment/blob/master/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/EnumDataListSource.cs#L188. The Enum.Parse call at that line won’t just check the name of the value, but will also check the integer value of the enum against the contents of the string. To use this as a workaround, you have to manually assign integer values to all values in your enum, and as some of my enums have over 2,000 values that can become quite onerous. I believe the code fix would be to simply move line 188 referenced above down below the foreach block that ends at line 200.

Do you have Umbraco ModelsBuilder enabled?

  • Yes, it is enabled.

What browsers are you seeing the problem on?

Firefox, Chrome, Safari, Microsoft Edge

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
benjaminccommented, Jan 13, 2022

I got lots of good data, at the bottom, but the TLDR is that Enum.Parse is MUCH faster than the current loop, especially when you start working with large enums with lots of values. There is a better way though, as it seems that a dictionary lookup is 5-20x faster than Enum.Parse! With that in mind, I would propose changing the ConvertValue method as follows:

private static readonly ConcurrentDictionary<string, Dictionary<string, object>> _enumValues = new ConcurrentDictionary<string, Dictionary<string, object>>();
public object ConvertValue(Type type, string value)
{
   if (string.IsNullOrWhiteSpace(value) == false && type?.IsEnum == true)
   {
      var valueMap = _enumValues.GetOrAdd(type, x => {
         var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
         var map = new Dictionary<string, object>(fields.Length);

         foreach (var field in fields)
         {
            var attr = field.GetCustomAttribute<DataListItemAttribute>(false);
            var attr2 = field.GetCustomAttribute<EnumMemberAttribute>(false);
            map[attr2?.Value ?? attr?.Value ?? field.Name] = Enum.Parse(type, field.Name);
         }
   
         return map;
      });

      if (valueMap.TryGetValue(value, out var enumValue)) return enumValue;

      // NOTE: Can't use `Enum.TryParse` here, as it's only available with generic types in .NET 4.8.
      try { return Enum.Parse(type, value, true); } catch { /* ¯\_(ツ)_/¯ */ }

#if NET472
      _logger.Debug<EnumDataListSource>($"Unable to find value '{value}' in enum '{type.FullName}'.");
#else
      _logger.LogDebug($"Unable to find value '{value}' in enum '{type.FullName}'.");
#endif
   }

   return type.GetDefaultValue();
}

Here are the test results for reference. Let me know if you are interested in the code I used, and I can share that as well.

.NET Framework 4.8
Enum with 10 values
	Starting Enum.Parse test
	Ran 1000000 iterations in 00:00:00.4597142 for an average runtime of 0.000459ms or 4.597142ticks
	Starting Attribute test
	Ran 10000 iterations in 00:00:01.0632018 for an average runtime of 0.1063ms or 1063.2018ticks
	Starting Dictionary test
	Ran 1000000 iterations in 00:00:00.0831061 for an average runtime of 8.3E-05ms or 0.831061ticks

Enum with 100 values
	Starting Enum.Parse test
	Ran 1000000 iterations in 00:00:00.6160590 for an average runtime of 0.000616ms or 6.16059ticks
	Starting Attribute test
	Ran 10000 iterations in 00:00:05.3377078 for an average runtime of 0.5337ms or 5337.7078ticks
	Starting Dictionary test
	Ran 1000000 iterations in 00:00:00.0392129 for an average runtime of 3.9E-05ms or 0.392129ticks

Enum with 1000 values
	Starting Enum.Parse test
	Ran 1000000 iterations in 00:00:00.3734050 for an average runtime of 0.000373ms or 3.73405ticks
	Starting Attribute test
	Ran 10000 iterations in 00:00:49.1137151 for an average runtime of 4.9113ms or 49113.7151ticks
	Starting Dictionary test
	Ran 1000000 iterations in 00:00:00.0561070 for an average runtime of 5.6E-05ms or 0.56107ticks


.NET 5.0
Enum with 10 values
	Starting Enum.Parse test
	Ran 1000000 iterations in 00:00:00.1720516 for an average runtime of 0.000172ms or 1.720516ticks
	Starting Attribute test
	Ran 10000 iterations in 00:00:00.3967951 for an average runtime of 0.0396ms or 396.7951ticks
	Starting Dictionary test
	Ran 1000000 iterations in 00:00:00.0410241 for an average runtime of 4.1E-05ms or 0.410241ticks

Enum with 100 values
	Starting Enum.Parse test
	Ran 1000000 iterations in 00:00:00.1496173 for an average runtime of 0.000149ms or 1.496173ticks
	Starting Attribute test
	Ran 10000 iterations in 00:00:03.1624293 for an average runtime of 0.3162ms or 3162.4293ticks
	Starting Dictionary test
	Ran 1000000 iterations in 00:00:00.0428021 for an average runtime of 4.2E-05ms or 0.428021ticks

Enum with 1000 values
	Starting Enum.Parse test
	Ran 1000000 iterations in 00:00:00.1367134 for an average runtime of 0.000136ms or 1.367134ticks
	Starting Attribute test
	Ran 10000 iterations in 00:00:37.9425542 for an average runtime of 3.7942ms or 37942.5542ticks
	Starting Dictionary test
	Ran 1000000 iterations in 00:00:00.0385279 for an average runtime of 3.8E-05ms or 0.385279ticks


.NET 6.0
Enum with 10 values
	Starting Enum.Parse test
	Ran 1000000 iterations in 00:00:00.6987096 for an average runtime of 0.000698ms or 6.987096ticks
	Starting Attribute test
	Ran 10000 iterations in 00:00:00.4145160 for an average runtime of 0.0414ms or 414.516ticks
	Starting Dictionary test
	Ran 1000000 iterations in 00:00:00.0333034 for an average runtime of 3.3E-05ms or 0.333034ticks

Enum with 100 values
	Starting Enum.Parse test
	Ran 1000000 iterations in 00:00:00.1666437 for an average runtime of 0.000166ms or 1.666437ticks
	Starting Attribute test
	Ran 10000 iterations in 00:00:03.5440884 for an average runtime of 0.3544ms or 3544.0884ticks
	Starting Dictionary test
	Ran 1000000 iterations in 00:00:00.0371926 for an average runtime of 3.7E-05ms or 0.371926ticks

Enum with 1000 values
	Starting Enum.Parse test
	Ran 1000000 iterations in 00:00:00.1517192 for an average runtime of 0.000151ms or 1.517192ticks
	Starting Attribute test
	Ran 10000 iterations in 00:00:29.8459201 for an average runtime of 2.9845ms or 29845.9201ticks
	Starting Dictionary test
	Ran 1000000 iterations in 00:00:00.0369712 for an average runtime of 3.6E-05ms or 0.369712ticks
0reactions
leekellehercommented, Mar 1, 2022

@benjaminc Just to let you know that I’ve implemented a dictionary/cache lookup for the Enum data-source, (see commit 361ff9a28a39795e263a0bcd75d78bb1ba07f96c). The code is different to what you offered as I’ve incorporated the GetItems in there too, since it was already iterating over the enum fields.

This will be part of the next release, should be v3.2.0 - no timeframe on that just yet, soon-ish though.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Cannot deserialize the current JSON array into type
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or...
Read more >
System.Text.Json deserializing of object[bool] does not ...
In .net core 2.1, the false value deserializes as boolean: Type of 'Bool param' is System.Boolean; value is False.
Read more >
How do i use the deserialized JSON data and what is ...
A part of this project is retrieving all the constant values from my JSON file and using them in my game. My JSON...
Read more >
Cannot deserialize JSON object
I have Debug.Logged the value directly one line before attempt of deserialization (the location where the error is thrown). Console shows indeed ...
Read more >
Serialization and Deserialization - WCF
The DataContractSerializer serializes strings containing control characters with a hexadecimal value below 20 as XML entities. This may cause a ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found