Last time I presented an outline of my Converter class and I demonstrated using that class to convert between two model objects. Today I'll begin a discussion of the internals of the Converter class itself.
When I created the Converter class I began by asking myself how I might go about writing a routine to convert the data between two typical model objects. I decided the first thing I would do is inventory the public properties on both objects and try to decide what properties should be mapped from one object to the other. To keep things simple I decided that my approach would be to map public properties to each other based solely on name.
After I decided how I would perform a manual conversion I started thinking about how I might automate the process to work with nothing but the incoming model objects - in other words, with no configuration information at runtime. I wanted to avoid a situation where a small change in a model class would trigger a need to modify a configuration file in order to perform a conversion. I decided the only way to do was to use reflection and build the conversion code dynamically. I decided that I would need to dynamically generate a conversion class for each unique combination of "source" and "destination" object and emit an assembly for that code at runtime. The only problem I saw with that approach is that reflection and code generation are both expensive runtime operations. So, I decided that I would perform the reflection/generation step once and then cache the results somehow. I decided to move ahead with that approach for my first attempt.
That's the overview, here's the actual code. Let's start at the beginning by looking at the Convert method. Here is a listing:
public object Convert(
object source,
Type toType
)
{
// Sanity check the arguments before them.
Guard.ThrowIfNull(source, "source");
Guard.ThrowIfNull(toType, "toType");
// Sanity check the toType argument before it.
if (toType == Type.Missing)
throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture,
Resources.Converter_InvalidType,
toType
),
"toType"
);
// Get the "from" type.
Type fromType = source.GetType();
// Can we take a shortcut?
if (fromType.Name == toType.Name)
return source;
// Get the converter instance.
IConverter converter = GetConverter(
fromType,
toType
);
// Perform the conversion and return the results.
return converter.Convert(source, toType);
}
The first thing we do is validate the incoming parameters (always a good idea). After that we get the types for both the source and the destination so we'll know what kind of conversion we need to perform. Once we have the types we call the GetCoverter method to get an IConverter object and call the Convert method to copy the data from the source object to a destination object of type 'toType". The GetConverter method's listing looks like this:
private IConverter GetConverter(
Type fromType,
Type toType
)
{
// Create a name for the converter class by combining the names
// of the "from" and "to" types in alphabetical order. Doing
// this ensures that the name will remain the same regardless
// of which way we are converting.
string className =
toType.Name.CompareTo(fromType.Name) <= 0 ?
toType.Namespace + toType.Name + "_to_" + fromType.Namespace + fromType.Name :
fromType.Namespace + fromType.Name + "_to_" + toType.Namespace + toType.Name;
// Remove any embedded dots from the class name.
className = className.Replace('.', '_');
IConverter converter;
LockCookie cookie;
try
{
// Play nicely with other threads.
cacheLock.AcquireReaderLock(Timeout.Infinite);
// Check the converter cache.
if (!converterCache.ContainsKey(className))
{
Assembly assembly;
// Check the assembly cache.
if (!assemblyCache.ContainsKey(className))
{
// Create the actual converter assembly.
assembly = CreateConverterAssembly(
className,
fromType,
toType
);
// Grab a writer lock.
cookie = cacheLock.UpgradeToWriterLock(
Timeout.Infinite
);
try
{
// Store the assembly in the cache.
assemblyCache[className] = assembly;
}
finally
{
// Dowgrade the lock.
cacheLock.DowngradeFromWriterLock(ref cookie);
}
}
else
assembly = assemblyCache[className];
// Create an instance of the converter.
converter = assembly.CreateInstance(
CONVERTER_NAMESPACE + "." + className
) as IConverter;
// Sanity check the converter instance.
if (converter == null)
throw new ConverterException(
string.Format(
CultureInfo.CurrentCulture,
Resources.Converter_FailedToCreateConverter,
fromType,
toType
)
);
// Grab a writer lock.
cookie = cacheLock.UpgradeToWriterLock(
Timeout.Infinite
);
try
{
// Store the converter in the cache.
converterCache[className] = converter;
}
finally
{
// Dowgrade the lock.
cacheLock.DowngradeFromWriterLock(ref cookie);
}
}
else
converter = converterCache[className];
}
finally
{
// Release the lock.
cacheLock.ReleaseReaderLock();
}
// Return the converter.
return converter;
}
The first thing we do in GetConverter is build a class name based on the "fromType" and the "toType" parameters, then we use that name to check for a converter helper in our internal cache. If we have ever performed a conversion for these types then we'll have a converter helper ready for use in the cache. If not, then we'll need to create a converter helper to perform the actual conversion, and we'll need to put that helper in our cache for later use. Most of the code in GetConverter is actually related to cache maintenance except the call to CreateConverterAssembly. If we need to create a converter helper, CreateConverterAssembly is where that action will take place. Once we have create a converter helper, and it's associated assembly, we use reflection again to create an instance of the helper and store it in the cache. Let's look at the CreateConverter Assembly method:
private static Assembly CreateConverterAssembly(
string className,
Type typeA,
Type typeB
)
{
// Create a compile unit.
CodeCompileUnit unit = new CodeCompileUnit();
// Add the "standard" assembly references.
unit.ReferencedAssemblies.Add("System.dll");
unit.ReferencedAssemblies.Add(
Path.GetFileName(Assembly.GetCallingAssembly().Location)
);
unit.ReferencedAssemblies.Add(
Path.GetFileName(typeA.Assembly.Location)
);
unit.ReferencedAssemblies.Add(
Path.GetFileName(typeB.Assembly.Location)
);
// Create a namespace.
CodeNamespace codeNameSpace = new CodeNamespace(
CONVERTER_NAMESPACE
);
// Add the namespace import statements.
codeNameSpace.Imports.Add(new CodeNamespaceImport("System"));
codeNameSpace.Imports.Add(
new CodeNamespaceImport(typeof(Converter).Namespace)
);
codeNameSpace.Imports.Add(
new CodeNamespaceImport(typeA.Namespace)
);
// Should we add an import statement for both namespaces?
if (typeA.Namespace != typeB.Namespace)
codeNameSpace.Imports.Add(
new CodeNamespaceImport(typeB.Namespace)
);
// Create the actual converter class.
CreateConverterClass(
className,
typeA,
typeB,
codeNameSpace
);
// Add the namespace to the compile unit.
unit.Namespaces.Add(codeNameSpace);
// Create a compiler.
DomCompiler compiler = new DomCompiler();
compiler.GenerateInMemory = true;
// Copy the referenced assemblies from the namespace.
foreach (string assemblyName in unit.ReferencedAssemblies)
compiler.ReferencedAssemblies.Add(assemblyName);
// Compile the codeDOM tree to an assembly.
return compiler.Compile(unit);
}
The purpose of CreateConverterAssembly is to generate a converter helper class and then compile that generated code into an in-memory assembly at runtime. (If you are unfamiliar with the CodeDOM classes then I would stop reading here and go Google for some light reading. An explanation of CodeDOM is WAY beyond the scope of this article.) We start the generation process by creating a CodeCompileUnit. That compile unit will eventually contain our converter helper class. The next thing we do is add the assembly references we'll need and add a namespace. The call to CreateConverterClass is where the actual converter helper is generated. Once the class is added to the namespace we use the DomCompiler (located in CG.Core) to compile the code and emit the assembly to memory.
That's about all the time I'll have for blogging today. Tomorrow I'll continue with things by discussing the CreateConverterClass method.
See ya!