Extract Available Settings Using C#

I was working these days on my library Blazor.Diagrams and I needed to write some documentation to show all the available/possible options. I could’ve done it manually, but there was quiet a few settings and I didn’t want to have to update the table every time new ones are added.

So I write a quick utility code that generates it for me on the go.

Example class

As an example class, we’ll just take my library’s settings BlazorOptions.

public class DiagramOptions
{
[Description("Key code for deleting entities")]
public string DeleteKey { get; set; } = "Delete";
[Description("Whether to inverse the zoom direction or not")]
public bool InverseZoom { get; set; }
[Description("The default component for nodes")]
public Type? DefaultNodeComponent { get; set; }
[Description("The grid size (grid-based snaping")]
public int? GridSize { get; set; }
[Description("Whether to enable the ability to group nodes together using [CTRL+ALT+G] or not")]
public bool GroupingEnabled { get; set; }
[Description("Whether to allow users to select multiple nodes at once using CTRL or not")]
public bool AllowMultiSelection { get; set; } = true;
[Description("Whether to allow panning or not")]
public bool AllowPanning { get; set; } = true;
[Description("Whether to allow zooming or not")]
public bool AllowZooming { get; set; } = true;
public DiagramLinkOptions Links { get; set; } = new DiagramLinkOptions();
}
public class DiagramLinkOptions
{
[Description("The default type of newly created links")]
public LinkType DefaultLinkType { get; set; }
[Description("The default component for links")]
public Type? DefaultLinkComponent { get; set; }
[Description("The default color for links")]
public string DefaultColor { get; set; } = "black";
[Description("The default color for selected links")]
public string DefaultSelectedColor { get; set; } = "rgb(110, 159, 212)";
}

Notice that I used the DescriptionAttribute available in System.ComponentModel to describe each available option. Of course, you can use whatever attribute you want.

Reflection to the rescue

First, we’ll create a class that holds all the information of a single setting:

public class PossibleOption
{
public string Name { get; }
public string Type { get; }
public string Default { get; }
public string Description { get; }
public PossibleOption(string name, string type, string @default, string description)
{
Name = name;
Type = type;
Default = @default;
Description = description;
}
}

Here’s what we’re gonna do:

  1. Create an instance of the class in order to be able to get the default values.
  2. For each property in the class:
    1. If the property’s type is a primitive, extract all the needed information (name, type, description and default value)
    2. If the property’s type is a complex object, run step 2 recursively.

We will also need to handle some edge cases for a better output. For example, it would be preferred to turn the type Nullable'1 into Int32?.

public static class ReflectionUtils
{
public static IEnumerable<PossibleOption> ExtractPossibleOptions<T>()
{
var type = typeof(T);
return ExtractPossibleOptions(type, string.Empty, Activator.CreateInstance(type));
}
private static IEnumerable<PossibleOption> ExtractPossibleOptions(Type type, string prefix, object instance)
{
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
var name = $"{prefix}{property.Name}";
var propertyValue = instance == null ? null : property.GetValue(instance);
if (!IsPrimitiveOrNullable(property.PropertyType))
{
foreach (var entry in ExtractPossibleOptions(property.PropertyType, name + ".", propertyValue))
yield return entry;
continue;
}
var typeName = FormatPropertyType(property.PropertyType);
var @default = propertyValue?.ToString();
var description = property.GetCustomAttribute<DescriptionAttribute>().Description;
yield return new PossibleOption(name, typeName, @default, description);
}
}
private static string FormatPropertyType(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
return $"{type.GetGenericArguments()[0].Name}?";
return type.Name;
}
private static bool IsPrimitiveOrNullable(Type type)
{
return type == typeof(object) ||
type == typeof(Type) ||
Type.GetTypeCode(type) != TypeCode.Object ||
Nullable.GetUnderlyingType(type) != null;
}
}

Example output

Since the documentation of my diagrams library is in Blazor itself, I am using the output of ExtractPossibleOptions to render a straightforward table:

<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
@foreach (var possibleOption in ReflectionUtils.ExtractPossibleOptions<DiagramOptions>())
{
<tr>
<th>@possibleOption.Name</th>
<td>@possibleOption.Type</td>
<td>@possibleOption.Default</td>
<td>@possibleOption.Description</td>
</tr>
}
</tbody>
</table>
</div>
Blazor.Diagrams' options table

Blazor.Diagrams' options table

As you can see, implementing this method took a couple of minutes and was very easy. Using this, I am sure of two things:

  1. The documentation table reflects the possible options in real time, since the rendering project references the library.
  2. The table will always be up to date and I save myself the constant “Don’t forget to document this new option” reminders.
Zanid Haytam Written by:

Zanid Haytam is an enthusiastic programmer that enjoys coding, reading code, hunting bugs and writing blog posts.