Click here to Skip to main content
15,885,244 members
Articles / Programming Languages / C#

Databasing framework with LINQ support

Rate me:
Please Sign up or sign in to vote.
4.40/5 (7 votes)
25 Sep 2009CPOL8 min read 32.3K   163   16  
An easy to use framework with multi-tier, user types, and LINQ support.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Text;
using Pfz.Extensions.AttributeExtensions;
using Pfz.Extensions.DatabaseNameExtensions;

namespace Pfz.Databasing.Internal
{
	internal sealed class CSharpGenerator
	{
		private StringBuilder fStringBuilder = new StringBuilder();
		private int fLevel = 0;
		private bool fLineHasRecords;

		private void p_Write(string line)
		{
			if (!fLineHasRecords)
			{
				for(int i=0; i<fLevel; i++)
					fStringBuilder.Append("\t");
					
				fLineHasRecords = true;
			}
			
			fStringBuilder.Append(line);
		}
		private void p_WriteLine(string line)
		{
			p_Write(line);
			fStringBuilder.Append("\r\n");
			fLineHasRecords = false;
		}
		private void p_WriteLine(params object[] values)
		{
			p_Write(values);
			
			fStringBuilder.Append("\r\n");
			fLineHasRecords = false;
		}

		private void p_Write(params object[] values)
		{
			foreach (object value in values)
			{
				Type type = value as Type;
				if (type == null)
					p_Write(value.ToString());
				else
				{
					p_Write("global::");
					p_Write(p_GetName(type));
				}
			}
		}
		
		private static List<PropertyInfo> p_GetProperties(Type type, bool primaryKeyOnly)
		{
			List<PropertyInfo> result = new List<PropertyInfo>();
			p_AddProperties(result, type, primaryKeyOnly);
			return result;
		}
		private static void p_AddProperties(List<PropertyInfo> list, Type type, bool primaryKeyOnly)
		{
			foreach(var interfaceType in type.GetInterfaces())
				foreach(var propertyInfo in interfaceType.GetProperties())
					p_AddProperty(list, propertyInfo, primaryKeyOnly);

			foreach(var propertyInfo in type.GetProperties())
				p_AddProperty(list, propertyInfo, primaryKeyOnly);
		}
		private static void p_AddProperty(List<PropertyInfo> list, PropertyInfo propertyInfo, bool primaryKeyOnly)
		{
			if (primaryKeyOnly && !propertyInfo.ContainsCustomAttribute<PrimaryKeyAttribute>())
				return;
			
			if (!propertyInfo.CanRead)
				return;
			
			if (!propertyInfo.CanWrite)
				return;
				
			if (propertyInfo.ContainsCustomAttribute<DatabaseIgnoredAttribute>())
				return;
				
			list.Add(propertyInfo);
		}
		
