Teaching SQLite how to handle DateTimeOffset values #3

Welcome back to part #3 of the aspect-oriented DateTimeOffset SQLite serializer! In this part, we’re going to actually read .NET assemblies, manipulate them, and recompile them. To be able to reconstruct the solution on your side, I’d suggest that you create a sample assembly with an SQLite table model class that contains a DateTimeOffset property flagged with the [DateTimeOffsetSerialize] attribute, as discussed in the previous blog post. In addition, create a new .NET console application project, and reference the SQLite.Net-PCL, SQLite.Net.Core-PCL, and Mono.Cecil NuGet libraries. We’ll need the former two libraries to be able to access certain SQLite attribute classes; Mono.Cecil will help us in decompilation of assemblies.

Read and write assemblies:

The first thing our application needs to do is read a given .NET assembly. To do so, it needs to know the assembly’s path – for the moment, let’s just hard-code our sample assembly’s file name as public property to simplify testing (we’ll refactor this later on):

class PostBuildTask
{
	public string AssemblyPath { get; set; } = @"C:\workspace\some_program_containing_sqlite_tables.exe";
	
	static void Main(string[] args)
	{
		//TODO
	}
}

Within the program’s main method, we simply start with reading in the given assembly, looping through all classes declared within the assembly, and writing it back to disk (without any changes). The Mono.Cecil library provides some helper classes and convenience methods that make this process really straightforward:

  • The assembly itself will be represented by an instance of the ModuleDefinition class. This class offers the static ReadModule method which handles all the file loading stuff and directly creates an instance for us.
  • In order to deal with advanced options during assembly loading, we use an instance of DefaultAssemblyResolver, and pass it to the ReadModule method. We need those advanced options due to two reasons:
    1. We must register the assembly’s path as search directory for additional assemblies (e.g., the SQLite.NET library references) that will be loaded automatically when needed at a later point of time during the manipulation process, and
    2. we need to load debug symbols in combination with the assembly, to ensure that the resulting assembly (after writing it back to disk) still allows debugging, reacts to breakpoints, etc.

After all, this first part of the Main method looks as follows:

var resolver = new DefaultAssemblyResolver();
resolver.AddSearchDirectory(Path.GetDirectoryName(AssemblyPath));
var module = ModuleDefinition.ReadModule(AssemblyPath, new ReaderParameters()
{
	AssemblyResolver = resolver,
	ReadSymbols = true
});

All the classes declared by the assembly are represented as TypeDefinition instances, and can be accessed directly through the ModuleDefinition’s Types collection property. Note that I intruduced a hasChanged boolean flag that we will set to true or false in the future, in order to ensure the assembly file is rewritten only if some of the CIL code has changed (imagine an assembly without a single property flagged as [DateTimeOffsetSerialize] – there is no need in modifying such a file):

bool hasChanged = false;

foreach (var type in module.Types)
{
	//TODO
}

if (hasChanged)
	module.Assembly.Write(AssemblyPath, new WriterParameters
	{
		WriteSymbols = true
	});

Obviously, our program does not manipulate anything so far. To change that, let’s discuss what will be needed to replace the TODO flag within the foreach that loops through all classes: First, find all properties of type DateTimeOffset flagged with the [DateTimeOffsetSerialize] attribute. Second, manipulate their signature and getter / setter methods. To separate the actual business logic, I created two extension methods, which we will bring to life in the next blog post:

internal static IEnumerable<PropertyDefinition> FindFlaggedProperties(this TypeDefinition type)
{
	var results = new List<PropertyDefinition>();

	//TODO loop through all properties of the given type and check whether they are of type
	//     DateTimeOffset and flagged with the DateTimeOffsetSerialize attribute

	return results;
}

internal static void RebuildProperty(this TypeDefinition type, PropertyDefinition property)
{
	//TODO create additional string property, and modify original one
}

For now, we can finish the program’s Main method by inserting references to these helper methods. Each class needs to be scanned for properties to be modified, afterwards each of these properties must be processed individually. As soon as at least one property has been adapted, the hasChanged flag is set in order to ensure the whole assembly file will be regenerated:

foreach (var type in module.Types)
{
	var flaggedProperties = type.FindFlaggedProperties();
	if (!flaggedProperties.Any())
		continue;

	foreach (var property in flaggedProperties)
	{
		type.RebuildProperty(property);
	}
	hasChanged = true;
}

The actual logic for finding flagged properties and manipulating them will be discussed in part #4 of this series of blog articles.