For some dynamic databinding in Blazor controls, I have to use a model like :
public class DynamicDataGridResult
{
public DynamicDataGridResult()
{
Columns = new Dictionary<string, ColumnDefinition>();
Data = new List<Dictionary<string, object>>();
}
public Dictionary<string, ColumnDefinition> Columns { get; set; }
public List<Dictionary<string, object>> Data { get; set; }
}
public class ColumnDefinition
{
public string PropertyName { get; set; }
public string TypeFullName { get; set; }
public string Title { get; set; }
public string? Formatter { get; set; }
public Type GetColumnType()
{
var type = Type.GetType(TypeFullName);
return type;
}
}
In Columns, I describe my columns (TypeFullName to use for displaying, directly in .Net fashion, formatter for fine tuning ToString(xx), etc).
In Data, I can put my values.
Here's a working sample for both (code below is only here for example purpose, to illustrate with sample data but not important for my problem, see below if you want to skip please) :
DynamicDataGridResult dataGridModel = new();
var columns = new List<ColumnDefinition>()
{
new() { TypeFullName = typeof(int).FullName!, PropertyName = "EmployeeID", Title = "Employee ID"},
new() { TypeFullName = typeof(decimal?).FullName!, PropertyName = "Rating", Title = "Rating (Nullable<decimal>)" },
new() { TypeFullName = typeof(string).FullName!, PropertyName = "FirstName", Title = "First name"},
new() { TypeFullName = typeof(string).FullName!, PropertyName = "LastName", Title = "Last name"},
new() { TypeFullName = typeof(DateTime).FullName!, PropertyName ="HireDate", Title = "Hire date"},
};
columns.ForEach(c =>
{
if (c.TypeFullName.Contains("Decimal", StringComparison.OrdinalIgnoreCase)
|| c.TypeFullName.Contains("Double", StringComparison.OrdinalIgnoreCase)
|| c.TypeFullName.Contains("Single", StringComparison.OrdinalIgnoreCase))
{
c.Formatter = "{0:N2}";
}
});
dataGridModel.Columns = columns.ToDictionary(k => k.PropertyName);
dataGridModel.Data = Enumerable.Range(0, 25).Select(i =>
{
var row = new Dictionary<string, object>();
foreach (var column in dataGridModel.Columns)
{
object value;
if (column.Value.TypeFullName == typeof(int).FullName)
value = i;
else if (column.Value.TypeFullName == typeof(decimal?).FullName)
value = i % 3 != 0 ? ((double)i / 2.1234) : null;
else if (column.Value.TypeFullName == typeof(DateTime).FullName)
value = DateTime.Now.AddMonths(i);
else
value = $"{column.Key}{i}";
row.Add(column.Key, value);
}
return row;
}).ToList();
I believe you got the point.
If I directly bind my controls with these objects, it's ok.
But if I serialize/deserialize them through a Http.PostAsJsonAsync(url) / ReadFromJsonAsync() of System.Text.Json, it's broken, because all my "objects" have became some "JsonElement". Of course I understand this behavior, now I have to deal with it.
So my question is : how can I deserialize these as regular .Net objects, given I have the type name in another property ?
I searched to create a custom JsonConverter, but that's a monstruous beast, with a lot of subclasses and internal/sealed class, so I was able to override it.
Alternatively, I did something else to "recompute" my dictionaries after deserialization.
Here's a part of the conversion method :
private IEnumerable<Dictionary<string, object>> JsonElementAsObjects()
{
var liste = new List<Dictionary<string, object>>();
foreach (var row in GridResult.Data)
{
var dictionary = new Dictionary<string, object>();
foreach (var col in row)
{
var columnDefinition = Columns[col.Key];
object value = null;
if (col.Value != null)
{
value = GetObject((JsonElement) col.Value, columnDefinition);
}
dictionary.Add(col.Key, value);
}
liste.Add(dictionary);
}
return liste;
}
I tried to find a method in JsonElement / JsonValue to convert it to objects, but I only found a generic method (ConvertJsonElement<TypeToConvert>()
in JsonValue<T>
which doesn't really help, as my types are known only at runtime).
So I temporary extract some of its content, and adapted it to use runtime type :
private object GetObject(JsonElement element, ColumnDefinition columnDefinition)
{
var typeToConvert = columnDefinition.GetColumnType();
switch (element.ValueKind)
{
case JsonValueKind.Number:
if (typeToConvert == typeof(int) || typeToConvert == typeof(int?))
{
return element.GetInt32();
}
if (typeToConvert == typeof(long) || typeToConvert == typeof(long?))
{
return element.GetInt64();
}
if (typeToConvert == typeof(double) || typeToConvert == typeof(double?))
{
return element.GetDouble();
}
if (typeToConvert == typeof(short) || typeToConvert == typeof(short?))
{
return element.GetInt16();
}
if (typeToConvert == typeof(decimal) || typeToConvert == typeof(decimal?))
{
return element.GetDecimal();
}
if (typeToConvert == typeof(byte) || typeToConvert == typeof(byte?))
{
return element.GetByte();
}
if (typeToConvert == typeof(float) || typeToConvert == typeof(float?))
{
return element.GetSingle();
}
if (typeToConvert == typeof(uint) || typeToConvert == typeof(uint?))
{
return element.GetUInt32();
}
if (typeToConvert == typeof(ushort) || typeToConvert == typeof(ushort?))
{
return element.GetUInt16();
}
if (typeToConvert == typeof(ulong) || typeToConvert == typeof(ulong?))
{
return element.GetUInt64();
}
if (typeToConvert == typeof(sbyte) || typeToConvert == typeof(sbyte?))
{
return element.GetSByte();
}
break;
case JsonValueKind.String:
if (typeToConvert == typeof(string))
{
return element.GetString()!;
}
if (typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?))
{
return element.GetDateTime();
}
if (typeToConvert == typeof(DateTimeOffset) || typeToConvert == typeof(DateTimeOffset?))
{
return element.GetDateTimeOffset();
}
if (typeToConvert == typeof(Guid) || typeToConvert == typeof(Guid?))
{
return element.GetGuid();
}
if (typeToConvert == typeof(char) || typeToConvert == typeof(char?))
{
string? str = element.GetString();
Debug.Assert(str != null);
if (str.Length == 1)
{
return str[0];
}
}
break;
case JsonValueKind.True:
case JsonValueKind.False:
if (typeToConvert == typeof(bool) || typeToConvert == typeof(bool?))
{
return element.GetBoolean();
}
break;
}
return element;
}
It's working perfectly with this.
But having this is obviously an heavy useless post-process, with a lot of boilerplate code to maintain.
Could you please give me a cleaner solution to deserialize my data as "true objects", or at least to adapt them after deserialization like I did, but with a shorter code, using some hidden tricks in System.Text.Json ?
Thank you !