using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Dynamic; using System.Text.RegularExpressions; using System.Globalization; using System.Collections.Specialized; using System.Data; using System.Configuration; using System.Data.Common; namespace Massive{ public static class ObjectExtensions{ /// <summary> /// Extension method for adding in a bunch of parameters /// </summary> public static void AddParams(this DbCommand cmd, object[] args) { foreach (var item in args) { AddParam(cmd,item); } } /// <summary> /// Extension for adding single parameter /// </summary> public static void AddParam(this DbCommand cmd, object item) { var p = cmd.CreateParameter(); p.ParameterName = string.Format("@{0}", cmd.Parameters.Count); //fix for NULLs as parameter values if (item == null) { p.Value = DBNull.Value; } else { //fix for Guids if (item.GetType() == typeof(Guid)) { p.Value = item.ToString(); p.DbType = DbType.String; } else { p.Value = item; } } cmd.Parameters.Add(p); } /// <summary> /// Turns an IDataReader to a Dynamic list of things /// </summary> public static List<dynamic> ToExpandoList(this IDataReader rdr) { var result = new List<dynamic>(); //work with the Expando as a Dictionary while (rdr.Read()) { dynamic e = new ExpandoObject(); var d = e as IDictionary<string, object>; for (int i = 0; i < rdr.FieldCount; i++) { d.Add(rdr.GetName(i), rdr[i]); } result.Add(e); } return result; } /// <summary> /// Turns the object into an ExpandoObject /// </summary> /// <param name="o"></param> /// <returns></returns> public static dynamic ToExpando(this object o) { var result = new ExpandoObject(); //work with the Expando as a Dictionary var d = result as IDictionary<string, object>; //shouldn't have to... but just in case if (o.GetType() == typeof(ExpandoObject)) { return o; } //special for form submissions if (o.GetType() == typeof(NameValueCollection)) { var nv = (NameValueCollection)o; //there's a better way to do this... I just don't know what it is... foreach (var item in nv.Keys) { var key = item.ToString(); d.Add(key, nv.Get(key)); } } else { //assume it's a regular lovely object var props = o.GetType().GetProperties(); foreach (var item in props) { var val = item.GetValue(o, null); d.Add(item.Name, val); } } return result; } /// <summary> /// Turns the object into a Dictionary /// </summary> /// <param name="thingy"></param> /// <returns></returns> public static IDictionary<string, object> ToDictionary(this object thingy) { var expando = thingy.ToExpando(); return (IDictionary<string, object>)expando; } } /// <summary> /// A class that wraps your database table in Dynamic Funtime /// </summary> public abstract class DynamicModel:DynamicObject { DbProviderFactory _factory; string _connectionStringName; string _connectionString; public IList<dynamic> Query(string sql, params object[] args) { var result = new List<dynamic>(); var cmd = CreateCommand(sql, args); using (var conn = OpenConnection()) { cmd.Connection= conn; var rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection); result = rdr.ToExpandoList(); //can't help it - like being explicit rdr.Dispose(); cmd.Dispose(); } return result; } /// <summary> /// Creates a DBCommand that you can use for loving your database. /// </summary> DbCommand CreateCommand(string sql, params object[] args) { DbCommand result = null; result = _factory.CreateCommand(); result.CommandText = sql; if(args.Length > 0) result.AddParams(args); return result; } DbConnection GetConnection() { var connection = _factory.CreateConnection(); connection.ConnectionString = _connectionString; return connection; } DbConnection OpenConnection() { var conn = GetConnection(); conn.Open(); return conn; } /// <summary> /// Creates a slick, groovy little wrapper for your action /// </summary> /// <param name="connectionStringName"></param> public DynamicModel(string connectionStringName) { //can be overridden by property setting TableName = this.GetType().Name; _connectionStringName = connectionStringName; var providerName = "System.Data.SqlClient"; if (ConfigurationManager.ConnectionStrings[_connectionStringName] != null) { providerName = ConfigurationManager.ConnectionStrings[_connectionStringName].ProviderName ?? "System.Data.SqlClient"; } else { throw new InvalidOperationException("Can't find a connection string with the name '" + _connectionStringName + "'"); } _factory = DbProviderFactories.GetFactory(providerName); _connectionString = ConfigurationManager.ConnectionStrings[_connectionStringName].ConnectionString; } string _primaryKeyField; /// <summary> /// Conventionally returns a PK field. The default is "ID" if you don't set one /// </summary> public string PrimaryKeyField { get { //a bit of convention here if (!string.IsNullOrEmpty(_primaryKeyField)) return _primaryKeyField; //oh well - did our best return "ID"; } set { _primaryKeyField = value; } } /// <summary> /// Conventionally introspects the object passed in for a field that /// looks like a PK. If you've named your PrimaryKeyField, this becomes easy /// </summary> public bool HasPrimaryKey(object o) { var result = o.ToDictionary().ContainsKey(PrimaryKeyField); return result; } /// <summary> /// If the object passed in has a property with the same name as your PrimaryKeyField /// it is returned here. /// </summary> public object GetPrimaryKey(object o) { var d = o.ToDictionary(); object result = null; d.TryGetValue(PrimaryKeyField, out result); return result; } /// <summary> /// The name of the Database table we're working with. This defaults to /// the class name - set this value if it's different /// </summary> public string TableName { get; set; } /// <summary> /// Adds a record to the database. You can pass in an Anonymous object, an ExpandoObject, /// A regular old POCO, or a NameValueColletion from a Request.Form or Request.QueryString /// </summary> public dynamic Insert(object o) { dynamic result = 0; if (BeforeInsert(o)) { var cmd = CreateInsertCommand(o); using (var conn = OpenConnection()) { cmd.Connection = conn; result = cmd.ExecuteScalar(); cmd.Dispose(); AfterInsert(o); } } return result; } /// <summary> /// Creates a command for use with transactions - internal stuff mostly, but here for you to play with /// </summary> public DbCommand CreateInsertCommand(object o) { DbCommand result = null; //turn this into an expando - we'll need that for the validators var expando = o.ToExpando(); var settings = (IDictionary<string, object>)expando; var sbKeys = new StringBuilder(); var sbVals = new StringBuilder(); var stub = "INSERT INTO {0} ({1}) \r\n VALUES ({2}); \r\nSELECT SCOPE_IDENTITY()"; result = CreateCommand(stub); int counter = 0; foreach (var item in settings) { sbKeys.AppendFormat("{0},", item.Key); sbVals.AppendFormat("@{0},", counter.ToString()); result.AddParam(item.Value); counter++; } if (counter > 0) { //strip off trailing commas var keys = sbKeys.ToString().Substring(0, sbKeys.Length - 1); var vals = sbVals.ToString().Substring(0, sbVals.Length - 1); var sql = string.Format(stub, TableName, keys, vals); result.CommandText = sql; } else { throw new InvalidOperationException("Can't parse this object to the database - there are no properties set"); } return result; } /// <summary> /// Creates a command for use with transactions - internal stuff mostly, but here for you to play with /// </summary> public DbCommand CreateUpdateCommand(object o, object key) { var expando = o.ToExpando(); var settings = (IDictionary<string, object>)expando; var sbKeys = new StringBuilder(); var stub = "UPDATE {0} SET {1} WHERE {2} = @{3}"; var args = new List<object>(); var result = CreateCommand(stub); int counter = 0; foreach (var item in settings) { var val = item.Value; if (!item.Key.Equals(PrimaryKeyField, StringComparison.CurrentCultureIgnoreCase) && item.Value != null) { result.AddParam(val); sbKeys.AppendFormat("{0} = @{1}, \r\n", item.Key, counter.ToString()); counter++; } } if (counter > 0) { //add the key result.AddParam(key); //strip the last commas var keys = sbKeys.ToString().Substring(0, sbKeys.Length - 4); var sql = string.Format(stub, TableName, keys, PrimaryKeyField, counter); result.CommandText = sql; } else { //throw throw new InvalidOperationException("No parsable object was sent in - could not divine any name/value pairs"); } return result; } /// <summary> /// Updates a record in the database. You can pass in an Anonymous object, an ExpandoObject, /// A regular old POCO, or a NameValueColletion from a Request.Form or Request.QueryString /// </summary> public int Update(object o, object key) { //turn this into an expando - we'll need that for the validators int result = 0; if (BeforeUpdate(o)) { using (var conn = OpenConnection()) { var cmd = CreateUpdateCommand(o, key); result = cmd.ExecuteNonQuery(); AfterUpdate(o); } } return result; } /// <summary> /// Updates a bunch of records in the database within a transaction. You can pass Anonymous objects, ExpandoObjects, /// Regular old POCOs - these all have to have a PK set /// </summary> public int InsertMany(IEnumerable<object> things) { int result = 0; using (var conn = OpenConnection()) { var tx = conn.BeginTransaction(); foreach (var item in things) { if (BeforeInsert(item)) { var cmd = CreateInsertCommand(item); cmd.Connection = conn; cmd.Transaction = tx; cmd.ExecuteNonQuery(); cmd.Dispose(); AfterInsert(item); } result++; } tx.Commit(); conn.Dispose(); tx.Dispose(); } return result; } /// <summary> /// Updates a bunch of records in the database within a transaction. You can pass Anonymous objects, ExpandoObjects, /// Regular old POCOs - these all have to have a PK set /// </summary> public int UpdateMany(IEnumerable<object> things) { //turn this into an expando - we'll need that for the validators int result = 0; using (var conn = OpenConnection()) { var tx = conn.BeginTransaction(); foreach (var item in things) { var pk = GetPrimaryKey(item); if (pk == null) throw new InvalidOperationException("Please be sure to set a value for the primary key"); if (BeforeUpdate(item)) { var cmd = CreateUpdateCommand(item, pk); cmd.Connection = conn; cmd.Transaction = tx; cmd.ExecuteNonQuery(); cmd.Dispose(); AfterUpdate(item); result++; } } tx.Commit(); conn.Dispose(); tx.Dispose(); } return result; } /// <summary> /// If you're feeling lazy, or are just unsure about whether to use Update or Insert you can use /// this method. It will look for a PrimaryKeyField with a set value to determine if this should /// be an Insert or Save. You can pass in an Anonymous object, an ExpandoObject, /// A regular old POCO, or a NameValueColletion from a Request.Form or Request.QueryString /// </summary> public dynamic Save(object o) { dynamic result = 0; if (BeforeSave(o)) { var expando = o.ToExpando(); //decide insert or update if (HasPrimaryKey(expando)) { result = Update(expando, GetPrimaryKey(o)); } else { result = Insert(expando); } AfterSave(o); } return result; } /// <summary> /// Removes a record from the database /// </summary> public int Delete(object key) { //execute var sql = string.Format("DELETE FROM {0} WHERE {1} = @0", TableName, PrimaryKeyField); var result = 0; var cmd = CreateCommand(sql, key); using (var conn = OpenConnection()) { cmd.Connection = conn; result = cmd.ExecuteNonQuery(); cmd.Dispose(); } return result; } /// <summary> /// Removes one or more records from the DB according to the passed-in WHERE /// </summary> public dynamic Delete(string where, params object[] args) { //execute var sql = string.Format("DELETE FROM {0} ", TableName); if (!where.Trim().StartsWith("where", StringComparison.CurrentCultureIgnoreCase)) { sql += "WHERE "; } sql += where; var result = 0; var cmd = CreateCommand(sql, args); using (var conn = OpenConnection()) { cmd.Connection = conn; result = cmd.ExecuteNonQuery(); cmd.Dispose(); } return result; } /// <summary> /// Returns all records complying with the passed-in WHERE clause and arguments, /// ordered as specified, limited (TOP) by limit. /// </summary> public IEnumerable<dynamic> All(string where="", string orderBy="", int limit=0, params object[] args) { string sql = "SELECT * FROM {0} "; if (limit > 0) { sql = "SELECT TOP "+limit+" * FROM {0} "; } if (!string.IsNullOrEmpty(where)) { if (where.Trim().StartsWith("where", StringComparison.CurrentCultureIgnoreCase)) { sql += where; } else { sql += "WHERE " + where; } } if (!String.IsNullOrEmpty(orderBy)) { if (!orderBy.Trim().StartsWith("order by", StringComparison.CurrentCultureIgnoreCase)) { sql += " ORDER BY "; } sql += orderBy; } return Query(string.Format(sql, TableName), args); } /// <summary> /// Returns a single row from the database /// </summary> /// <returns>ExpandoObject</returns> public dynamic Single(object key) { var sql = string.Format("SELECT * FROM {0} WHERE {1} = @0", TableName, PrimaryKeyField); var single = Query(sql,key).FirstOrDefault(); return single; } #region hooks //hooks for save routines public virtual bool BeforeInsert(object o) { return true; } public virtual bool BeforeUpdate(object o) { return true; } public virtual bool BeforeSave(object o) { return true; } public virtual bool BeforeDelete(object key) { return true; } public virtual void AfterInsert(object o) { } public virtual void AfterUpdate(object o) { } public virtual void AfterSave(object o) { } public virtual void AfterDelete(object key) { } #endregion } }