		public void Implement(Type type)
		{
			PropertyInfoPaths propertyPaths = new PropertyInfoPaths(type, false);
			PropertyInfoPaths primaryKeyPaths = new PropertyInfoPaths(type, true);
			var dictionaryOfPropertyPaths = propertyPaths.GetDictionaryByPrimaryProperty();
			var propertyInfos = p_GetProperties(type, false);
		
			bool isPersisted = type.ContainsCustomAttribute<DatabasePersistedAttribute>();

			string fieldsSql = null;
			string whereSql = null;
			string whereSqlMT = null;
			if (isPersisted)
			{
				fieldsSql = p_GetFieldsSql(propertyPaths);
				whereSql = p_GetWhereSql(type, primaryKeyPaths, null);
				whereSqlMT = p_GetWhereSql(type, primaryKeyPaths, "MT.");
			}
		
			p_Write("namespace Pfz.Databasing.ImplementedRecords.");
			p_Write(type.Namespace);
			using (p_CreateBlock())
			{
				string databaseName = type.GetDatabaseName();
				if (isPersisted)
				{
					p_WriteLine("[global::System.Serializable]");
					p_WriteLine("[", typeof(DatabaseNameAttribute), "(\"", databaseName, "\")]");
					p_Write("public class ");
				}
				else
					p_Write("public static class ");
					
				p_Write(type.Name);
				
				if (isPersisted)
				{
					p_WriteLine(":");
					p_WriteLine("  ", typeof(RecordBase), ",");
					p_WriteLine("  ", type);
				}
				else
					p_WriteLine("");
					
				using (p_CreateBlock())
				{
					p_Write("public const string FieldsSql = \"");
					p_Write(fieldsSql);
					p_WriteLine("\";");

					p_Write("public const string PrimaryKeyWhereSql = \"");
					p_Write(whereSql);
					p_WriteLine("\";");
					
					foreach(var pair in dictionaryOfPropertyPaths)
					{
						var firstProperty = pair.Key;
						if (firstProperty.DeclaringType != type)
							continue;
						
						var paths = pair.Value;
						if (paths[0].Length == 1)
						{
							p_Write("public static readonly global::System.Reflection.PropertyInfo PropertyOf_");
							p_Write(firstProperty.Name);
							p_Write(" = typeof(global::");
							p_Write(p_GetName(type));
							p_Write(").GetProperty(\"");
							p_Write(firstProperty.Name);
							p_WriteLine("\");");
						}
						else
						{
							int pathIndex = -1;
							foreach(var path in paths)
							{
								pathIndex++;
								
								p_Write("public static readonly ", typeof(PropertyInfoPath), " PropertyPathOf_");
								p_Write(firstProperty.Name);
								p_Write("_", pathIndex);
								p_Write(" = new ", typeof(PropertyInfoPath), "(new global::System.Reflection.PropertyInfo[]{");
								
								foreach(var propertyInfo in path)
									p_Write("typeof(", propertyInfo.DeclaringType, ").GetProperty(\"", propertyInfo.Name, "\"), ");
									
								fStringBuilder.Length -= 2;
								
								p_WriteLine("});");
							}
						}
					}
					
					p_WritePropertiesCollection(type, primaryKeyPaths, "PrimaryKeyProperties");
					p_WritePropertiesCollection(type, propertyPaths, "DatabaseProperties");

					p_Write("public static readonly ", typeof(ReadOnlyCollection<PropertyInfo>), " NonPersistedReferenceProperties = new ", typeof(ReadOnlyCollection<PropertyInfo>), "(new ", typeof(PropertyInfo[]), "{");
					bool hasAny = false;
					foreach(var property in propertyInfos)
						hasAny |= p_WriteNonPersistedReferenceProperty(property);
						
					if (hasAny)
						fStringBuilder.Length -= 2;
					
					p_WriteLine("});");

					if (!isPersisted)
						return;

					p_Write("public const string SimpleSelectAllSql = \"SELECT ");
					p_Write(fieldsSql);
					p_Write(" FROM ");
					p_Write(databaseName);
					p_WriteLine(" MT\";");
					
					p_Write("public const string SelectByPrimaryKeySql = \"SELECT ");
					p_Write(fieldsSql);
					p_Write(" FROM ");
					p_Write(databaseName);
					p_Write(" MT WHERE ");
					p_Write(whereSqlMT);
					p_WriteLine("\";");
					
					p_WriteInsertSql(databaseName, propertyPaths.Count, fieldsSql);
					
					p_WriteLine("public const string DeleteSql = \"DELETE FROM ", databaseName, " WHERE ", whereSql, "\";");

					p_CreateBasicConstructor(type);
					p_CreateFromDatabaseConstructor(type, propertyPaths, dictionaryOfPropertyPaths);
					p_CreateCloneConstructor(type, dictionaryOfPropertyPaths);

					p_WriteLine("public override object Clone()");
					using(p_CreateBlock())
					{
						p_WriteLine(type.Name, " result = new ", type.Name, "(fRecordMode, this);");
						p_WriteLine("result.fOldRecord = fOldRecord;");
						p_WriteLine("result.fMustDeleteOnApply = fMustDeleteOnApply;");
						p_WriteLine("return result;");
					}
					
					p_WriteLine("public override ", typeof(IRecord), " UntypedCreateUpdateRecord()");
					using(p_CreateBlock())
					{
						p_WriteLine("if (fRecordMode != ", typeof(RecordMode), ".ReadOnly) throw new ", typeof(DatabaseException), "(\"Can't create an update record from records that are not read-only.\");");
						p_WriteLine(type.Name, " result = new ", type.Name, "(", typeof(RecordMode), ".Update, this);");
						p_WriteLine("result.fOldRecord = this;");
						p_WriteLine("return result;");
					}

					p_WriteGetHashCode(primaryKeyPaths);
					p_WriteEquals(type, propertyPaths);
					
					foreach(var pair in dictionaryOfPropertyPaths)
					{
						var propertyInfo = pair.Key;
						Type propertyType = propertyInfo.PropertyType;
						
						p_Write("private ");
						p_Write(propertyType);
						p_Write(" _");
						p_Write(propertyInfo.Name);
						p_WriteLine(";");
						
						if (typeof(IRecord).IsAssignableFrom(propertyType))
						{
							p_Write("private object[] __keyOf_");
							p_Write(propertyInfo.Name);
							p_WriteLine(";");
						}
						
						p_Write("public ");
						p_Write(propertyType);
						p_Write(" ");
						p_WriteLine(propertyInfo.Name);
						using(p_CreateBlock())
						{
							p_WriteLine("get");
							using (p_CreateBlock())
							{
								if (typeof(IRecord).IsAssignableFrom(propertyType))
								{
									p_Write("if (_");
									p_Write(propertyInfo.Name);
									p_WriteLine(" == null)");
									using(p_CreateBlock())
									{
										p_Write("if (__keyOf_");
										p_Write(propertyInfo.Name);
										p_WriteLine(" == null) return null;");
										
										p_Write("_");
										p_Write(propertyInfo.Name);
										p_Write(" = global::");
										p_Write(typeof(Record).FullName);
										p_Write(".LoadByPrimaryKey<", propertyInfo.PropertyType, ">(__keyOf_");
										p_Write(propertyInfo.Name);
										p_WriteLine(");");
										p_WriteLine("__keyOf_", propertyInfo.Name, " = null;");
									}
								}
							
								p_Write("return _");
								p_Write(propertyInfo.Name);
								p_WriteLine(";");
							}

							p_WriteLine("set");
							using (p_CreateBlock())
							{
								p_WriteLine("switch(fRecordMode)");
								using(p_CreateBlock())
								{
									p_Write("case ");
									p_Write(typeof(RecordMode));
									p_Write(".");
									p_Write(RecordMode.Insert.ToString());
									p_WriteLine(":");
									p_Write("case ");
									p_Write(typeof(RecordMode));
									p_Write(".");
									p_Write(RecordMode.Update.ToString());
									p_WriteLine(":");

									if (typeof(IRecord).IsAssignableFrom(propertyType))
									{
										p_WriteLine("if (value != null)");
										using(p_CreateBlock())
										{
											p_WriteLine("if (value.GetRecordMode() != ", typeof(RecordMode),".ReadOnly)");
											p_WriteLine("\tthrow new ", typeof(DatabaseException), "(\"You can only point to already persisted (read-only) records.\");");
										}
									}


									p_Write("  _");
									p_Write(propertyInfo.Name);
									p_WriteLine(" = value;");
									
									if (typeof(IRecord).IsAssignableFrom(propertyType))
										p_WriteLine("  __keyOf_", propertyInfo.Name, " = null;");
									
									p_WriteLine("  break;");
									
									p_WriteLine("default: throw new global::System.Data.ReadOnlyException();");
								}
							}
						}
					}
				}
			}
		}

