Creating an ORM in C# - Part 5

6/30/2009

Part 1 - Defining our classes
Part 2 - Reflection and class analysis
Part 3 - Creating our database and tables
Part 4 - Creating our stored procedures

Today I finally got around to adding support for classes... Well straight one to one mappings anyway, many to many and many to one are a bit trickier so I've put those off for another post. As always, you may want to read the earlier posts if you haven't already. But in this post I'm going to cover loading of classes, lazy loading, and cascading updates/inserts.

Basically when you think about it, as far as our CRM is concerned, a class is just another data type. However unlike say an int, we really don't want to load it when the rest of the class is loaded. I mean we can but that would potentially cause issues (we might end up loading our entire database or even end up in an endless loop of loading). So what most people do to combat this is use something called lazy loading. Lazy loading is simply a design pattern where you load/initialize an object only when it is needed. So how do we go about setting up our system to do lazy loading?

   1: public Property(Attribute Attribute, TypeBuilder TypeBuilder, FieldBuilder ChangedField,ClassManager Manager)
   2: {
   3:     Field = new Field(Attribute, TypeBuilder, FieldAttributes.Private);
   4:     if (Attribute.AttributeType == AttributeType.Map)
   5:     {
   6:         Attribute TempIDAttribute = new Attribute();
   7:         TempIDAttribute.AttributeType = Attribute.AttributeType;
   8:         Class TempClass = Manager[Attribute.Type];
   9:         foreach (Property TempProperty in TempClass.Properties)
  10:         {
  11:             if (TempProperty.Attribute.AttributeType == AttributeType.ID)
  12:             {
  13:                 TempIDAttribute.Type = TempProperty.Attribute.Type;
  14:             }
  15:         }
  16:         TempIDAttribute.Name = Attribute.Name + "ID";
  17:         IDField = new Field(TempIDAttribute, TypeBuilder, FieldAttributes.Public);
  18:     }
  19:     MethodAttributes GetSetAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual;
  20:     MethodBuilder ValuePropertyGet = TypeBuilder.DefineMethod("get_" + Attribute.Name, GetSetAttributes, Attribute.Type, Type.EmptyTypes);
  21:     ILGenerator Generator = ValuePropertyGet.GetILGenerator();
  22:     if (Attribute.AttributeType == AttributeType.Map)
  23:     {
  24:         Label ExitIfStatement = Generator.DefineLabel();
  25:         Label ExitIf2Statement = Generator.DefineLabel();
  26:         Label ExitStatement = Generator.DefineLabel();
  27:         Generator.DeclareLocal(Field.FieldType);
  28:         Generator.DeclareLocal(typeof(bool));
  29:         Generator.Emit(OpCodes.Nop);
  30:         Generator.Emit(OpCodes.Ldarg_0);
  31:         Generator.Emit(OpCodes.Ldfld, Field.FieldBuilder);
  32:         Generator.Emit(OpCodes.Ldnull);
  33:         Generator.Emit(OpCodes.Ceq);
  34:         Generator.Emit(OpCodes.Ldc_I4_0);
  35:         Generator.Emit(OpCodes.Ceq);
  36:         Generator.Emit(OpCodes.Stloc_1);
  37:         Generator.Emit(OpCodes.Ldloc_1);
  38:         Generator.Emit(OpCodes.Brtrue_S, ExitIfStatement);
  39:         Generator.Emit(OpCodes.Nop);
  40:         Generator.Emit(OpCodes.Call, typeof(HaterAide.Factory).GetMethod("CreateSession"));
  41:         Generator.Emit(OpCodes.Ldarg_0);
  42:         Generator.Emit(OpCodes.Ldfld, IDField.FieldBuilder);
  43:         Generator.Emit(OpCodes.Box, IDField.FieldType);
  44:         Generator.Emit(OpCodes.Ldarg_0);
  45:         Generator.Emit(OpCodes.Ldflda, Field.FieldBuilder);
  46:         MethodInfo TempMethod = typeof(HaterAide.Session).GetMethod("Select");
  47:         TempMethod = TempMethod.MakeGenericMethod(new Type[] { Field.FieldType });
  48:         Generator.Emit(OpCodes.Callvirt, TempMethod);
  49:         Generator.Emit(OpCodes.Nop);
  50:         Generator.Emit(OpCodes.Nop);
  51:         Generator.MarkLabel(ExitIfStatement);
  52:         Generator.Emit(OpCodes.Ldarg_0);
  53:         Generator.Emit(OpCodes.Ldfld, Field.FieldBuilder);
  54:         Generator.Emit(OpCodes.Ldnull);
  55:         Generator.Emit(OpCodes.Ceq);
  56:         Generator.Emit(OpCodes.Ldc_I4_0);
  57:         Generator.Emit(OpCodes.Ceq);
  58:         Generator.Emit(OpCodes.Stloc_1);
  59:         Generator.Emit(OpCodes.Ldloc_1);
  60:         Generator.Emit(OpCodes.Brtrue_S, ExitIf2Statement);
  61:         Generator.Emit(OpCodes.Nop);
  62:         Generator.Emit(OpCodes.Ldarg_0);
  63:         Generator.Emit(OpCodes.Newobj, Field.FieldType.GetConstructor(new Type[0] { }));
  64:         Generator.Emit(OpCodes.Stfld, Field.FieldBuilder);
  65:         Generator.Emit(OpCodes.Nop);
  66:         Generator.MarkLabel(ExitIf2Statement);
  67:         Generator.Emit(OpCodes.Ldarg_0);
  68:         Generator.Emit(OpCodes.Ldfld, Field.FieldBuilder);
  69:         Generator.Emit(OpCodes.Stloc_0);
  70:         Generator.Emit(OpCodes.Br_S, ExitStatement);
  71:         Generator.MarkLabel(ExitStatement);
  72:         Generator.Emit(OpCodes.Ldloc_0);
  73:         Generator.Emit(OpCodes.Ret);
  74:     }
  75:     else
  76:     {
  77:         Generator.Emit(OpCodes.Ldarg_0);
  78:         Generator.Emit(OpCodes.Ldfld, Field.FieldBuilder);
  79:         Generator.Emit(OpCodes.Ret);
  80:     } 
  81:  
  82:  
  83:     MethodBuilder ValuePropertySet = TypeBuilder.DefineMethod("set_" + Attribute.Name, GetSetAttributes, null, new Type[] { Attribute.Type }); 
  84:  
  85:     Generator = ValuePropertySet.GetILGenerator(); 
  86:  
  87:     Generator.Emit(OpCodes.Ldarg_0);
  88:     Generator.Emit(OpCodes.Ldarg_1);
  89:     Generator.Emit(OpCodes.Stfld, Field.FieldBuilder);
  90:     Generator.Emit(OpCodes.Ldarg_0);
  91:     Generator.Emit(OpCodes.Ldfld, ChangedField);
  92:     Generator.Emit(OpCodes.Ldstr, Attribute.Name);
  93:     Generator.Emit(OpCodes.Callvirt, typeof(List<string>).GetMethod("Add"));
  94:     Generator.Emit(OpCodes.Ret); 
  95:  
  96:     _Attribute = Attribute;
  97: }

