Reputation: 250
Before anyone start suggesting any libraries like Newtonsoft.Json
, System.Text.Json
or whatever other nice and simple thing I would love to use, know that I am unable to use anything other than System.Json
, because I am working within the constraints of an application, (I'm making a plugin), I have no influence over and has stopped active development, (it's an ERP system that only has security patches once per year, and feature requests result in passive-aggressive responses; even when I offered to do the changes myself free of charge).
I have some json, and some nice domain models, (objects, classes, models, entities, whatever you like to call public classes with properties), and I want them to get married. And using reflection is a pain when there are nesting.
Could anyone please show me some nice ways of doing this that does not require any nugets or dlls? All I'm finding when searching is related to all other libraries than System.Json
.
Here's what I've been doing before I gave up, (I have refactored to something that looks like the use-case but that's because of contractual reasons):
public void BuildSettings(string settingsPath = "appsettings.json", params Type[] types)
{
if (!types.Any())
throw new ArgumentException("The type parameters cannot be empty", nameof(types));
var file = new FileInfo(settingsPath);
if (!file.Exists)
throw new ArgumentException($"No settings file found in the path '{settingsPath}'", nameof(settingsPath));
using (var reader = file.OpenText())
{
var rootJson = JsonValue.Load(reader);
if (rootJson.JsonType != JsonType.Object)
throw new ArgumentException($"The settings file must be a Json Object, but a '{rootJson.JsonType}' was found", nameof(settingsPath));
var jsonObject = rootJson as JsonObject;
if (jsonObject == null)
throw new NullReferenceException("The json object is null");
foreach (var type in types)
{
if (jsonObject.ContainsKey(type.Name))
{
var jsonSetting = jsonObject[type.Name] as JsonObject;
var properties = type.GetProperties();
foreach (var property in properties)
{
var value = jsonSetting[property.Name];
var propertyType = property.PropertyType;
property.SetValue();
// TODO: Ask StackOwerflow
}
}
}
}
}
There are some stupidity in this, but I don't make the rules
Upvotes: 1
Views: 384
Reputation: 4411
I guess you are in for some long hours... looks to me like you have to develop an ORM, I was faced with the same once where I had to make an ORM for ATM machines.
This code will not work for you as it assumes an IDataReader however you are in need of the property mapping using reflection and this code is in there.
Let me know if this get's you going as it has some optimisation in it for reusing reflected types etc.
I think you need to test for a property being a Class and create it using the activator and use an efficient cast using
if (property.PropertyType.BaseType == typeof(Enum))
{
property.SetValue(obj, (int)value);
}
else if (property.PropertyType.BaseType == typeof(Guid))
{
property.SetValue(obj, Guid.Parse(value.ToString().ToUpper()));
}
else
{
property.SetValue(obj, Convert.ChangeType(value, property.PropertyType));
}
Ido not know how my callses you need to support or if it is a fixed amount, you might just be after making a static T Parse(this T target, string json) method where you fragment the json string based on { and } brackets to get properties and [ and ] to get Arrays.
Here is the code for that, I used it for a VCARD Json parser I hade to make some time ago
/// <summary>
/// Splits the specified string in sections of open en closing characters.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="open">The opening char indicating where to start to read .</param>
/// <param name="close">The close char, indicating the part where should stop reading.</param>
/// <returns>IReadOnlyList<System.String>.</returns>
/// <exception cref="System.ArgumentNullException">text</exception>
/// <exception cref="ArgumentNullException">Will throw an exception if the string that needs to be split is null or empty</exception>
public static IReadOnlyList<string> Split(this string text, char open, char close)
{
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
var counted = 0;
var result = new List<string>();
var sb = new StringBuilder();
foreach (char c in text)
{
if (c == open)
{
if (counted != 0)
sb.Append(c);
counted++;
continue;
}
if (c == close)
{
counted--;
if (counted != 0)
sb.Append(c);
continue;
}
if (counted > 0)
{
sb.Append(c);
}
else if (counted == 0 && sb.Length > 0)
{
result.Add(sb.ToString());
sb.Clear();
}
}
return result;
}
Here is the full mapper I had to make where you see the mentioned reflection
class Mapper
{
ConcurrentDictionary<Type, PropertyInfo[]> _properties = new ConcurrentDictionary<Type, PropertyInfo[]>();
ConcurrentDictionary<string, List<string>> _fieldNames = new ConcurrentDictionary<string, List<string>>();
/// <summary>
/// Maps the specified reader to a given class. the reader must contain all properties of the type provided.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="reader">The reader.</param>
/// <returns></returns>
public IEnumerable<T> Map<T>(SqlDataReader reader)
{
var result = new List<T>();
if (!reader.HasRows)
return result;
var type = typeof(T);
if (!_properties.TryGetValue(type, out PropertyInfo[] prop))
{
prop = type.GetProperties();
_properties.TryAdd(type, prop);
}
if (!_fieldNames.TryGetValue(type.Name, out List<string> fieldNames))
{
var names = new List<string>(reader.FieldCount);
for (int i = 0; i < reader.FieldCount; i++)
{
names.Add(reader.GetName(i));
}
fieldNames = names;
_fieldNames.TryAdd(type.Name, fieldNames);
}
while (reader.Read())
{
var obj = Activator.CreateInstance<T>();
foreach (var property in prop)
{
if (fieldNames.Contains(property.Name))
{
var value = reader[property.Name];
if (value == DBNull.Value)
continue;
if (property.PropertyType.BaseType == typeof(Enum))
{
property.SetValue(obj, (int)value);
}
else if (property.PropertyType.BaseType == typeof(Guid))
{
property.SetValue(obj, Guid.Parse(value.ToString().ToUpper()));
}
else
{
property.SetValue(obj, Convert.ChangeType(value, property.PropertyType));
}
}
}
result.Add(obj);
}
return result;
}
public IEnumerable<T> Map<T,Y>(SqlDataReader reader,Y owner)
{
var result = new List<T>();
if (!reader.HasRows)
return result;
var type = typeof(T);
if (!_properties.TryGetValue(type, out PropertyInfo[] prop))
{
prop = type.GetProperties();
_properties.TryAdd(type, prop);
}
if (!_fieldNames.TryGetValue(type.Name, out List<string> fieldNames))
{
var names = new List<string>(reader.FieldCount);
for (int i = 0; i < reader.FieldCount; i++)
{
names.Add(reader.GetName(i));
}
fieldNames = names;
_fieldNames.TryAdd(type.Name, fieldNames);
}
while (reader.Read())
{
var obj = Activator.CreateInstance<T>();
foreach (var property in prop)
{
if (property.PropertyType == typeof(Y))
{
property.SetValue(obj, owner);
continue;
}
if (fieldNames.Contains(property.Name))
{
var value = reader[property.Name];
if (value == DBNull.Value)
continue;
if (property.PropertyType.BaseType == typeof(Enum))
{
property.SetValue(obj, (int)value);
}
else
{
property.SetValue(obj, Convert.ChangeType(value, property.PropertyType));
}
}
}
result.Add(obj);
}
return result;
}
/// <summary>
/// Maps the specified reader to a given class. the reader must contain all properties of the type provided.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="reader">The reader.</param>
/// <returns></returns>
public T MapOne<T>(SqlDataReader reader)
{
if (!reader.HasRows)
return default;
var type = typeof(T);
if (!_properties.TryGetValue(type, out PropertyInfo[] prop))
{
prop = type.GetProperties();
_properties.TryAdd(type, prop);
}
if (!_fieldNames.TryGetValue(type.Name, out List<string> fieldNames))
{
var names = new List<string>(reader.FieldCount);
for (int i = 0; i < reader.FieldCount; i++)
{
names.Add(reader.GetName(i));
}
fieldNames = names;
_fieldNames.TryAdd(type.Name, fieldNames);
}
if (reader.Read())
{
var obj = Activator.CreateInstance<T>();
foreach (var property in prop)
{
if (fieldNames.Contains(property.Name))
property.SetValue(obj, reader[property.Name]);
}
return obj;
}
else
{
return default;
}
}
/// <summary>
/// Maps the specified reader to a given class. the reader must contain all properties of the type provided.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="reader">The reader.</param>
/// <returns></returns>
public IEnumerable<T> Map<T>(SqlDataReader reader, object[] args)
{
var result = new List<T>();
if (!reader.HasRows)
return result;
var type = typeof(T);
if (!_properties.TryGetValue(type, out PropertyInfo[] prop))
{
prop = type.GetProperties();
_properties.TryAdd(type, prop);
}
if (!_fieldNames.TryGetValue(type.Name, out List<string> fieldNames))
{
var names = new List<string>(reader.FieldCount);
for (int i = 0; i < reader.FieldCount; i++)
{
names.Add(reader.GetName(i));
}
fieldNames = names;
_fieldNames.TryAdd(type.Name, fieldNames);
}
while (reader.Read())
{
var obj = (T)Activator.CreateInstance(type, args);
foreach (var property in prop)
{
if (fieldNames.Contains(property.Name))
property.SetValue(obj, reader[property.Name]);
}
result.Add(obj);
}
return result;
}
}
Upvotes: 1