		private bool p_WriteNonPersistedReferenceProperty(PropertyInfo property)
		{
			var propertyType = property.PropertyType;
			if (!typeof(IRecord).IsAssignableFrom(propertyType))
				return false;
			
			if (propertyType.ContainsCustomAttribute<DatabasePersistedAttribute>())
				return false;
			
			p_Write("typeof(", property.DeclaringType, ").GetProperty(\"", property.Name, "\"), ");
			return true;
		}

		private void p_WriteGetHashCode(PropertyInfoPaths primaryKeyPaths)
		{
			p_WriteLine("public override int GetHashCode()");
			using(p_CreateBlock())
			{
				p_WriteLine("unchecked");
				using(p_CreateBlock())
				{
					p_WriteLine("int __result = 0;");
					PropertyInfo lastProperty = null;
					foreach(var primaryKeyPath in primaryKeyPaths)
					{
						PropertyInfo property = primaryKeyPath[0];
						if (property == lastProperty)
							continue;
						lastProperty = property;
						
						p_WriteLine("if (_", property.Name, " != null) __result ^= _", property.Name, ".GetHashCode();");
					}
					
					p_WriteLine("return __result;");
				}
			}
		}
		private void p_WriteEquals(Type type, PropertyInfoPaths propertyPaths)
		{
			p_WriteLine("public override bool Equals(object __obj)");
			using(p_CreateBlock())
			{
				p_WriteLine(type.Name, " __other = __obj as ", type.Name, ";");
				p_WriteLine("if (__other == null) return false;");
				
				PropertyInfo lastProperty = null;
				foreach(var propertyPath in propertyPaths)
				{
					PropertyInfo property = propertyPath[0];
					if (property == lastProperty)
						continue;
					lastProperty = property;

					Type propertyType = property.PropertyType;
					if (propertyType.IsPrimitive || propertyType.IsEnum || propertyType == typeof(string))
						p_WriteLine("if(_", property.Name, " != __other._", property.Name, ") return false;");
					else
					if (propertyType.IsValueType)
						p_WriteLine("if(!_", property.Name, ".Equals(__other._", property.Name, ")) return false;");
					else
						p_WriteLine("if (!object.Equals(_", property.Name, ", __other._", property.Name, ")) return false;");
				}
				
				p_WriteLine("return true;");
			}
		}