As you can see above the Property constructor has been changed slightly. There are two sections that check if the attribute type is a mapping. These are our updates to deal with classes. The first section defines an ID field (the ID of the mapped class will go there when we load it). The second section is the actual code for getting the object. It checks whether the object is null, if it is null it loads the object and saves it for later (by creating a session object and simply calling Select), however if it is still null it creates a default object. Either way, it returns whatever is held in the object. Now the only tricky thing in here is the fact that Select is a generic function. It took me a good hour to figure out that while I could get the function itself easily enough that I had to define what type it should use. That's done with the MakeGenericMethod call. It seems simple once you've seen it but you'll hit your head against the wall for a while if you don't know what to do as the exceptions that are returned are fairly useless...

Anyway, that's it; we now have lazy loading and classes working... Ok, we still have some slight issues with the database, but the lazy loading is set up and our reflection is working properly. At this point though, we aren't setting up the tables or stored procedures to hold the class mappings. So how do we do this? Well because of how I've set up the SQL building, all we really need to do is create a new data type:

   1: internal class MapClassType:IDataType
   2: {
   3:     public MapClassType(Attribute Attribute, Reflection.Class Class, ClassManager Manager)
   4:         : base(Attribute)
   5:     {
   6:         _Class = Class;
   7:         _Manager = Manager; 
   8:  
   9:         MappedClass = _Manager[Attribute.Type];
  10:         MappedDataType = GlobalFunctions.GetSQLType(MappedClass.IDField, MappedClass, _Manager);
  11:         DataType = MappedDataType.DataType;
  12:         Type = MappedDataType.Type;
  13:     } 
  14:  
  15:     private Class _Class = null;
  16:     private ClassManager _Manager = null;
  17:     public Class MappedClass { get; set; }
  18:     public IDataType MappedDataType { get; set; } 
  19:  
  20:     public override string CreateTableCommand()
  21:     {
  22:         StringBuilder Builder = new StringBuilder(Name + " " + MappedDataType.Type);
  23:         return Builder.ToString();
  24:     }
  25: } 
  26:  
  27: internal static class GlobalFunctions
  28: {
  29:     public static IDataType GetSQLType(Attribute Attribute,Class Class,ClassManager Manager)
  30:     {
  31:         if (Attribute.Type.FullName.Equals("System.Int32", StringComparison.CurrentCultureIgnoreCase))
  32:         {
  33:             return new HaterAide.SQL.SQLServer.Statements.DataTypes.Int(Attribute);
  34:         }
  35:         else if (Attribute.Type.FullName.Equals("System.String", StringComparison.CurrentCultureIgnoreCase))
  36:         {
  37:             return new HaterAide.SQL.SQLServer.Statements.DataTypes.NVarChar(Attribute);
  38:         }
  39:         else if (Attribute.Type.FullName.Equals("System.Single", StringComparison.CurrentCultureIgnoreCase))
  40:         {
  41:             return new HaterAide.SQL.SQLServer.Statements.DataTypes.Float(Attribute);
  42:         }
  43:         else if (Attribute.Type.FullName.Equals("System.Double", StringComparison.CurrentCultureIgnoreCase))
  44:         {
  45:             return new HaterAide.SQL.SQLServer.Statements.DataTypes.Float(Attribute);
  46:         }
  47:         else if (Attribute.Type.FullName.Equals("System.Boolean", StringComparison.CurrentCultureIgnoreCase))
  48:         {
  49:             return new HaterAide.SQL.SQLServer.Statements.DataTypes.Bit(Attribute);
  50:         }
  51:         else if (Attribute.Type.FullName.StartsWith("System.DateTime", StringComparison.CurrentCultureIgnoreCase))
  52:         {
  53:             return new HaterAide.SQL.SQLServer.Statements.DataTypes.DateTime(Attribute);
  54:         }
  55:         else if (Attribute.Type.FullName.StartsWith("System.Guid", StringComparison.CurrentCultureIgnoreCase))
  56:         {
  57:             return new HaterAide.SQL.SQLServer.Statements.DataTypes.UniqueIdentifier(Attribute);
  58:         }
  59:         else if (Attribute.Type.FullName.StartsWith("System.Collections.Generic.List", StringComparison.CurrentCultureIgnoreCase))
  60:         {
  61:             return new HaterAide.SQL.SQLServer.Statements.DataTypes.List(Attribute, Class, Manager);
  62:         }
  63:         if (Attribute.AttributeType == AttributeType.Map)
  64:         {
  65:             return new HaterAide.SQL.SQLServer.Statements.DataTypes.MapClassType(Attribute, Class, Manager);
  66:         }
  67:         return null;
  68:     }
  69: }

As you can see, it acts similarly to the List class in that it doesn't use its own information but instead uses the ID field of the class that it maps to. And after we add it to the global function so that it's being returned, Insert, Select, Update, and Delete classes will simply pick it up and use it properly for the table/stored procedures that are created.

So we have our table that can store the class's ID so that it can load and map the classes properly. However we now have to deal with the more annoying part. We need to actually set up the various stored procedures such that they can use the ID properly.

   1: private void SetupProperties(object Object, SQLHelper Helper, Type ObjectType)
   2: {
   3:     foreach (IDataType Column in Columns)
   4:     {
   5:         if (Column != null && Column is MapClassType)
   6:         {
   7:             MapClassType TempColumn = (MapClassType)Column;
   8:             object MappedObject=ObjectType.GetProperty(TempColumn.Name).GetValue(Object,null);
   9:             if (MappedObject != null)
  10:             {
  11:                 Type MappedObjectType = MappedObject.GetType();
  12:                 object Value = MappedObjectType.GetProperty(TempColumn.MappedClass.IDField.Name).GetValue(MappedObject, null);
  13:                 Helper.AddParameter("@" + TempColumn.Name, Value, TempColumn.MappedDataType.DataType);
  14:             }
  15:             else if(TempColumn.MappedDataType.DataType==SqlDbType.Int)
  16:             {
  17:                 Helper.AddParameter("@" + TempColumn.Name, 0, TempColumn.MappedDataType.DataType);
  18:             }
  19:             else if (TempColumn.MappedDataType.DataType == SqlDbType.UniqueIdentifier)
  20:             {
  21:                 Helper.AddParameter("@" + TempColumn.Name, Guid.Empty, TempColumn.MappedDataType.DataType);
  22:             }
  23:         }
  24:         else if (Column != null && !Column.Type.Equals("List", StringComparison.CurrentCultureIgnoreCase))
  25:         {
  26:             PropertyInfo PropertyInfo = ObjectType.GetProperty(Column.Name);
  27:             object Value = PropertyInfo.GetValue(Object, null);
  28:             if (Column is NVarChar && !Column.IsPrimaryKey)
  29:             {
  30:                 foreach (IConstraint Constraint in Column.Constraints)
  31:                 {
  32:                     if (Constraint is Size)
  33:                     {
  34:                         Helper.AddParameter("@" + Column.Name, (string)Value, ((Size)Constraint).Length);
  35:                         break;
  36:                     }
  37:                 }
  38:             }
  39:             else if (!Column.IsPrimaryKey)
  40:             {
  41:                 Helper.AddParameter("@" + Column.Name, Value, Column.DataType);
  42:             }
  43:         }
  44:     }
  45: }

