In this series, I will show how to generate, compile and load .NET code at runtime using different techniques.
For illustration purposes, a simple example is used throughout this article. The scenario of the example is a MVVM application with 100s of models. Many of these models need a corresponding view model that wrapes the models properties into e.g. notifiying properties.
A possible approach to build such an application could be to build a view model base class, that “magically” wrapps around a model class and instantly generates a matching view model with all the models properties. This article demonstrates how to do that.
General Approach
As often in software developement, the recommended approach here is to separate WHAT will be generated from HOW it is generated, i.e. to define a model and a view.
The objective here is to generate .NET assemblies that contain classes that can dynamically be instanciated and used.
A data model that describes the classes contained in such an assembly will be referred to as the generation model.
The object that will generate these classes (using either of the techniques discussed in this article) will be referred to as the generation view.
The models, view models and views of the example application will be referred to as the runtime models, view models and views.
The idea how to create a “magic” view model is the following: at runtime…
- …the runtime model is analyzed via reflection to determine the properties of the runtime view model to generate. These properties and other configuration parameters make up the generation model
- Call a generation view with the generation model as a parameter to generate an assembly. The generation view could use one of the techniques discussed in this article:
- Generate source code and compile it into MSIL using templating engines or pure string-concatenation, generate source code of any .NET langauge (in this article, C# is used) and compile it into a .NET assembly.
- Directly generate MSIL code. The .NET framework allowes to emit MSIL opcodes into dynamic assemblies. This can be done directly using Reflection or by abstraction using Linq Expression Trees.
- Load the generated types. Once the view model assemblies are generated, the appropriate types must be loaded and instanciated with model instances as constructor parameters.
In short, these steps are:
Data Model → Generation Model → (Source Code →) Dynamic Assembly → Loading.
Generation Model
The generation model will define types and properties to be generated. Therefore, there are two classes that make up the generation model: TypeGenerationModel and PropertyGenerationModel.
/// <summary> /// Defines the type that will be generated. /// </summary> public class TypeGenerationModel { /// <summary> /// Gets the namespace of the generated view model. /// </summary> public string Namespace { get; private set; } /// <summary> /// Gets the name of the generated view model for which to generate the view model. /// </summary> public string Name { get; private set; } /// <summary> /// Gets the type of the data model. /// </summary> public Type ModelType { get; private set; } /// <summary> /// Gets the type name of the data model. /// </summary> public string ModelTypeName { get; private set; } /// <summary> /// Gets the properties to generate. /// </summary> public IEnumerable<propertygenerationmodel> Properties { get; private set; } /// <summary> /// Initializes a new instance of the <see cref="TypeGenerationModel"/> class. /// </summary> /// <param name="modelType">Type of the data model for which to generate the view model.</param> /// <param name="namespace">The @namespace of the generated view model.</param> public TypeGenerationModel(Type modelType, string @namespace) { Namespace = @namespace; ModelType = modelType; ModelTypeName = modelType.GetTypeName(); Name = modelType.Name; Properties = ModelType .GetProperties(BindingFlags.Instance | BindingFlags.Public) .Select(p => new PropertyGenerationModel(this, p)) .Where(p => p.CanGet || p.CanSet) .ToList(); } } /// <summary> /// Defines a view model's property that will be generated. /// </summary> public class PropertyGenerationModel { /// <summary> /// Gets the full name of the property type. /// </summary> public string TypeName { get; private set; } /// <summary> /// Gets the name of the property. /// </summary> public string Name { get; private set; } /// <summary> /// Gets a value indicating whether this property leads to another generation model. /// </summary> public bool IsGenerationModel { get; private set; } /// <summary> /// Gets a value indicating whether this instance is a list. /// </summary> public bool IsList { get; private set; } /// <summary> /// Gets or sets a value indicating whether this property has a setter. /// </summary> public bool CanSet { get; private set; } /// <summary> /// Gets or sets a value indicating whether this property has a getter. /// </summary> public bool CanGet { get; private set; } /// <summary> /// Gets or sets the type of the element returned by the property. /// </summary> public Type ElementType { get; private set; } /// <summary> /// Initializes a new instance of the <see cref="PropertyGenerationModel"/> class. /// </summary> /// <param name="parent">The type model that contains the property.</param> /// <param name="property">The property info to generate from.</param> public PropertyGenerationModel(TypeGenerationModel parent, PropertyInfo property) { // Let the property of the view model as the property of the model. Name = property.Name; var getter = property.GetGetMethod(); CanGet = getter != null && getter.IsPublic; var setter = property.GetSetMethod(); CanSet = setter != null && setter.IsPublic; // In this small demo, only support Lists to contain child view models. IsList = property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(List<>); // If this is a list, we'll need type of the elements, // not of the property itseld. var itemType = IsList ? property.PropertyType.GetGenericArguments().First() : property.PropertyType; // Assume that all models are in one assembly and can be // identified by just being in that assembly. IsViewModel = itemType.Assembly == property.DeclaringType.Assembly; ElementType = IsGenerationModel ? itemType : property.PropertyType; // Get the type name in a format that can be placed into code // (i.e. MyType<T> rather than MyType`1 for generic types) TypeName = IsGenerationModel ? parent.Namespace + "." + itemType.Name : property.PropertyType.GetTypeName(); } }
Since the data model may reference other data models, the types of these data models need to be collected and added to the generation model as well. The following code creates a generation model from the root type and traverses the proeprties until all data model types are collected:
internal class Program { private static void Main(string[] args) { var generationModels = CreateGenerationModels(typeof(Person), "Generated"); // ... rest of the work here ... } public static IEnumerable<TypeGenerationModel> CreateGenerationModels(Type type, string @namespace, HashSet<Type> processedTypes) { if (processedTypes == null) processedTypes = new HashSet<Type>(); processedTypes.Add(type); var model = new TypeGenerationModel(type, @namespace); yield return model; var models = from p in model.Properties where p.IsGenerationModel && !processedTypes.Contains(p.ElementType) from m in CreateGenerationModels(p.ElementType, @namespace, processedTypes) select m; foreach (var referencedModel in models) { yield return referencedModel; } } }
The runtime view models (the types that will be generated) will inherit a common base class called ViewModelBase. This base class contains common code needed to do some of the “magic”. For instance, if one of the models properties isn’t a simple type, but another model a collection of other models, then the properties values must be wrapped in a view model as well. The base class contains the methods needed to do this kind of stuff.
The next sections will describe generation views that use this generation model to generate the described types.