		private void p_WriteInsertSql(string databaseName, int count, string fieldsSql)
		{
			p_Write("public const string InsertSql = \"INSERT INTO ", databaseName, "(", fieldsSql, ") VALUES (");
			
			for (int i=0; i<count; i++)
				p_Write("@P", i, ", ");
				
			fStringBuilder.Length -= 2;
			
			p_WriteLine(")\";");
		}

		private void p_CreateCloneConstructor(Type type, Dictionary<PropertyInfo, List<PropertyInfoPath>> properties)
		{
			p_Write("public ");
			p_Write(type.Name);
			p_Write("(global::");
			p_Write(typeof(RecordMode).FullName);
			p_Write(" recordMode, ");
			p_Write(type.Name);
			p_WriteLine(" source)");
			using (p_CreateBlock())
			{
				p_WriteLine("if (source == null) throw new global::System.ArgumentNullException(\"source\");");
				p_WriteLine("fRecordMode = recordMode;");
				p_WriteLine("CopyValuesFrom(source);");
			}
			
			p_WriteLine("public void CopyValuesFrom(", type.Name, " source)");
			using (p_CreateBlock())
			{
				foreach (var property in properties.Keys)
				{
					if (typeof(ICloneable).IsAssignableFrom(property.PropertyType))
						p_WriteLine("if (source._", property.Name, " != null) _", property.Name, " = (", property.PropertyType, ")(((", typeof(ICloneable), ")source._", property.Name, ").Clone()); else _", property.Name, " = default(", property.PropertyType, ");");
					else
					{
						p_Write("_");
						p_Write(property.Name);
						p_Write(" = source._");
						p_Write(property.Name);
						p_WriteLine(";");
					}
					
					if (typeof(IRecord).IsAssignableFrom(property.PropertyType))
						p_WriteLine("__keyOf_", property.Name, " = source.__keyOf_", property.Name, ";");
				}
			}
		}

