.NET Standard 2.0 Initializer String Generation using Reflection

Posted in software by Christopher R. Wirz on Sun Sep 24 2017



.NET Standard is the common set of APIs that all .NET implementations will conform to per the standard. This will prevent future fragmentation of the method calls and will ensure your .Net code will work in a portable way going forward. To that effect, it replaces Portable Class Libraries (PCLs) as the tool for building .NET libraries that work everywhere.

If you have read this blog before, you've probably seen a few interesting things done with the reflection API. I've been using an open-source project called Object Exporter in the past, but I wanted the ability to log only objects which threw an exception. This meant that I had to do it in code, not from the IDE. Object Explorer uses the EnvDTE api, which is probably overkill for this application - so we look to reflection once again.

Note: .Net Standard 2.0 does not have the entire reflection API seen in .Net Framework 4.6 and .Net Core 2.0, but it has enough for this demo.

Because the goal is not to modify the code under test, extension methods are used to add capabilities to a class. The methods will need to write out a parameter-less constructor and walk through the object's members. Let's try the example...


using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

/// <summary>
///     Adds methods to class objects using the extension pattern
/// </summary>
public static partial class Extensions {

/// <summary>
///     Gets the accessible members for a given type
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A list of members that can be set publicly</returns>
public static List<MemberInfo> GetAccessibleMemberInfo(this Type type)
{
	List<MemberInfo> members = new List<MemberInfo>();

	// Add Settable Properties
	members.AddRange(type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
							.Where(y => y.CanWrite));

	// Add Fields
	members.AddRange(type.GetFields(BindingFlags.Instance | BindingFlags.Public));

	return members;
}

/// <summary>
///     Converts a class object to its C# initializer string
/// </summary>
/// <typeparam name="T">The class type (can be inferred)</typeparam>
/// <param name="obj">The object.</param>
/// <param name="maxDepth">The maximum depth.</param>
/// <param name="tabs">The propagated tabs for pretty print.</param>
/// <returns>A string giving the C# to initialize the object</returns>
public static string ToObjectCSInitializer<T>(this T obj, int maxDepth = 3, string tabs = "") where T : class
{
	var sb = new StringBuilder(256);
	var t = obj.GetType();
	if (t.IsArray || typeof(IList).IsAssignableFrom(t))
	{
		return obj.GetObjectValueString(tabs, string.Empty, maxDepth);
	}
	sb.AppendLine($"new " + t.FullName + "() { ");
	var arr = t.GetAccessibleMemberInfo().ToArray();
	if (!arr.Any() || maxDepth == 0) { sb.AppendLine(tabs + "}"); return sb.ToString(); }
	int i = 0;
	while (i < arr.Length - 1)
	{
		try
		{
			sb.AppendLine(string.Format(tabs + "\t{0} = ", arr[i].Name) + arr[i].GetMemberValue(obj).GetObjectValueString(tabs + "\t", ",", maxDepth - 1).Trim());
		}
		catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); }
		i++;
	}
	try
	{
		sb.AppendLine(string.Format(tabs + "\t{0} = ", arr[i].Name) + arr[i].GetMemberValue(obj).GetObjectValueString(tabs + "\t", string.Empty, maxDepth - 1).Trim());
	}
	catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); }
	sb.AppendLine(tabs + "}");

	return sb.ToString().Trim();
}

/// <summary>
///     Gets the value based on member info.
/// </summary>
/// <param name="info">The member information.</param>
/// <param name="obj">The object instance.</param>
/// <returns>The value of the member</returns>
public static object GetMemberValue(this MemberInfo info, object obj)
{
	return (info as PropertyInfo)?.GetValue(obj, null) ?? (info as FieldInfo)?.GetValue(obj);
}

/// <summary>
///     Gets the C# value of the object as a string
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="tabs">The propagated tabs for pretty print.</param>
/// <param name="endComma">The end comma.</param>
/// <param name="maxDepth">The maximum depth.</param>
/// <returns>A string that should initialize a similar object in C#</returns>
private static string GetObjectValueString(this object obj, string tabs, string endComma = ",", int maxDepth = 2)
{
	var sb = new StringBuilder();
	if (maxDepth < 0) { return sb.ToString(); }

	if (Object.ReferenceEquals(obj, null))
	{
		sb.AppendLine(tabs + string.Format("null" + endComma));
		return sb.ToString();
	}
	var t = obj.GetType();
	switch (t.Name.ToLowerInvariant())
	{
		case "int":
		case "int64":
		case "int32":
		case "int16":
		case "decimal":
			sb.AppendLine(string.Format("{0}" + endComma, obj));
			return sb.ToString();
		case "double":
		case "float":
			sb.AppendLine(string.Format("{0}" + endComma, Convert.ToDouble(obj)));
			return sb.ToString();
		case "char":
			sb.AppendLine(string.Format("'{0}'" + endComma, obj));
			return sb.ToString();
		case "bool":
		case "boolean":
			sb.AppendLine(string.Format("{0}" + endComma, Convert.ToBoolean(obj) == true ? "true" : "false"));
			return sb.ToString();
		case "datetime":
			var date = Convert.ToDateTime(obj);
			sb.AppendLine(string.Format("new System.DateTime({0},{1},{2},{3},{4},{5})" + endComma, date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second));
			return sb.ToString();
		case "string":
			var val = obj.ToString();
			sb.AppendLine(string.Format("{0}" + endComma, string.IsNullOrEmpty(val) ? "string.Empty" : "\"" + val.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""));
			return sb.ToString();
		default:
			break;
	}
	if (t.IsArray)
	{
		var arr = obj as Array;
		if (arr == null) { return sb.ToString(); }

		var ut = t.GetElementType();
		sb.AppendLine("new " + ut.FullName + "[]{");
		int i = 0;
		while (i < arr.Length - 1)
		{
			sb.Append(tabs + "\t");
			sb.AppendLine(arr.GetValue(i).GetObjectValueString(tabs + "\t", ",", maxDepth - 1).Trim());
			i++;
		}
		sb.Append(tabs + "\t");
		sb.AppendLine(arr.GetValue(i).GetObjectValueString(tabs + "\t", string.Empty, maxDepth - 1).Trim());
		sb.AppendLine(tabs + "}" + endComma);
	}
	else if (typeof(IList).IsAssignableFrom(t))
	{
		var ol = obj as IList;
		if (ol == null) { return sb.ToString(); }
		var ut = t.GetGenericArguments()[0];
		var arr = Array.CreateInstance(ut, ol.Count);
		ol.CopyTo(arr, 0);
		if (arr == null) { return sb.ToString(); }
		sb.AppendLine("new System.Collections.Generic.List<" + ut.FullName + ">(){");
		int i = 0;
		while (i < arr.Length - 1)
		{
			sb.Append(tabs + "\t");
			sb.AppendLine(arr.GetValue(i).GetObjectValueString(tabs + "\t", ",", maxDepth - 1).Trim());
			i++;
		}
		sb.Append(tabs + "\t");
		sb.AppendLine(arr.GetValue(i).GetObjectValueString(tabs + "\t", string.Empty, maxDepth - 1).Trim());
		sb.AppendLine(tabs + "}" + endComma);
	}
	else
	{
		sb.AppendLine(obj.ToObjectCSInitializer(maxDepth, tabs).Trim() + endComma);
	}
	return sb.ToString();
}