Above is a function from the Insert class. The function looks for nonlist items and adds them to the stored procedure. The top part checks if the individual column is a mapped class. Inside that if statement, it first gets the object's value. If the object is not null it gets the object's ID field and the value of that field. It then adds that value into the stored procedure.  A similar addition is made to the Update class. The Delete class doesn't require any updates at all since the only thing the stored procedure cares about is the ID of the class. The Select class is slightly different.

   1: private object LoadMainClass(object IDValue, SQLHelper Helper, object ClassInstance, IDataType IDField)
   2: {
   3:     try
   4:     {
   5:         Helper.Open();
   6:         if (IDField is NVarChar)
   7:         {
   8:             foreach (IConstraint Constraint in IDField.Constraints)
   9:             {
  10:                 if (Constraint is Size)
  11:                 {
  12:                     Helper.AddParameter("@" + IDField.Name, (string)IDValue, ((Size)Constraint).Length);
  13:                 }
  14:             }
  15:         }
  16:         else
  17:         {
  18:             Helper.AddParameter("@" + IDField.Name, IDValue, IDField.DataType);
  19:         }
  20:         Helper.ExecuteReader();
  21:         if (Helper.Read())
  22:         {
  23:             ClassInstance = Activator.CreateInstance(_Class.DerivedType);
  24:             foreach (IDataType Column in Columns)
  25:             {
  26:                 if (Column != null && Column is MapClassType)
  27:                 {
  28:                     FieldInfo TempField = _Class.DerivedType.GetField("_" + Column.Name + "IDDerived");
  29:                     TempField.SetValue(ClassInstance, Helper.GetParameter(Column.Name, null));
  30:                 }
  31:                 else if (Column != null && !Column.Type.Equals("List", StringComparison.CurrentCultureIgnoreCase))
  32:                 {
  33:                     PropertyInfo TempProperty = _Class.DerivedType.GetProperty(Column.Name);
  34:                     if (TempProperty.PropertyType.FullName.Equals("System.Single", StringComparison.CurrentCultureIgnoreCase))
  35:                     {
  36:                         TempProperty.SetValue(ClassInstance, float.Parse(Helper.GetParameter(Column.Name, null).ToString()), null);
  37:                     }
  38:                     else
  39:                     {
  40:                         TempProperty.SetValue(ClassInstance, Helper.GetParameter(Column.Name, null), null);
  41:                     }
  42:                 }
  43:             }
  44:         }
  45:     }
  46:     catch { }
  47:     finally { Helper.Close(); }
  48:     return ClassInstance;
  49: }

Luckily we've set the mapped classes ID field in a predictable manner. So what we do is go through the list of columns like before and if we see a MapClassType object, we get the ID field that we created back in the reflection stage. We then set that value to the ID value that we have stored in our database. We don't load the entire mapped class, just the ID, and we let the lazy loading that we set up before handle the rest.