		private void p_CreateFromDatabaseConstructor(Type type, PropertyInfoPaths properties, Dictionary<PropertyInfo, List<PropertyInfoPath>> dictionary)
		{
			p_WriteLine("public ", type.Name, "(", typeof(IDatabaseConnection), " connection, object[] itemArray)");
			using (p_CreateBlock())
			{
				p_WriteLine(typeof(LocalDatabaseManager), " local__databaseManager = ", typeof(LocalDatabaseManager), ".Value;");
				
				p_WriteLine("fRecordMode = ", typeof(RecordMode), ".ReadOnly;");

				int itemIndex = -1;
				foreach(var pair in dictionary)
				{
					var firstProperty = pair.Key;
					var paths = pair.Value;
					
					if (paths[0].Length > 1)
					{
						if (firstProperty.ContainsCustomAttribute<DatabaseNullableAttribute>())
						{
							p_Write("if(");

							int count = paths.Count;
							for (int i = 1; i <= count; i++)
							{
								if (i > 1)
									p_Write(" || ");

								p_Write("itemArray[");
								p_Write((i + itemIndex).ToString());
								p_Write("] != global::System.DBNull.Value");
							}

							p_Write(") ");
						}
						
						p_Write("__keyOf_");
						p_Write(firstProperty.Name);
						p_Write(" = new object[]{");
						
						int pathIndex = -1;
						foreach(var path in paths)
						{
							pathIndex++;
							itemIndex++;
							
							var lastProperty = path[path.Length-1];
							
							p_Write
							(
								"local__databaseManager.ChangeTypeFromDatabase<",
								lastProperty.PropertyType,
								">(connection, global::Pfz.Databasing.ImplementedRecords.",
								firstProperty.DeclaringType.FullName,
								".PropertyPathOf_",
								firstProperty.Name,
								"_",
								pathIndex,
								", itemArray[",
								itemIndex,
								"]), "
							);
						}
						fStringBuilder.Length -= 2;
						
						p_WriteLine("};");
						
					}
					else
					{
				        itemIndex++;

				        p_WriteLine
				        (
				            "_",
				            firstProperty.Name,
				            " = local__databaseManager.ChangeTypeFromDatabase<",
				            firstProperty.PropertyType,
				            ">(connection, global::Pfz.Databasing.ImplementedRecords.",
				            firstProperty.DeclaringType.FullName,
				            ".PropertyOf_",
				            firstProperty.Name,
				            ", itemArray[",
				            itemIndex,
				            "]);"
				        );
					}
				}
			}
		}

		private int p_CreateFromDatabaseConstructor3(int itemIndex, PropertyInfo propertyInfo, Type propertyType, PropertyInfoPaths keyProperties)
		{
			foreach (var keyPropertyPath in keyProperties)
			{
				var keyProperty = keyPropertyPath[keyPropertyPath.Length-1];
				
				if (typeof(IRecord).IsAssignableFrom(keyProperty.PropertyType))
				{
					p_Write("new object[]{");

					var otherKeys = new PropertyInfoPaths(keyProperty.PropertyType, true);
					itemIndex = p_CreateFromDatabaseConstructor3(itemIndex, propertyInfo, propertyType, otherKeys);
					
					p_Write("}, ");
				}
				else
				{
					itemIndex++;

					p_Write
					(
						"local__databaseManager.ChangeTypeFromDatabase<",
						keyProperty.PropertyType,
						">(global::Pfz.Databasing.ImplementedRecords.",
						p_GetName(propertyInfo.DeclaringType),
						".PropertyOf_",
						propertyInfo.Name,
						", global::Pfz.Databasing.ImplementedRecords.",
						p_GetName(keyProperty.DeclaringType),
						".PropertyOf_",
						keyProperty.Name,
						", itemArray[",
						itemIndex,
						"]), "
					);
				}
			}
			
			fStringBuilder.Length -= 2;

			return itemIndex;
		}

