andrew4582

Massive DB

Jul 5th, 2012
236
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 40.73 KB | None | 0 0
  1. /*
  2.  Example:
  3.  var tztable = new DynamicModel("DBZOrders_PROD",
  4.         tableName :"aht_TimeZone",
  5.         primaryKeyField :"ID");
  6.            
  7.     var records = tztable
  8.         .All()
  9.         .OrderBy(t=>t.SortOrder);
  10.  
  11.     Console.WriteLine("Count -> {0:N0}",records.Count());
  12.     Console.WriteLine();
  13.     foreach(var item in records) {
  14.         string desc = item.Description;
  15.  
  16.             if(desc.Length > 20)
  17.             desc = desc.Substring(0,20);
  18.         Console.WriteLine("{0,-20}{1,5:N0}",desc,item.GMTDiff);
  19.     }
  20.  */
  21.  
  22. /*
  23.  * See docs at the end of file
  24.  */
  25. using System;
  26. using System.Collections.Generic;
  27. using System.Collections.Specialized;
  28. using System.Configuration;
  29. using System.Data;
  30. using System.Data.Common;
  31. using System.Dynamic;
  32. using System.Linq;
  33. using System.Text;
  34.  
  35. namespace Massive {
  36.     public static class ObjectExtensions {
  37.         /// <summary>
  38.         /// Extension method for adding in a bunch of parameters
  39.         /// </summary>
  40.         public static void AddParams(this DbCommand cmd, params object[] args) {
  41.             foreach (var item in args) {
  42.                 AddParam(cmd, item);
  43.             }
  44.         }
  45.         /// <summary>
  46.         /// Extension for adding single parameter
  47.         /// </summary>
  48.         public static void AddParam(this DbCommand cmd, object item) {
  49.             var p = cmd.CreateParameter();
  50.             p.ParameterName = string.Format("@{0}", cmd.Parameters.Count);
  51.             if (item == null) {
  52.                 p.Value = DBNull.Value;
  53.             } else {
  54.                 if (item.GetType() == typeof(Guid)) {
  55.                     p.Value = item.ToString();
  56.                     p.DbType = DbType.String;
  57.                     p.Size = 4000;
  58.                 } else if (item.GetType() == typeof(ExpandoObject)) {
  59.                     var d = (IDictionary<string, object>)item;
  60.                     p.Value = d.Values.FirstOrDefault();
  61.                 } else {
  62.                     p.Value = item;
  63.                 }
  64.                 if (item.GetType() == typeof(string))
  65.                     p.Size = ((string)item).Length > 4000 ? -1 : 4000;
  66.             }
  67.             cmd.Parameters.Add(p);
  68.         }
  69.         /// <summary>
  70.         /// Turns an IDataReader to a Dynamic list of things
  71.         /// </summary>
  72.         public static List<dynamic> ToExpandoList(this IDataReader rdr) {
  73.             var result = new List<dynamic>();
  74.             while (rdr.Read()) {
  75.                 result.Add(rdr.RecordToExpando());
  76.             }
  77.             return result;
  78.         }
  79.         public static dynamic RecordToExpando(this IDataReader rdr) {
  80.             dynamic e = new ExpandoObject();
  81.             var d = e as IDictionary<string, object>;
  82.             for (int i = 0; i < rdr.FieldCount; i++)
  83.                 d.Add(rdr.GetName(i), DBNull.Value.Equals(rdr[i]) ? null : rdr[i]);
  84.             return e;
  85.         }
  86.         /// <summary>
  87.         /// Turns the object into an ExpandoObject
  88.         /// </summary>
  89.         public static dynamic ToExpando(this object o) {
  90.             var result = new ExpandoObject();
  91.             var d = result as IDictionary<string, object>; //work with the Expando as a Dictionary
  92.             if (o.GetType() == typeof(ExpandoObject)) return o; //shouldn't have to... but just in case
  93.             if (o.GetType() == typeof(NameValueCollection) || o.GetType().IsSubclassOf(typeof(NameValueCollection))) {
  94.                 var nv = (NameValueCollection)o;
  95.                 nv.Cast<string>().Select(key => new KeyValuePair<string, object>(key, nv[key])).ToList().ForEach(i => d.Add(i));
  96.             } else {
  97.                 var props = o.GetType().GetProperties();
  98.                 foreach (var item in props) {
  99.                     d.Add(item.Name, item.GetValue(o, null));
  100.                 }
  101.             }
  102.             return result;
  103.         }
  104.  
  105.         /// <summary>
  106.         /// Turns the object into a Dictionary
  107.         /// </summary>
  108.         public static IDictionary<string, object> ToDictionary(this object thingy) {
  109.             return (IDictionary<string, object>)thingy.ToExpando();
  110.         }
  111.     }
  112.  
  113.     /// <summary>
  114.     /// Convenience class for opening/executing data
  115.     /// </summary>
  116.     public static class DB {
  117.         public static DynamicModel Current {
  118.             get {
  119.                 if (ConfigurationManager.ConnectionStrings.Count > 1) {
  120.                     return new DynamicModel(ConfigurationManager.ConnectionStrings[1].Name);
  121.                 }
  122.                 throw new InvalidOperationException("Need a connection string name - can't determine what it is");
  123.             }
  124.         }
  125.     }
  126.    
  127.     /// <summary>
  128.     /// A class that wraps your database table in Dynamic Funtime
  129.     /// </summary>
  130.     public class DynamicModel : DynamicObject {
  131.         DbProviderFactory _factory;
  132.         string ConnectionString;
  133.         public static DynamicModel Open(string connectionStringName) {
  134.             dynamic dm = new DynamicModel(connectionStringName);
  135.             return dm;
  136.         }
  137.         public DynamicModel(string connectionStringName, string tableName = "",
  138.             string primaryKeyField = "", string descriptorField = "") {
  139.             TableName = tableName == "" ? this.GetType().Name : tableName;
  140.             PrimaryKeyField = string.IsNullOrEmpty(primaryKeyField) ? "ID" : primaryKeyField;
  141.             DescriptorField = descriptorField;
  142.             var _providerName = "System.Data.SqlClient";
  143.            
  144.             if(ConfigurationManager.ConnectionStrings[connectionStringName].ProviderName != null)
  145.                 _providerName = ConfigurationManager.ConnectionStrings[connectionStringName].ProviderName;
  146.            
  147.             _factory = DbProviderFactories.GetFactory(_providerName);
  148.             ConnectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
  149.         }
  150.  
  151.         /// <summary>
  152.         /// Creates a new Expando from a Form POST - white listed against the columns in the DB
  153.         /// </summary>
  154.         public dynamic CreateFrom(NameValueCollection coll) {
  155.             dynamic result = new ExpandoObject();
  156.             var dc = (IDictionary<string, object>)result;
  157.             var schema = Schema;
  158.             //loop the collection, setting only what's in the Schema
  159.             foreach (var item in coll.Keys) {
  160.                 var exists = schema.Any(x => x.COLUMN_NAME.ToLower() == item.ToString().ToLower());
  161.                 if (exists) {
  162.                     var key = item.ToString();
  163.                     var val = coll[key];
  164.                     dc.Add(key, val);
  165.                 }
  166.             }
  167.             return result;
  168.         }
  169.         /// <summary>
  170.         /// Gets a default value for the column
  171.         /// </summary>
  172.         public dynamic DefaultValue(dynamic column) {
  173.             dynamic result = null;
  174.             string def = column.COLUMN_DEFAULT;
  175.             if (String.IsNullOrEmpty(def)) {
  176.                 result = null;
  177.             } else if (def == "getdate()" || def == "(getdate())") {
  178.                 result = DateTime.Now.ToShortDateString();
  179.             } else if (def == "newid()") {
  180.                 result = Guid.NewGuid().ToString();
  181.             } else {
  182.                 result = def.Replace("(", "").Replace(")", "");
  183.             }
  184.             return result;
  185.         }
  186.         /// <summary>
  187.         /// Creates an empty Expando set with defaults from the DB
  188.         /// </summary>
  189.         public dynamic Prototype {
  190.             get {
  191.                 dynamic result = new ExpandoObject();
  192.                 var schema = Schema;
  193.                 foreach (dynamic column in schema) {
  194.                     var dc = (IDictionary<string, object>)result;
  195.                     dc.Add(column.COLUMN_NAME, DefaultValue(column));
  196.                 }
  197.                 result._Table = this;
  198.                 return result;
  199.             }
  200.         }
  201.         public string DescriptorField { get; protected set; }
  202.         /// <summary>
  203.         /// List out all the schema bits for use with ... whatever
  204.         /// </summary>
  205.         IEnumerable<dynamic> _schema;
  206.         public IEnumerable<dynamic> Schema {
  207.             get {
  208.                 if (_schema == null)
  209.                     _schema = Query("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @0", TableName);
  210.                 return _schema;
  211.             }
  212.         }
  213.  
  214.         /// <summary>
  215.         /// Enumerates the reader yielding the result - thanks to Jeroen Haegebaert
  216.         /// </summary>
  217.         public virtual IEnumerable<dynamic> Query(string sql, params object[] args) {
  218.             using (var conn = OpenConnection()) {
  219.                 var rdr = CreateCommand(sql, conn, args).ExecuteReader();
  220.                 while (rdr.Read()) {
  221.                     yield return rdr.RecordToExpando(); ;
  222.                 }
  223.             }
  224.         }
  225.         public virtual IEnumerable<dynamic> Query(string sql, DbConnection connection, params object[] args) {
  226.             using (var rdr = CreateCommand(sql, connection, args).ExecuteReader()) {
  227.                 while (rdr.Read()) {
  228.                     yield return rdr.RecordToExpando(); ;
  229.                 }
  230.             }
  231.         }
  232.         /// <summary>
  233.         /// Returns a single result
  234.         /// </summary>
  235.         public virtual object Scalar(string sql, params object[] args) {
  236.             object result = null;
  237.             using (var conn = OpenConnection()) {
  238.                 result = CreateCommand(sql, conn, args).ExecuteScalar();
  239.             }
  240.             return result;
  241.         }
  242.         /// <summary>
  243.         /// Creates a DBCommand that you can use for loving your database.
  244.         /// </summary>
  245.         DbCommand CreateCommand(string sql, DbConnection conn, params object[] args) {
  246.             var result = _factory.CreateCommand();
  247.             result.Connection = conn;
  248.             result.CommandText = sql;
  249.             if (args.Length > 0)
  250.                 result.AddParams(args);
  251.             return result;
  252.         }
  253.         /// <summary>
  254.         /// Returns and OpenConnection
  255.         /// </summary>
  256.         public virtual DbConnection OpenConnection() {
  257.             var result = _factory.CreateConnection();
  258.             result.ConnectionString = ConnectionString;
  259.             result.Open();
  260.             return result;
  261.         }
  262.         /// <summary>
  263.         /// Builds a set of Insert and Update commands based on the passed-on objects.
  264.         /// These objects can be POCOs, Anonymous, NameValueCollections, or Expandos. Objects
  265.         /// With a PK property (whatever PrimaryKeyField is set to) will be created at UPDATEs
  266.         /// </summary>
  267.         public virtual List<DbCommand> BuildCommands(params object[] things) {
  268.             var commands = new List<DbCommand>();
  269.             foreach (var item in things) {
  270.                 if (HasPrimaryKey(item)) {
  271.                     commands.Add(CreateUpdateCommand(item, GetPrimaryKey(item)));
  272.                 } else {
  273.                     commands.Add(CreateInsertCommand(item));
  274.                 }
  275.             }
  276.             return commands;
  277.         }
  278.  
  279.  
  280.         public virtual int Execute(DbCommand command) {
  281.             return Execute(new DbCommand[] { command });
  282.         }
  283.  
  284.         public virtual int Execute(string sql, params object[] args) {
  285.             return Execute(CreateCommand(sql, null, args));
  286.         }
  287.         /// <summary>
  288.         /// Executes a series of DBCommands in a transaction
  289.         /// </summary>
  290.         public virtual int Execute(IEnumerable<DbCommand> commands) {
  291.             var result = 0;
  292.             using (var conn = OpenConnection()) {
  293.                 using (var tx = conn.BeginTransaction()) {
  294.                     foreach (var cmd in commands) {
  295.                         cmd.Connection = conn;
  296.                         cmd.Transaction = tx;
  297.                         result += cmd.ExecuteNonQuery();
  298.                     }
  299.                     tx.Commit();
  300.                 }
  301.             }
  302.             return result;
  303.         }
  304.         public virtual string PrimaryKeyField { get; set; }
  305.         /// <summary>
  306.         /// Conventionally introspects the object passed in for a field that
  307.         /// looks like a PK. If you've named your PrimaryKeyField, this becomes easy
  308.         /// </summary>
  309.         public virtual bool HasPrimaryKey(object o) {
  310.             return o.ToDictionary().ContainsKey(PrimaryKeyField);
  311.         }
  312.         /// <summary>
  313.         /// If the object passed in has a property with the same name as your PrimaryKeyField
  314.         /// it is returned here.
  315.         /// </summary>
  316.         public virtual object GetPrimaryKey(object o) {
  317.             object result = null;
  318.             o.ToDictionary().TryGetValue(PrimaryKeyField, out result);
  319.             return result;
  320.         }
  321.         public virtual string TableName { get; set; }
  322.         /// <summary>
  323.         /// Returns all records complying with the passed-in WHERE clause and arguments,
  324.         /// ordered as specified, limited (TOP) by limit.
  325.         /// </summary>
  326.         public virtual IEnumerable<dynamic> All(string where = "", string orderBy = "", int limit = 0, string columns = "*", params object[] args) {
  327.             string sql = BuildSelect(where, orderBy, limit);
  328.             return Query(string.Format(sql, columns, TableName), args);
  329.         }
  330.         private static string BuildSelect(string where, string orderBy, int limit) {
  331.             string sql = limit > 0 ? "SELECT TOP " + limit + " {0} FROM {1} " : "SELECT {0} FROM {1} ";
  332.             if (!string.IsNullOrEmpty(where))
  333.                 sql += where.Trim().StartsWith("where", StringComparison.OrdinalIgnoreCase) ? where : " WHERE " + where;
  334.             if (!String.IsNullOrEmpty(orderBy))
  335.                 sql += orderBy.Trim().StartsWith("order by", StringComparison.OrdinalIgnoreCase) ? orderBy : " ORDER BY " + orderBy;
  336.             return sql;
  337.         }
  338.  
  339.         /// <summary>
  340.         /// Returns a dynamic PagedResult. Result properties are Items, TotalPages, and TotalRecords.
  341.         /// </summary>
  342.         public virtual dynamic Paged(string where = "", string orderBy = "", string columns = "*", int pageSize = 20, int currentPage = 1, params object[] args)
  343.         {
  344.             return BuildPagedResult(where: where, orderBy: orderBy, columns: columns, pageSize: pageSize, currentPage: currentPage, args: args);
  345.         }
  346.  
  347.         public virtual dynamic Paged(string sql, string primaryKey, string where = "", string orderBy = "", string columns = "*", int pageSize = 20, int currentPage = 1, params object[] args)
  348.         {
  349.             return BuildPagedResult(sql, primaryKey, where, orderBy, columns, pageSize, currentPage, args);
  350.         }
  351.  
  352.         private dynamic BuildPagedResult(string sql = "", string primaryKeyField = "", string where = "", string orderBy = "", string columns = "*", int pageSize = 20, int currentPage = 1, params object[] args)
  353.         {
  354.             dynamic result = new ExpandoObject();
  355.             var countSQL = "";
  356.             if (!string.IsNullOrEmpty(sql))
  357.                 countSQL = string.Format("SELECT COUNT({0}) FROM ({1}) AS PagedTable", primaryKeyField, sql);
  358.             else
  359.                 countSQL = string.Format("SELECT COUNT({0}) FROM {1}", PrimaryKeyField, TableName);
  360.  
  361.             if (String.IsNullOrEmpty(orderBy))
  362.             {
  363.                 orderBy = string.IsNullOrEmpty(primaryKeyField) ? PrimaryKeyField : primaryKeyField;
  364.             }
  365.  
  366.             if (!string.IsNullOrEmpty(where))
  367.             {
  368.                 if (!where.Trim().StartsWith("where", StringComparison.CurrentCultureIgnoreCase))
  369.                 {
  370.                     where = " WHERE " + where;
  371.                 }
  372.             }
  373.  
  374.             var query = "";
  375.             if (!string.IsNullOrEmpty(sql))
  376.                 query = string.Format("SELECT {0} FROM (SELECT ROW_NUMBER() OVER (ORDER BY {2}) AS Row, {0} FROM ({3}) AS PagedTable {4}) AS Paged ", columns, pageSize, orderBy, sql, where);
  377.             else
  378.                 query = string.Format("SELECT {0} FROM (SELECT ROW_NUMBER() OVER (ORDER BY {2}) AS Row, {0} FROM {3} {4}) AS Paged ", columns, pageSize, orderBy, TableName, where);
  379.  
  380.             var pageStart = (currentPage - 1) * pageSize;
  381.             query += string.Format(" WHERE Row > {0} AND Row <={1}", pageStart, (pageStart + pageSize));
  382.             countSQL += where;
  383.             result.TotalRecords = Scalar(countSQL, args);
  384.             result.TotalPages = result.TotalRecords / pageSize;
  385.             if (result.TotalRecords % pageSize > 0)
  386.                 result.TotalPages += 1;
  387.             result.Items = Query(string.Format(query, columns, TableName), args);
  388.             return result;
  389.         }
  390.         /// <summary>
  391.         /// Returns a single row from the database
  392.         /// </summary>
  393.         public virtual dynamic Single(string where, params object[] args) {
  394.             var sql = string.Format("SELECT * FROM {0} WHERE {1}", TableName, where);
  395.             return Query(sql, args).FirstOrDefault();
  396.         }
  397.         /// <summary>
  398.         /// Returns a single row from the database
  399.         /// </summary>
  400.         public virtual dynamic Single(object key, string columns = "*") {
  401.             var sql = string.Format("SELECT {0} FROM {1} WHERE {2} = @0", columns, TableName, PrimaryKeyField);
  402.             return Query(sql, key).FirstOrDefault();
  403.         }
  404.         /// <summary>
  405.         /// This will return a string/object dictionary for dropdowns etc
  406.         /// </summary>
  407.         public virtual IDictionary<string, object> KeyValues(string orderBy = "") {
  408.             if (String.IsNullOrEmpty(DescriptorField))
  409.                 throw new InvalidOperationException("There's no DescriptorField set - do this in your constructor to describe the text value you want to see");
  410.             var sql = string.Format("SELECT {0},{1} FROM {2} ", PrimaryKeyField, DescriptorField, TableName);
  411.             if (!String.IsNullOrEmpty(orderBy))
  412.                 sql += "ORDER BY " + orderBy;
  413.  
  414.             var results = Query(sql).ToList().Cast<IDictionary<string, object>>();
  415.             return results.ToDictionary(key => key[PrimaryKeyField].ToString(), value => value[DescriptorField]);
  416.         }
  417.  
  418.         /// <summary>
  419.         /// This will return an Expando as a Dictionary
  420.         /// </summary>
  421.         public virtual IDictionary<string, object> ItemAsDictionary(ExpandoObject item) {
  422.             return (IDictionary<string, object>)item;
  423.         }
  424.         //Checks to see if a key is present based on the passed-in value
  425.         public virtual bool ItemContainsKey(string key, ExpandoObject item) {
  426.             var dc = ItemAsDictionary(item);
  427.             return dc.ContainsKey(key);
  428.         }
  429.         /// <summary>
  430.         /// Executes a set of objects as Insert or Update commands based on their property settings, within a transaction.
  431.         /// These objects can be POCOs, Anonymous, NameValueCollections, or Expandos. Objects
  432.         /// With a PK property (whatever PrimaryKeyField is set to) will be created at UPDATEs
  433.         /// </summary>
  434.         public virtual int Save(params object[] things) {
  435.             foreach (var item in things) {
  436.                 if (!IsValid(item)) {
  437.                     throw new InvalidOperationException("Can't save this item: " + String.Join("; ", Errors.ToArray()));
  438.                 }
  439.             }
  440.             var commands = BuildCommands(things);
  441.             return Execute(commands);
  442.         }
  443.         public virtual DbCommand CreateInsertCommand(dynamic expando) {
  444.             DbCommand result = null;
  445.             var settings = (IDictionary<string, object>)expando;
  446.             var sbKeys = new StringBuilder();
  447.             var sbVals = new StringBuilder();
  448.             var stub = "INSERT INTO {0} ({1}) \r\n VALUES ({2})";
  449.             result = CreateCommand(stub, null);
  450.             int counter = 0;
  451.             foreach (var item in settings) {
  452.                 sbKeys.AppendFormat("{0},", item.Key);
  453.                 sbVals.AppendFormat("@{0},", counter.ToString());
  454.                 result.AddParam(item.Value);
  455.                 counter++;
  456.             }
  457.             if (counter > 0) {
  458.                 var keys = sbKeys.ToString().Substring(0, sbKeys.Length - 1);
  459.                 var vals = sbVals.ToString().Substring(0, sbVals.Length - 1);
  460.                 var sql = string.Format(stub, TableName, keys, vals);
  461.                 result.CommandText = sql;
  462.             } else throw new InvalidOperationException("Can't parse this object to the database - there are no properties set");
  463.             return result;
  464.         }
  465.         /// <summary>
  466.         /// Creates a command for use with transactions - internal stuff mostly, but here for you to play with
  467.         /// </summary>
  468.         public virtual DbCommand CreateUpdateCommand(dynamic expando, object key) {
  469.             var settings = (IDictionary<string, object>)expando;
  470.             var sbKeys = new StringBuilder();
  471.             var stub = "UPDATE {0} SET {1} WHERE {2} = @{3}";
  472.             var args = new List<object>();
  473.             var result = CreateCommand(stub, null);
  474.             int counter = 0;
  475.             foreach (var item in settings) {
  476.                 var val = item.Value;
  477.                 if (!item.Key.Equals(PrimaryKeyField, StringComparison.OrdinalIgnoreCase) && item.Value != null) {
  478.                     result.AddParam(val);
  479.                     sbKeys.AppendFormat("{0} = @{1}, \r\n", item.Key, counter.ToString());
  480.                     counter++;
  481.                 }
  482.             }
  483.             if (counter > 0) {
  484.                 //add the key
  485.                 result.AddParam(key);
  486.                 //strip the last commas
  487.                 var keys = sbKeys.ToString().Substring(0, sbKeys.Length - 4);
  488.                 result.CommandText = string.Format(stub, TableName, keys, PrimaryKeyField, counter);
  489.             } else throw new InvalidOperationException("No parsable object was sent in - could not divine any name/value pairs");
  490.             return result;
  491.         }
  492.         /// <summary>
  493.         /// Removes one or more records from the DB according to the passed-in WHERE
  494.         /// </summary>
  495.         public virtual DbCommand CreateDeleteCommand(string where = "", object key = null, params object[] args) {
  496.             var sql = string.Format("DELETE FROM {0} ", TableName);
  497.             if (key != null) {
  498.                 sql += string.Format("WHERE {0}=@0", PrimaryKeyField);
  499.                 args = new object[] { key };
  500.             } else if (!string.IsNullOrEmpty(where)) {
  501.                 sql += where.Trim().StartsWith("where", StringComparison.OrdinalIgnoreCase) ? where : "WHERE " + where;
  502.             }
  503.             return CreateCommand(sql, null, args);
  504.         }
  505.  
  506.         public bool IsValid(dynamic item) {
  507.             Errors.Clear();
  508.             Validate(item);
  509.             return Errors.Count == 0;
  510.         }
  511.  
  512.         //Temporary holder for error messages
  513.         public IList<string> Errors = new List<string>();
  514.         /// <summary>
  515.         /// Adds a record to the database. You can pass in an Anonymous object, an ExpandoObject,
  516.         /// A regular old POCO, or a NameValueColletion from a Request.Form or Request.QueryString
  517.         /// </summary>
  518.         public virtual dynamic Insert(object o) {
  519.             var ex = o.ToExpando();
  520.             if (!IsValid(ex)) {
  521.                 throw new InvalidOperationException("Can't insert: " + String.Join("; ", Errors.ToArray()));
  522.             }
  523.             if (BeforeSave(ex)) {
  524.                 using (dynamic conn = OpenConnection()) {
  525.                     var cmd = CreateInsertCommand(ex);
  526.                     cmd.Connection = conn;
  527.                     cmd.ExecuteNonQuery();
  528.                     cmd.CommandText = "SELECT @@IDENTITY as newID";
  529.                     ex.ID = cmd.ExecuteScalar();
  530.                     Inserted(ex);
  531.                 }
  532.                 return ex;
  533.             } else {
  534.                 return null;
  535.             }
  536.         }
  537.         /// <summary>
  538.         /// Updates a record in the database. You can pass in an Anonymous object, an ExpandoObject,
  539.         /// A regular old POCO, or a NameValueCollection from a Request.Form or Request.QueryString
  540.         /// </summary>
  541.         public virtual int Update(object o, object key) {
  542.             var ex = o.ToExpando();
  543.             if (!IsValid(ex)) {
  544.                 throw new InvalidOperationException("Can't Update: " + String.Join("; ", Errors.ToArray()));
  545.             }
  546.             var result = 0;
  547.             if (BeforeSave(ex)) {
  548.                 result = Execute(CreateUpdateCommand(ex, key));
  549.                 Updated(ex);
  550.             }
  551.             return result;
  552.         }
  553.         /// <summary>
  554.         /// Removes one or more records from the DB according to the passed-in WHERE
  555.         /// </summary>
  556.         public int Delete(object key = null, string where = "", params object[] args) {
  557.             var deleted = this.Single(key);
  558.             var result = 0;
  559.             if (BeforeDelete(deleted)) {
  560.                 result = Execute(CreateDeleteCommand(where: where, key: key, args: args));
  561.                 Deleted(deleted);
  562.             }
  563.             return result;
  564.         }
  565.  
  566.         public void DefaultTo(string key, object value, dynamic item) {
  567.             if (!ItemContainsKey(key, item)) {
  568.                 var dc = (IDictionary<string, object>)item;
  569.                 dc[key] = value;
  570.             }
  571.         }
  572.  
  573.         //Hooks
  574.         public virtual void Validate(dynamic item) { }
  575.         public virtual void Inserted(dynamic item) { }
  576.         public virtual void Updated(dynamic item) { }
  577.         public virtual void Deleted(dynamic item) { }
  578.         public virtual bool BeforeDelete(dynamic item) { return true; }
  579.         public virtual bool BeforeSave(dynamic item) { return true; }
  580.  
  581.         //validation methods
  582.         public virtual void ValidatesPresenceOf(object value, string message = "Required") {
  583.             if (value == null)
  584.                 Errors.Add(message);
  585.             if (String.IsNullOrEmpty(value.ToString()))
  586.                 Errors.Add(message);
  587.         }
  588.         //fun methods
  589.         public virtual void ValidatesNumericalityOf(object value, string message = "Should be a number") {
  590.             var type = value.GetType().Name;
  591.             var numerics = new string[] { "Int32", "Int16", "Int64", "Decimal", "Double", "Single", "Float" };
  592.             if (!numerics.Contains(type)) {
  593.                 Errors.Add(message);
  594.             }
  595.         }
  596.         public virtual void ValidateIsCurrency(object value, string message = "Should be money") {
  597.             if (value == null)
  598.                 Errors.Add(message);
  599.             decimal val = decimal.MinValue;
  600.             decimal.TryParse(value.ToString(), out val);
  601.             if (val == decimal.MinValue)
  602.                 Errors.Add(message);
  603.  
  604.  
  605.         }
  606.         public int Count() {
  607.             return Count(TableName);
  608.         }
  609.         public int Count(string tableName, string where="") {
  610.             return (int)Scalar("SELECT COUNT(*) FROM " + tableName+" "+where);
  611.         }
  612.  
  613.         /// <summary>
  614.         /// A helpful query tool
  615.         /// </summary>
  616.         public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) {
  617.             //parse the method
  618.             var constraints = new List<string>();
  619.             var counter = 0;
  620.             var info = binder.CallInfo;
  621.             // accepting named args only... SKEET!
  622.             if (info.ArgumentNames.Count != args.Length) {
  623.                 throw new InvalidOperationException("Please use named arguments for this type of query - the column name, orderby, columns, etc");
  624.             }
  625.             //first should be "FindBy, Last, Single, First"
  626.             var op = binder.Name;
  627.             var columns = " * ";
  628.             string orderBy = string.Format(" ORDER BY {0}", PrimaryKeyField);
  629.             string sql = "";
  630.             string where = "";
  631.             var whereArgs = new List<object>();
  632.  
  633.             //loop the named args - see if we have order, columns and constraints
  634.             if (info.ArgumentNames.Count > 0) {
  635.  
  636.                 for (int i = 0; i < args.Length; i++) {
  637.                     var name = info.ArgumentNames[i].ToLower();
  638.                     switch (name) {
  639.                         case "orderby":
  640.                             orderBy = " ORDER BY " + args[i];
  641.                             break;
  642.                         case "columns":
  643.                             columns = args[i].ToString();
  644.                             break;
  645.                         default:
  646.                             constraints.Add(string.Format(" {0} = @{1}", name, counter));
  647.                             whereArgs.Add(args[i]);
  648.                             counter++;
  649.                             break;
  650.                     }
  651.                 }
  652.             }
  653.  
  654.             //Build the WHERE bits
  655.             if (constraints.Count > 0) {
  656.                 where = " WHERE " + string.Join(" AND ", constraints.ToArray());
  657.             }
  658.             //probably a bit much here but... yeah this whole thing needs to be refactored...
  659.             if (op.ToLower() == "count") {
  660.                 result = Scalar("SELECT COUNT(*) FROM " + TableName + where, whereArgs.ToArray());
  661.             } else if (op.ToLower() == "sum") {
  662.                 result = Scalar("SELECT SUM(" + columns + ") FROM " + TableName + where, whereArgs.ToArray());
  663.             } else if (op.ToLower() == "max") {
  664.                 result = Scalar("SELECT MAX(" + columns + ") FROM " + TableName + where, whereArgs.ToArray());
  665.             } else if (op.ToLower() == "min") {
  666.                 result = Scalar("SELECT MIN(" + columns + ") FROM " + TableName + where, whereArgs.ToArray());
  667.             } else if (op.ToLower() == "avg") {
  668.                 result = Scalar("SELECT AVG(" + columns + ") FROM " + TableName + where, whereArgs.ToArray());
  669.             } else {
  670.  
  671.                 //build the SQL
  672.                 sql = "SELECT TOP 1 " + columns + " FROM " + TableName + where;
  673.                 var justOne = op.StartsWith("First") || op.StartsWith("Last") || op.StartsWith("Get") || op.StartsWith("Single");
  674.  
  675.                 //Be sure to sort by DESC on the PK (PK Sort is the default)
  676.                 if (op.StartsWith("Last")) {
  677.                     orderBy = orderBy + " DESC ";
  678.                 } else {
  679.                     //default to multiple
  680.                     sql = "SELECT " + columns + " FROM " + TableName + where;
  681.                 }
  682.  
  683.                 if (justOne) {
  684.                     //return a single record
  685.                     result = Query(sql + orderBy, whereArgs.ToArray()).FirstOrDefault();
  686.                 } else {
  687.                     //return lots
  688.                     result = Query(sql + orderBy, whereArgs.ToArray());
  689.                 }
  690.             }
  691.             return true;
  692.         }
  693.     }
  694. }
  695.  
  696. /*
  697.  Massive is a Single File Database Lover. It's Better Than Chocolate. No Really.
  698. ===============================================================================
  699.  
  700.  
  701. I'm sharing this with the world because we need another way to access data - don't you think? Truthfully - I wanted to see if I could flex the C# 4 stuff and
  702. run up data access with a single file. I used to have this down to 350 lines, but you also needed to reference WebMatrix.Data. Now you don't - this is ready to roll
  703. and weighs in at a lovely 524 lines of code. Most of which is comments.
  704.  
  705. How To Install It?
  706. ------------------
  707. Drop the code file into your app and change it as you wish.
  708.  
  709. How Do You Use It?
  710. ------------------
  711. Massive is a "wrapper" for your DB tables and uses System.Dynamic extensively. If you try to use this with C# 3.5 or below, it will explode and you will be sad. Me too honestly - I like how this doesn't require any DLLs other than what's in the GAC. Yippee.
  712.  
  713.  * Get a Database. Northwind will work nicely. Add a connection to your database in your web.config (or app.config). Don't forget the providerName! If you don't know what that is - just add providerName = 'System.Data.SqlClient' right after the whole connectionString stuff.
  714.  * Create a class that wraps a table. You can call it whatever you like, but if you want to be cool just name it the same as your table.
  715.  * Query away and have fun
  716.  
  717. Code Please
  718. -----------
  719. Let's say we have a table named "Products". You create a class like this:
  720.  
  721. ```csharp
  722. public class Products:DynamicModel {
  723.     //you don't have to specify the connection - Massive will use the first one it finds in your config
  724.     public Products():base("northwind", "products","productid") {}
  725. }
  726. ```
  727.  
  728. You could also just instantiate it inline, as needed:
  729.  
  730. ```csharp
  731. var tbl = new DynamicModel("northwind", tableName:"Products", primaryKeyField:"ProductID");
  732. ```
  733.  
  734. Or ignore the object hierarchy altogether:
  735.    
  736. ```csharp
  737. Massive.DB.Current.Query(...);
  738. ```
  739.  
  740. Now you can query thus:
  741.  
  742. ```csharp
  743. var table = new Products();
  744. //grab all the products
  745. var products = table.All();
  746. //just grab from category 4. This uses named parameters
  747. var productsFour = table.All(columns: "ProductName as Name", where: "WHERE categoryID=@0",args: 4);
  748. ```
  749.  
  750. That works, but Massive is "dynamic" - which means that it can figure a lot of things out on the fly. That query above can be rewritten like this:
  751.  
  752. ```csharp
  753. dynamic table = new Products(); //"dynamic" is important here - don't use "var"!
  754. var productsFour = table.Find(CategoryID:4,columns:"ProductName");
  755. ```
  756.    
  757. The "Find" method doesn't exist, but since Massive is dynamic it will try to infer what you mean by using DynamicObject's TryInvokeMember. See the source for more details. There's more on the dynamic query stuff down below.
  758.    
  759. You can also run ad-hoc queries as needed:
  760.  
  761. ```csharp
  762. var result = tbl.Query("SELECT * FROM Categories");
  763. ```
  764.  
  765. This will pull categories and enumerate the results - streaming them as opposed to bulk-fetching them (thanks to Jeroen Haegebaert for the code). If you need to run a Fetch - you can:
  766.  
  767. ```csharp
  768. var result = tbl.Fetch("SELECT * FROM Categories");
  769. ```
  770.  
  771. If you want to have a paged result set - you can:
  772.  
  773. ```csharp
  774. var result = tbl.Paged(where: "UnitPrice > 20", currentPage:2, pageSize: 20);
  775. ```
  776.  
  777. In this example, ALL of the arguments are optional and default to reasonable values. CurrentPage defaults to 1, pageSize defaults to 20, where defaults to nothing.
  778.  
  779. What you get back is `IEnumerable<ExpandoObject>` - meaning that it's malleable and exciting. It will take the shape of whatever you return in your query, and it will have properties and so on. You can assign events to it, you can create delegates on the fly. You can give it chocolate, and it will kiss you.
  780.  
  781. That's pretty much it. One thing I really like is the groovy DSL that Massive uses - it looks just like SQL. If you want, you can use this DSL to query the database:
  782.  
  783. ```csharp
  784. var table = new Products();
  785. var productsThatILike = table.Query("SELECT ProductName, CategoryName FROM Products INNER JOIN Categories ON Categories.CategoryID = Products.CategoryID WHERE CategoryID = @0",5);
  786. //get down!
  787. ```
  788.  
  789. Some of you might look at that and think it looks suspiciously like inline SQL. It *does* look sort of like it doesn't it! But I think it reads a bit better than Linq to SQL - it's a bit closer to the mark if you will.
  790.  
  791. Inserts and Updates
  792. -------------------
  793. Massive is built on top of dynamics - so if you send an object to a table, it will get parsed into a query. If that object has a property on it that matches the primary key, Massive will think you want to update something. Unless you tell it specifically to update it.
  794.  
  795. You can send just about anything into the MassiveTransmoQueryfier and it will magically get turned into SQL:
  796.  
  797. ```csharp
  798. var table = new Products();
  799. var poopy = new {ProductName = "Chicken Fingers"};
  800. //update Product with ProductID = 12 to have a ProductName of "Chicken Fingers"
  801. table.Update(poopy, 12);
  802. ```
  803.  
  804. This also works if you have a form on your web page with the name "ProductName" - then you submit it:
  805.  
  806. ```csharp
  807. var table = new Products();
  808. //update Product with ProductID = 12 to have a ProductName of whatever was submitted via the form
  809. table.Update(poopy, Request.Form);
  810. ```
  811.  
  812. Insert works the same way:
  813.  
  814. ```csharp
  815. //pretend we have a class like Products but it's called Categories
  816. var table = new Categories();
  817. //do it up - the new ID will be returned from the query
  818. var newID = table.Insert(new {CategoryName = "Buck Fify Stuff", Description = "Things I like"});
  819. ```
  820.  
  821. Yippee Skippy! Now we get to the fun part - and one of the reasons I had to spend 150 more lines of code on something you probably won't care about. What happens when we send a whole bunch of goodies to the database at once!
  822.  
  823. ```csharp
  824. var table = new Products();
  825. //OH NO YOU DIDN't just pass in an integer inline without a parameter!
  826. //I think I might have... yes
  827. var drinks = table.All("WHERE CategoryID = 8");
  828. //what we get back here is an IEnumerable < ExpandoObject > - we can go to town
  829. foreach(var item in drinks.ToArray()){
  830.     //turn them into Haack Snacks
  831.     item.CategoryID = 12;
  832. }
  833. //Let's update these in bulk, in a transaction shall we?
  834. table.Save(drinks.ToArray());
  835. ```
  836.    
  837. Named Argument Query Syntax
  838. -------------------
  839. I recently added the ability to run more friendly queries using Named Arguments and C#4's Method-on-the-fly syntax. Originally this was trying to be like ActiveRecord, but I figured "C# is NOT Ruby, and Named Arguments can be a lot more clear". In addition, Mark Rendle's Simple.Data is already doing this so ... why duplicate things?
  840.  
  841. If your needs are more complicated - I would suggest just passing in your own SQL with Query().
  842.  
  843. ```csharp
  844. //important - must be dynamic
  845. dynamic table = new Products();
  846.  
  847. var drinks = table.FindBy(CategoryID:8);
  848. //what we get back here is an IEnumerable < ExpandoObject > - we can go to town
  849. foreach(var item in drinks){
  850.     Console.WriteLine(item.ProductName);
  851. }
  852. //returns the first item in the DB for category 8
  853. var first = table.First(CategoryID:8);
  854.  
  855. //you dig it - the last as sorted by PK
  856. var last = table.Last(CategoryID:8);
  857.  
  858. //you can order by whatever you like
  859. var firstButReallyLast = table.First(CategoryID:8,OrderBy:"PK DESC");
  860.  
  861. //only want one column?
  862. var price = table.First(CategoryID:8,Columns:"UnitPrice").UnitPrice;
  863.  
  864. //Multiple Criteria?
  865. var items = table.Find(CategoryID:5, UnitPrice:100, OrderBy:"UnitPrice DESC");
  866. ```
  867.    
  868. Aggregates with Named Arguments
  869. -------------------------------
  870. You can do the same thing as above for aggregates:
  871.  
  872. ```csharp
  873. var sum = table.Sum(columns:Price, CategoryID:5);
  874. var avg = table.Avg(columns:Price, CategoryID:3);
  875. var min = table.Min(columns:ID);
  876. var max = table.Max(columns:CreatedOn);
  877. var count = table.Count();
  878. ```
  879.    
  880. Metadata
  881. --------
  882. If you find that you need to know information about your table - to generate some lovely things like ... whatever - just ask for the Schema property. This will query INFORMATION_SCHEMA for you, and you can take a look at DATA_TYPE, DEFAULT_VALUE, etc for whatever system you're running on.
  883.  
  884. In addition, if you want to generate an empty instance of a column - you can now ask for a "Prototype()" - which will return all the columns in your table with the defaults set for you (getdate(), raw values, newid(), etc).
  885.  
  886. Factory Constructor
  887. -------------------
  888. One thing that can be useful is to use Massive to just run a quick query. You can do that now by using "Open()" which is a static builder on DynamicModel:
  889.  
  890. ```csharp
  891. var db = Massive.DynamicModel.Open("myConnectionStringName");
  892. ```
  893.  
  894. You can execute whatever you like at that point.
  895.  
  896. Validations
  897. -----------
  898. One thing that's always needed when working with data is the ability to stop execution if something isn't right. Massive now has Validations, which are built with the Rails approach in mind:
  899.  
  900. ```csharp
  901. public class Productions:DynamicModel {
  902.     public Productions():base("MyConnectionString","Productions","ID") {}
  903.     public override void Validate(dynamic item) {
  904.         ValidatesPresenceOf("Title");
  905.         ValidatesNumericalityOf(item.Price);
  906.         ValidateIsCurrency(item.Price);
  907.         if (item.Price <= 0)
  908.             Errors.Add("Price can't be negative");
  909.     }
  910. }
  911. ```
  912.  
  913. The idea here is that `Validate()` is called prior to Insert/Update. If it fails, an Error collection is populated and an InvalidOperationException is thrown. That simple. With each of the validations above, a message can be passed in.
  914.  
  915. CallBacks
  916. ---------
  917. Need something to happen after Update/Insert/Delete? Need to halt before save? Massive has callbacks to let you do just that:
  918.  
  919. ```csharp
  920. public class Customers:DynamicModel {
  921.     public Customers():base("MyConnectionString","Customers","ID") {}
  922.    
  923.     //Add the person to Highrise CRM when they're added to the system...
  924.     public override void Inserted(dynamic item) {
  925.         //send them to Highrise
  926.         var svc = new HighRiseApi();
  927.         svc.AddPerson(...);
  928.     }
  929. }
  930. ```
  931.  
  932. The callbacks you can use are:
  933.  
  934.  * Inserted
  935.  * Updated
  936.  * Deleted
  937.  * BeforeDelete
  938.  * BeforeSave
  939.  
  940.  
  941.  
  942.  */
Advertisement
Add Comment
Please, Sign In to add comment