Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Big update - removed async bits as they left a connection open. Added…

… validations and callbacks. Added aggregate query stuff to named argument dynamic thingers.
  • Loading branch information...
commit a1ec04d7028967ae6d547b6d3cc38118965bec04 1 parent 5ad17a5
@subsonic subsonic authored
Showing with 260 additions and 135 deletions.
  1. +211 −123 Massive.cs
  2. +49 −12 README.markdown
View
334 Massive.cs
@@ -9,6 +9,7 @@
using System.Text;
using System.Threading.Tasks;
using System.Data.SqlClient;
+using System.Text.RegularExpressions;
namespace Massive {
public static class ObjectExtensions {
@@ -79,6 +80,7 @@ public static class ObjectExtensions {
}
return result;
}
+
/// <summary>
/// Turns the object into a Dictionary
/// </summary>
@@ -96,7 +98,8 @@ public class DynamicModel : DynamicObject {
dynamic dm = new DynamicModel(connectionStringName);
return dm;
}
- public DynamicModel(string connectionStringName, string tableName = "", string primaryKeyField = "") {
+ public DynamicModel(string connectionStringName, string tableName = "",
+ string primaryKeyField = "", string descriptorField = "") {
TableName = tableName == "" ? this.GetType().Name : tableName;
PrimaryKeyField = string.IsNullOrEmpty(primaryKeyField) ? "ID" : primaryKeyField;
var _providerName = "System.Data.SqlClient";
@@ -117,12 +120,7 @@ public class DynamicModel : DynamicObject {
if (exists) {
var key = item.ToString();
var val = coll[key];
- if (!String.IsNullOrEmpty(val)) {
- //what to do here? If it's empty... set it to NULL?
- //if it's a string value - let it go through if it's NULLABLE?
- //Empty? WTF?
- dc.Add(key, val);
- }
+ dc.Add(key, val);
}
}
return result;
@@ -159,14 +157,20 @@ public class DynamicModel : DynamicObject {
return result;
}
}
+ private string _descriptorField;
+ public string DescriptorField {
+ get {
+ return _descriptorField;
+ }
+ }
/// <summary>
/// List out all the schema bits for use with ... whatever
/// </summary>
IEnumerable<dynamic> _schema;
public IEnumerable<dynamic> Schema {
get {
- if(_schema == null)
- _schema= Query("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @0", TableName);
+ if (_schema == null)
+ _schema = Query("SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @0", TableName);
return _schema;
}
}
@@ -182,21 +186,6 @@ public class DynamicModel : DynamicObject {
}
}
}
- /// <summary>
- /// Executes the reader using SQL async API - thanks to Damian Edwards
- /// </summary>
- public void QueryAsync(string sql, Action<List<dynamic>> callback, params object[] args) {
- using (var conn = new SqlConnection(ConnectionString)) {
- var cmd = new SqlCommand(sql, conn);
- cmd.AddParams(args);
- conn.Open();
- var task = Task.Factory.FromAsync<IDataReader>(cmd.BeginExecuteReader, cmd.EndExecuteReader, null);
- task.ContinueWith(x => callback.Invoke(x.Result.ToExpandoList()));
- //make sure this is closed off.
- conn.Close();
- }
- }
-
public virtual IEnumerable<dynamic> Query(string sql, DbConnection connection, params object[] args) {
using (var rdr = CreateCommand(sql, connection, args).ExecuteReader()) {
while (rdr.Read()) {
@@ -250,15 +239,7 @@ public class DynamicModel : DynamicObject {
}
return commands;
}
- /// <summary>
- /// Executes a set of objects as Insert or Update commands based on their property settings, within a transaction.
- /// These objects can be POCOs, Anonymous, NameValueCollections, or Expandos. Objects
- /// With a PK property (whatever PrimaryKeyField is set to) will be created at UPDATEs
- /// </summary>
- public virtual int Save(params object[] things) {
- var commands = BuildCommands(things);
- return Execute(commands);
- }
+
public virtual int Execute(DbCommand command) {
return Execute(new DbCommand[] { command });
@@ -303,8 +284,98 @@ public class DynamicModel : DynamicObject {
}
public virtual string TableName { get; set; }
/// <summary>
- /// Creates a command for use with transactions - internal stuff mostly, but here for you to play with
+ /// Returns all records complying with the passed-in WHERE clause and arguments,
+ /// ordered as specified, limited (TOP) by limit.
/// </summary>
+ public virtual IEnumerable<dynamic> All(string where = "", string orderBy = "", int limit = 0, string columns = "*", params object[] args) {
+ string sql = BuildSelect(where, orderBy, limit);
+ return Query(string.Format(sql, columns, TableName), args);
+ }
+ private static string BuildSelect(string where, string orderBy, int limit) {
+ string sql = limit > 0 ? "SELECT TOP " + limit + " {0} FROM {1} " : "SELECT {0} FROM {1} ";
+ if (!string.IsNullOrEmpty(where))
+ sql += where.Trim().StartsWith("where", StringComparison.OrdinalIgnoreCase) ? where : "WHERE " + where;
+ if (!String.IsNullOrEmpty(orderBy))
+ sql += orderBy.Trim().StartsWith("order by", StringComparison.OrdinalIgnoreCase) ? orderBy : " ORDER BY " + orderBy;
+ return sql;
+ }
+
+ /// <summary>
+ /// Returns a dynamic PagedResult. Result properties are Items, TotalPages, and TotalRecords.
+ /// </summary>
+ public virtual dynamic Paged(string where = "", string orderBy = "", string columns = "*", int pageSize = 20, int currentPage = 1, params object[] args) {
+ dynamic result = new ExpandoObject();
+ var countSQL = string.Format("SELECT COUNT({0}) FROM {1}", PrimaryKeyField, TableName);
+ if (String.IsNullOrEmpty(orderBy))
+ orderBy = PrimaryKeyField;
+
+ if (!string.IsNullOrEmpty(where)) {
+ if (!where.Trim().StartsWith("where", StringComparison.OrdinalIgnoreCase)) {
+ where = "WHERE " + where;
+ }
+ }
+ var sql = 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);
+ var pageStart = (currentPage - 1) * pageSize;
+ sql += string.Format(" WHERE Row > {0} AND Row <={1}", pageStart, (pageStart + pageSize));
+ countSQL += where;
+ result.TotalRecords = Scalar(countSQL, args);
+ result.TotalPages = result.TotalRecords / pageSize;
+ if (result.TotalRecords % pageSize > 0)
+ result.TotalPages += 1;
+ result.Items = Query(string.Format(sql, columns, TableName), args);
+ return result;
+ }
+ /// <summary>
+ /// Returns a single row from the database
+ /// </summary>
+ public virtual dynamic Single(string where, params object[] args) {
+ var sql = string.Format("SELECT * FROM {0} WHERE {1}", TableName, where);
+ return Query(sql, args).FirstOrDefault();
+ }
+ /// <summary>
+ /// Returns a single row from the database
+ /// </summary>
+ public virtual dynamic Single(object key, string columns = "*") {
+ var sql = string.Format("SELECT {0} FROM {1} WHERE {2} = @0", columns, TableName, PrimaryKeyField);
+ return Query(sql, key).FirstOrDefault();
+ }
+ /// <summary>
+ /// This will return a string/object dictionary for dropdowns etc
+ /// </summary>
+ public virtual IDictionary<string, object> KeyValues(string orderBy = "") {
+ if (String.IsNullOrEmpty(DescriptorField))
+ throw new InvalidOperationException("There's no DescriptorField set - do this in your constructor to describe the text value you want to see");
+ var sql = string.Format("SELECT {0},{1} FROM {2} ", PrimaryKeyField, DescriptorField, TableName);
+ if (!String.IsNullOrEmpty(orderBy))
+ sql += "ORDER BY " + orderBy;
+ return (IDictionary<string, object>)Query(sql);
+ }
+
+ /// <summary>
+ /// This will return an Expando as a Dictionary
+ /// </summary>
+ public virtual IDictionary<string, object> ItemAsDictionary(ExpandoObject item) {
+ return (IDictionary<string, object>)item;
+ }
+ //Checks to see if a key is present based on the passed-in value
+ public virtual bool ItemContainsKey(string key, ExpandoObject item) {
+ var dc = ItemAsDictionary(item);
+ return dc.ContainsKey(key);
+ }
+ /// <summary>
+ /// Executes a set of objects as Insert or Update commands based on their property settings, within a transaction.
+ /// These objects can be POCOs, Anonymous, NameValueCollections, or Expandos. Objects
+ /// With a PK property (whatever PrimaryKeyField is set to) will be created at UPDATEs
+ /// </summary>
+ public virtual int Save(params object[] things) {
+ foreach (var item in things) {
+ if (!IsValid(item)) {
+ throw new InvalidOperationException("Can't save this item: " + String.Join("; ", Errors.ToArray()));
+ }
+ }
+ var commands = BuildCommands(things);
+ return Execute(commands);
+ }
public virtual DbCommand CreateInsertCommand(object o) {
DbCommand result = null;
var expando = o.ToExpando();
@@ -369,97 +440,101 @@ public class DynamicModel : DynamicObject {
}
return CreateCommand(sql, null, args);
}
+
+ public bool IsValid(dynamic item) {
+ Errors.Clear();
+ Validate(item);
+ return Errors.Count == 0;
+ }
+
+ //Temporary holder for error messages
+ public IList<string> Errors = new List<string>();
/// <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 virtual object Insert(object o) {
- dynamic result = 0;
- using (var conn = OpenConnection()) {
- var cmd = CreateInsertCommand(o);
- cmd.Connection = conn;
- cmd.ExecuteNonQuery();
- cmd.CommandText = "SELECT @@IDENTITY as newID";
- result = cmd.ExecuteScalar();
+ public virtual dynamic Insert(object o) {
+ if (!IsValid(o.ToExpando())) {
+ throw new InvalidOperationException("Can't insert: " + String.Join("; ", Errors.ToArray()));
+ }
+ if (BeforeSave(o.ToExpando())) {
+ dynamic newRecord = new ExpandoObject();
+ using (var conn = OpenConnection()) {
+ var cmd = CreateInsertCommand(o);
+ cmd.Connection = conn;
+ cmd.ExecuteNonQuery();
+ cmd.CommandText = "SELECT @@IDENTITY as newID";
+ newRecord = o.ToExpando();
+ newRecord.ID = cmd.ExecuteScalar();
+ Inserted(newRecord);
+ }
+ return newRecord;
+ } else {
+ return null;
}
- return result;
}
/// <summary>
/// Updates a record in the database. You can pass in an Anonymous object, an ExpandoObject,
/// A regular old POCO, or a NameValueCollection from a Request.Form or Request.QueryString
/// </summary>
public virtual int Update(object o, object key) {
- return Execute(CreateUpdateCommand(o, key));
+ if (!IsValid(o.ToExpando())) {
+ throw new InvalidOperationException("Can't Update: " + String.Join("; ", Errors.ToArray()));
+ }
+ var result = 0;
+ if (BeforeSave(o.ToExpando())) {
+ result = Execute(CreateUpdateCommand(o, key));
+ Updated(o);
+ }
+ return result;
}
/// <summary>
/// Removes one or more records from the DB according to the passed-in WHERE
/// </summary>
public int Delete(object key = null, string where = "", params object[] args) {
- return Execute(CreateDeleteCommand(where: where, key: key, args: args));
- }
- /// <summary>
- /// Returns all records complying with the passed-in WHERE clause and arguments,
- /// ordered as specified, limited (TOP) by limit.
- /// </summary>
- public virtual IEnumerable<dynamic> All(string where = "", string orderBy = "", int limit = 0, string columns = "*", params object[] args) {
- string sql = BuildSelect(where, orderBy, limit);
- return Query(string.Format(sql, columns, TableName), args);
- }
- private static string BuildSelect(string where, string orderBy, int limit) {
- string sql = limit > 0 ? "SELECT TOP " + limit + " {0} FROM {1} " : "SELECT {0} FROM {1} ";
- if (!string.IsNullOrEmpty(where))
- sql += where.Trim().StartsWith("where", StringComparison.OrdinalIgnoreCase) ? where : "WHERE " + where;
- if (!String.IsNullOrEmpty(orderBy))
- sql += orderBy.Trim().StartsWith("order by", StringComparison.OrdinalIgnoreCase) ? orderBy : " ORDER BY " + orderBy;
- return sql;
- }
- /// <summary>
- /// Returns all records complying with the passed-in WHERE clause and arguments,
- /// ordered as specified, limited (TOP) by limit.
- /// </summary>
- public virtual void AllAsync(Action<List<dynamic>> callback, string where = "", string orderBy = "", int limit = 0, string columns = "*", params object[] args) {
- string sql = BuildSelect(where, orderBy, limit);
- QueryAsync(string.Format(sql, columns, TableName), callback, args);
- }
- /// <summary>
- /// Returns a dynamic PagedResult. Result properties are Items, TotalPages, and TotalRecords.
- /// </summary>
- public virtual dynamic Paged(string where = "", string orderBy = "", string columns = "*", int pageSize = 20, int currentPage = 1, params object[] args) {
- dynamic result = new ExpandoObject();
- var countSQL = string.Format("SELECT COUNT({0}) FROM {1}", PrimaryKeyField, TableName);
- if (String.IsNullOrEmpty(orderBy))
- orderBy = PrimaryKeyField;
-
- if (!string.IsNullOrEmpty(where)) {
- if (!where.Trim().StartsWith("where", StringComparison.OrdinalIgnoreCase)) {
- where = "WHERE " + where;
- }
+ var deleted = this.Single(key);
+ var result = 0;
+ if (BeforeDelete(deleted)) {
+ result = Execute(CreateDeleteCommand(where: where, key: key, args: args));
+ Deleted(deleted);
}
- var sql = 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);
- var pageStart = (currentPage - 1) * pageSize;
- sql += string.Format(" WHERE Row > {0} AND Row <={1}", pageStart, (pageStart + pageSize));
- countSQL += where;
- result.TotalRecords = Scalar(countSQL, args);
- result.TotalPages = result.TotalRecords / pageSize;
- if (result.TotalRecords % pageSize > 0)
- result.TotalPages += 1;
- result.Items = Query(string.Format(sql, columns, TableName), args);
return result;
}
- /// <summary>
- /// Returns a single row from the database
- /// </summary>
- public virtual dynamic Single(string where, params object[] args) {
- var sql = string.Format("SELECT * FROM {0} WHERE {1}", TableName, where);
- return Query(sql, args).FirstOrDefault();
+
+ //Hooks
+ public virtual void Validate(dynamic item) { }
+ public virtual void Inserted(dynamic item) { }
+ public virtual void Updated(dynamic item) { }
+ public virtual void Deleted(dynamic item) { }
+ public virtual bool BeforeDelete(dynamic item) { return true; }
+ public virtual bool BeforeSave(dynamic item) { return true; }
+
+ //validation methods
+ public virtual void ValidatesPresenceOf(object value, string message = "Required") {
+ if (value == null)
+ Errors.Add(message);
+ if (String.IsNullOrEmpty(value.ToString()))
+ Errors.Add(message);
+ }
+ //fun methods
+ public virtual void ValidatesNumericalityOf(object value, string message = "Should be a number") {
+ var type = value.GetType().Name;
+ var numerics = new string[] { "Int32", "Int16", "Int64", "Decimal", "Double", "Single", "Float" };
+ if (!numerics.Contains(type)) {
+ Errors.Add(message);
+ }
}
- /// <summary>
- /// Returns a single row from the database
- /// </summary>
- public virtual dynamic Single(object key, string columns = "*") {
- var sql = string.Format("SELECT {0} FROM {1} WHERE {2} = @0", columns, TableName, PrimaryKeyField);
- return Query(sql, key).FirstOrDefault();
+ public virtual void ValidateIsCurrency(object value, string message = "Should be money") {
+ if (value == null)
+ Errors.Add(message);
+ decimal val = decimal.MinValue;
+ decimal.TryParse(value.ToString(), out val);
+ if (val == decimal.MinValue)
+ Errors.Add(message);
+
+
}
+
/// <summary>
/// A helpful query tool
/// </summary>
@@ -472,12 +547,11 @@ public class DynamicModel : DynamicObject {
if (info.ArgumentNames.Count != args.Length) {
throw new InvalidOperationException("Please use named arguments for this type of query - the column name, orderby, columns, etc");
}
-
-
//first should be "FindBy, Last, Single, First"
var op = binder.Name;
var columns = " * ";
string orderBy = string.Format(" ORDER BY {0}", PrimaryKeyField);
+ string sql = "";
string where = "";
var whereArgs = new List<object>();
@@ -501,30 +575,44 @@ public class DynamicModel : DynamicObject {
}
}
}
+
//Build the WHERE bits
if (constraints.Count > 0) {
where = " WHERE " + string.Join(" AND ", constraints.ToArray());
}
- //build the SQL
- string sql = "SELECT TOP 1 " + columns + " FROM " + TableName + where;
- var justOne = op.StartsWith("First") || op.StartsWith("Last") || op.StartsWith("Get");
-
- //Be sure to sort by DESC on the PK (PK Sort is the default)
- if (op.StartsWith("Last")) {
- orderBy = orderBy + " DESC ";
+ //probably a bit much here but... yeah this whole thing needs to be refactored...
+ if (op.ToLower() == "count") {
+ result = Scalar("SELECT COUNT(*) FROM " + TableName + where, whereArgs.ToArray());
+ } else if (op.ToLower() == "sum") {
+ result = Scalar("SELECT SUM(" + columns + ") FROM " + TableName + where, whereArgs.ToArray());
+ } else if (op.ToLower() == "max") {
+ result = Scalar("SELECT MAX(" + columns + ") FROM " + TableName + where, whereArgs.ToArray());
+ } else if (op.ToLower() == "min") {
+ result = Scalar("SELECT MIN(" + columns + ") FROM " + TableName + where, whereArgs.ToArray());
+ } else if (op.ToLower() == "avg") {
+ result = Scalar("SELECT AVG(" + columns + ") FROM " + TableName + where, whereArgs.ToArray());
} else {
- //default to multiple
- sql = "SELECT " + columns + " FROM " + TableName + where;
- }
- if (justOne) {
- //return a single record
- result = Query(sql + orderBy, whereArgs.ToArray()).FirstOrDefault();
- } else {
- //return lots
- result = Query(sql + orderBy, whereArgs.ToArray());
- }
+ //build the SQL
+ sql = "SELECT TOP 1 " + columns + " FROM " + TableName + where;
+ var justOne = op.StartsWith("First") || op.StartsWith("Last") || op.StartsWith("Get") || op.StartsWith("Single");
+ //Be sure to sort by DESC on the PK (PK Sort is the default)
+ if (op.StartsWith("Last")) {
+ orderBy = orderBy + " DESC ";
+ } else {
+ //default to multiple
+ sql = "SELECT " + columns + " FROM " + TableName + where;
+ }
+
+ if (justOne) {
+ //return a single record
+ result = Query(sql + orderBy, whereArgs.ToArray()).FirstOrDefault();
+ } else {
+ //return lots
+ result = Query(sql + orderBy, whereArgs.ToArray());
+ }
+ }
return true;
}
}
View
61 README.markdown
@@ -133,6 +133,16 @@ If your needs are more complicated - I would suggest just passing in your own SQ
//Multiple Criteria?
var items = table.Find(CategoryID:5, UnitPrice:100, OrderBy:"UnitPrice DESC");
+Aggregates with Named Arguments
+-------------------------------
+You can do the same thing as above for aggregates:
+
+ var sum = table.Sum(columns:Price, CategoryID:5);
+ var avg = table.Sum(columns:Price, CategoryID:3);
+ var min = table.Min(columns:ID);
+ var max = table.Max(columns:CreatedOn);
+ var count = table.Count();
+
Metadata
--------
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.
@@ -146,15 +156,42 @@ One thing that can be useful is to use Massive to just run a quick query. You ca
You can execute whatever you like at that point.
-Asynchronous Execution
-----------------------
-Thanks to Damien Edwards, we now have the ability to query asynchronously using the Task Parallel Library:
-
- var p = new Products();
- p.AllAsync(result => {
- foreach (var item in result) {
- Console.WriteLine(item.ProductName);
- }
- });
-
-This will toss the execution (and what you need to do with it) into an asynchronous call, which is nice for scaling.
+Validations
+-----------
+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:
+
+ public class Productions:DynamicModel {
+ public Productions():base("MyConnectionString","Productions","ID") {}
+ public override void Validate(dynamic item) {
+ ValidatesPresenceOf("Title");
+ ValidatesNumericalityOf("Price");
+ ValidateIsCurrency("Price");
+ if (item.Price <= 0)
+ Errors.Add("Price can't be negative");
+ }
+ }
+
+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.
+
+CallBacks
+---------
+Need something to happen After Update/Insert/Delete? Need to halt BeforeSave? Massive has callbacks to let you do just that:
+
+ public class Customers:DynamicModel {
+ public Customers():base("MyConnectionString","Customers","ID") {}
+
+ //Add the person to Highrise CRM when they're added to the system...
+ public override void Inserted(dynamic item) {
+ //send them to Highrise
+ var svc = new HighRiseApi();
+ svc.AddPerson(...);
+ }
+ }
+The callbacks you can use are:
+*Inserted
+*Updated
+*Deleted
+*BeforeDelete
+*BeforeSave
+
+
Please sign in to comment.
Something went wrong with that request. Please try again.