		private void p_CreateBasicConstructor(Type type)
		{
			p_Write("public ");
			p_Write(type.Name);
			p_WriteLine("(){}");
		}

		private void p_WritePropertiesCollection(Type type, List<PropertyInfoPath> properties, string fieldName)
		{
			p_Write("public static readonly global::System.Collections.ObjectModel.ReadOnlyCollection<", typeof(PropertyInfoPath), "> ");
			p_Write(fieldName);
			p_Write(" = new global::System.Collections.ObjectModel.ReadOnlyCollection<", typeof(PropertyInfoPath), ">(new ", typeof(PropertyInfoPath), "[]{");
			foreach (var propertyPath in properties)
			{
				p_Write("new ", typeof(PropertyInfoPath), "(new global::System.Reflection.PropertyInfo[]{");
				
				foreach(var property in propertyPath)
				{
					p_Write("typeof(global::");
					p_Write(p_GetName(property.DeclaringType));
					p_Write(").GetProperty(\"");
					p_Write(property.Name);
					p_Write("\"), ");
				}
				fStringBuilder.Length -= 2;
				
				p_Write("}), ");
			}
			fStringBuilder.Length -= 2;
			p_WriteLine("});");
		}

		private string p_GetWhereSql(Type type, PropertyInfoPaths primaryKeyProperties, string prefix)
		{
			int itemIndex = -1;
			StringBuilder sql = new StringBuilder();
			foreach (var propertyPath in primaryKeyProperties)
			{
				itemIndex++;
				string databaseName = propertyPath.GetDatabaseName();
				
				sql.Append(prefix);
				sql.Append(databaseName);
				sql.Append("=@PW");
				sql.Append(itemIndex);
				sql.Append(" AND ");
			}
			
			if (itemIndex == -1)
				throw new ArgumentException("Persisted type " + type.FullName + " does not have a primary key.");
				
			sql.Length -= 5;
			return sql.ToString();
		}

		private string p_GetFieldsSql(PropertyInfoPaths properties)
		{
			StringBuilder fieldsSql = new StringBuilder();
			foreach (var propertyPath in properties)
			{
				string databaseName = propertyPath.GetDatabaseName();
				fieldsSql.Append("MT.");
				fieldsSql.Append(databaseName);
				fieldsSql.Append(", ");
			}
			fieldsSql.Length -= 2;
			return fieldsSql.ToString();
		}

		private void p_WriteReturnType(Type type)
		{
			if (type == typeof(void))
				p_Write("void");
			else
			{
				p_Write("global::");
				p_Write(p_GetName(type));
			}
		}
		
		private p_CreateBlockStruct p_CreateBlock()
		{
			return new p_CreateBlockStruct(this);
		}
		private struct p_CreateBlockStruct:
			IDisposable
		{
			private CSharpGenerator fGenerator;
			internal p_CreateBlockStruct(CSharpGenerator generator)
			{
				fGenerator = generator;
				
				generator.p_WriteLine("{");
				generator.fLevel ++;
			}
			public void Dispose()
			{
				fGenerator.fLevel --;
				fGenerator.p_WriteLine("}");
			}
		}
		
		public string Result
		{
			get
			{
				return fStringBuilder.ToString();
			}
		}
		
		private string p_GetName(Type type)
		{
			StringBuilder result = new StringBuilder();

			p_GetName2(type, result);
			
			return result.ToString();
		}

		private static void p_GetName2(Type type, StringBuilder result)
		{
			result.Append(type.Namespace);
			result.Append('.');

			if (!type.IsGenericType)
				result.Append(type.Name);
			else
			{
				result.Append(type.Name.Split('`')[0]);
				result.Append('<');

				foreach (Type genericType in type.GetGenericArguments())
				{
					p_GetName2(genericType, result);
					result.Append(", ");
				}
				result.Length -= 2;

				result.Append('>');
			}
		}
	}
}





By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.

At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do their work easier, faster and with less errors.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions