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:
- Create an instance of the class in order to be able to get the default values.
- For each property in the class:
- If the property’s type is a primitive, extract all the needed information (name, type, description and default value)
- 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
As you can see, implementing this method took a couple of minutes and was very easy. Using this, I am sure of two things:
- The documentation table reflects the possible options in real time, since the rendering project references the library.
- The table will always be up to date and I save myself the constant “Don’t forget to document this new option” reminders.