/// <summary>
///     Writes an object's C# initializer to a file
/// </summary>
/// <typeparam name="T">The class type</typeparam>
/// <param name="obj">The object.</param>
/// <param name="name">The name of the variable in the file.</param>
/// <param name="filename">The filename.</param>
/// <param name="maxDepth">The maximum depth.</param>
public static void WriteCSToFile<T>(this T obj, string name = null, string filename = null, int maxDepth = 3) where T : class
{
	try
	{
		if (string.IsNullOrWhiteSpace(name))
		{
			name = obj.GetType().Name.Split('`')[0].ToLowerInvariant();
		}
		if (string.IsNullOrWhiteSpace(filename))
		{
			filename = name + ".cs";
		}
		File.WriteAllText(filename, "var " + name + " = " + obj.ToObjectCSInitializer(maxDepth));
	}
	catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.Message); }
}

} // Extensions

That should be enough to test. Now we can try a test class.


class TestMember
{
	public decimal Df = 34;
	public decimal Dp { get; set; } = 45;

	public System.DateTime DTf = new System.DateTime(2017, 8, 14);

	public System.DateTime DTp { get; set; } = new System.DateTime(2017, 9, 14);

	public TestMember TM = null;
}

class Test
{
	public int If = 1;
	public int Ip { get; set; } = 2;

	public double Df = 3.4;
	public double Dp { get; set; } = 4.5;

	public char Cf = 'c';
	public char Cp { get; set; } = 'D';


	public string Sf = "StringField";
	public string Sp { get; set; } = "StringProperty";

	public string[] StringsArray = new string[]
	{
		"a_test", "a_this", "a_out"
	};

	public IList<string> StringsList = new List<string>()
	{
		"l_test", "l_this", "l_out"
	};

	public TestMember Tmf = new TestMember()
	{
		TM = new TestMember()
		{
			Df = 3434,
			Dp = 4545
		}
	};

	public TestMember Tmfp { get; set; } = new TestMember() { Df = 44, Dp = 55 };

	public static Test Sample { get; } = new InitializerTest.Test()
	{
		Ip = 2,
		Dp = 4.5,
		Cp = 'D',
		Sp = "StringProperty",
		Tmfp = new InitializerTest.TestMember()
		{
			Dp = 55,
			Df = 44,
			TM = null
		},
		If = 1,
		Df = 3.4,
		Cf = 'c',
		Sf = "StringField",
		StringsArray = new System.String[]{
			"a_test",
			"a_this",
			"a_out"
		},
		StringsList = new System.Collections.Generic.List<System.String>(){
			"l_test",
			"l_this",
			"l_out"
		},
		Tmf = new InitializerTest.TestMember()
		{
			Dp = 45,
			Df = 34,
			TM = new InitializerTest.TestMember()
			{
				Dp = 4545,
				Df = 3434,
				TM = null
			}
		}
	};
}

With the test class built, now try the extension method. We'll write the output to the debug console.


Debug.WriteLine((new Test())).ToObjectCSInitializer(maxDepth:3));

Which gives us the following results

new InitializerTest.Test() {
	Ip = 2,
	Dp = 4.5,
	Cp = 'D',
	Sp = "StringProperty",
	Tmfp = new InitializerTest.TestMember() {
		Dp = 55,
		DTp = new System.DateTime(2017,9,14,0,0,0),
		Df = 44,
		DTf = new System.DateTime(2017,8,14,0,0,0),
		TM = null
	},
	If = 1,
	Df = 3.4,
	Cf = 'c',
	Sf = "StringField",
	StringsArray = new System.String[]{
		"a_test",
		"a_this",
		"a_out"
	},
	StringsList = new System.Collections.Generic.List<System.String>(){
		"l_test",
		"l_this",
		"l_out"
	},
	Tmf = new InitializerTest.TestMember() {
		Dp = 45,
		DTp = new System.DateTime(2017,9,14,0,0,0),
		Df = 34,
		DTf = new System.DateTime(2017,8,14,0,0,0),
		TM = new InitializerTest.TestMember() {
			Dp = 4545,
			DTp = new System.DateTime(2017,9,14,0,0,0),
			Df = 3434,
			DTf = new System.DateTime(2017,8,14,0,0,0),
			TM = null
		}
	}
}

Everything seems to work. Now I can test my classes which have given an exception!