Now we have our table, our stored procedures, and our lazy loading. That just leaves cascading saving of objects. This is actually slightly more difficult than it may look on the surface. Take for instance two classes, each have a mapping to one another. We save class A which then cascades to class B which then cascades to class A... Yay, infinite loop. Luckily a while back I set up the definitions to take in a boolean stating whether or not to do cascading on a specific property. So we simply set it up such that class B says don't update class A where as class A says cascade down to B. This just means that the person defining the relationship needs to be careful but that's the only issue. So for the Insert and Update classes we simply add a call to the function below from the Run function.

   1: private void CascadeSave(object Object, Type ObjectType)
   2: {
   3:     Session TempSession = Factory.CreateSession();
   4:     foreach (IDataType Column in Columns)
   5:     {
   6:         if (Column.Attribute.Cascade)
   7:         {
   8:             PropertyInfo Property = ObjectType.GetProperty(Column.Name);
   9:             object Value=Property.GetValue(Object,null);
  10:             if (Value != null)
  11:             {
  12:                 TempSession.Save(Value);
  13:             }
  14:         }
  15:     }
  16: }

That's it really. We check our properties, we check if they're null, and we save them. And since we've dealt with the infinite loop issue, we can actually make this function pretty dumb. Now you may assume that we're done but there is yet one more thing that needs to be done. Specifically when we're inserting an object for the first time, there is an issue. You see the order of operations when inserting is we first insert the main class information, we get back our new ID and set that in the object, insert our lists, and then cascade our insertion. Now what happens when the objects that it cascades haven't been saved before? Their ID is going to be the default value (so 0 for an int, etc.). So when we first saved the initial object, all of those mapped classes are pointing to the class with an ID of 0. So how do we fix this? Well after the cascade, these items have an ID as they've been inserted. So at this point all that we need to do is update the main class (we can skip the lists, etc.). So we add this function:

   1: private void UpdateMainProperties(object Object, Type ObjectType,SQLHelper Helper)
   2: {
   3:     try
   4:     {
   5:         Helper.Command = _Class.OriginalType.Name + "_Update";
   6:         Helper.Open();
   7:         foreach (IDataType Column in Columns)
   8:         {
   9:             if (Column != null && Column is MapClassType)
  10:             {
  11:                 MapClassType TempColumn = (MapClassType)Column;
  12:                 object MappedObject = ObjectType.GetProperty(TempColumn.Name).GetValue(Object, null);
  13:                 if (MappedObject != null)
  14:                 {
  15:                     Type MappedObjectType = MappedObject.GetType();
  16:                     object Value = MappedObjectType.GetProperty(TempColumn.MappedClass.IDField.Name).GetValue(MappedObject, null);
  17:                     Helper.AddParameter("@" + TempColumn.Name, Value, TempColumn.MappedDataType.DataType);
  18:                 }
  19:                 else if (TempColumn.MappedDataType.DataType == SqlDbType.Int)
  20:                 {
  21:                     Helper.AddParameter("@" + TempColumn.Name, 0, TempColumn.MappedDataType.DataType);
  22:                 }
  23:                 else if (TempColumn.MappedDataType.DataType == SqlDbType.UniqueIdentifier)
  24:                 {
  25:                     Helper.AddParameter("@" + TempColumn.Name, Guid.Empty, TempColumn.MappedDataType.DataType);
  26:                 }
  27:             }
  28:             else if (Column != null && !Column.Type.Equals("List", StringComparison.CurrentCultureIgnoreCase))
  29:             {
  30:                 PropertyInfo PropertyInfo = ObjectType.GetProperty(Column.Name);
  31:                 object Value = PropertyInfo.GetValue(Object, null);
  32:                 if (Column is NVarChar && !Column.IsPrimaryKey)
  33:                 {
  34:                     foreach (IConstraint Constraint in Column.Constraints)
  35:                     {
  36:                         if (Constraint is Size)
  37:                         {
  38:                             Helper.AddParameter("@" + Column.Name, (string)Value, ((Size)Constraint).Length);
  39:                             break;
  40:                         }
  41:                     }
  42:                 }
  43:                 else
  44:                 {
  45:                     Helper.AddParameter("@" + Column.Name, Value, Column.DataType);
  46:                 }
  47:             }
  48:         }
  49:         Helper.ExecuteNonQuery();
  50:     }
  51:     catch { }
  52:     finally { Helper.Close(); }
  53: }

We could simply call Save again but our system is pretty dumb and would simply call cascade again. We don't want to deal with that so a simple update function works well enough. And with that function we're finally done with classes. Well one to one mapped classes anyway. We have yet to deal with the pain which is many to many and many to one. But hey, the CRM is getting closer to being done. So give it a try, leave feedback, and happy coding.



Comments