Commits

sake  committed 6eb8501

firt commit, all tests pass

  • Participants

Comments (0)

Files changed (22)

+syntax: glob
+*.suo
+*/bin/Debug/*
+*/bin/Release/*
+*.cache
+*.csproj.FileListAbsolute.txt
+*/obj/Debug/*
+*/obj/Release/*
+*.user
+
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual C# Express 2010
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Usually", "Usually\Usually.csproj", "{AB4B3A1B-3915-4198-96C3-A02B089E8878}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UsuallyTests", "UsuallyTests\UsuallyTests.csproj", "{082C43E4-ADAB-49B0-B103-B9FB76659734}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{AB4B3A1B-3915-4198-96C3-A02B089E8878}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AB4B3A1B-3915-4198-96C3-A02B089E8878}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AB4B3A1B-3915-4198-96C3-A02B089E8878}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AB4B3A1B-3915-4198-96C3-A02B089E8878}.Release|Any CPU.Build.0 = Release|Any CPU
+		{082C43E4-ADAB-49B0-B103-B9FB76659734}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{082C43E4-ADAB-49B0-B103-B9FB76659734}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{082C43E4-ADAB-49B0-B103-B9FB76659734}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{082C43E4-ADAB-49B0-B103-B9FB76659734}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal

File Usually/Data.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Data;
+
+namespace Usually
+{
+    public class Data
+    {
+        public static IDbCommand CreateCommand(IDbTransaction trans)
+        {
+            var cmd = trans.Connection.CreateCommand();
+            cmd.Transaction = trans;
+            return cmd;
+        }
+        public static string EscapeSqlString(string s)
+        {
+            return s.Replace("'", "''").Replace("\\", "\\\\");
+        }
+        public static object CleanUserEntry(object userEntry)
+        {
+            if (userEntry is string) 
+            {
+                var s = userEntry as string;
+                if (String.IsNullOrWhiteSpace(s)) return null;
+                return s.Trim();
+            }
+            return userEntry;
+        }
+        public static int GetNextIntKeyByMax(IDbTransaction trans, string table, string key)
+        {
+            using (var cmd = CreateCommand(trans))
+            {
+                cmd.CommandText = String.Format("select max({0}) from {1}", key, table);
+                var r = cmd.ExecuteScalar();
+                if ((r == null) || (r == DBNull.Value)) r = 0;
+                return ((int)r + 1);
+            }
+        }
+        public static bool CheckNameOnRead = true;
+        public static void CheckName(IDataRecord r, int i, string name)
+        {
+            if (!CheckNameOnRead) return;
+            if (name == null) return;
+            var got = r.GetName(i);
+            if (r.GetName(i) != name)
+                throw new Exception(
+                    String.Format(
+                      "Unmatch field name on load (expect \"{0}\", got \"{1}\")",
+                      name, got));
+        }
+        public static String ReadString(IDataRecord r, int i, string name = null)
+        {
+            CheckName(r, i, name);
+            if (r.IsDBNull(i)) return null;
+            return r.GetString(i);
+        }
+        public static Decimal? ReadDecimal(IDataRecord r, int i, string name = null)
+        {
+            CheckName(r, i, name);
+            if (r.IsDBNull(i)) return null;
+            return r.GetDecimal(i);
+        }
+        public static short? ReadShort(IDataRecord r, int i, string name = null)
+        {
+            CheckName(r, i, name);
+            if (r.IsDBNull(i)) return null;
+            return r.GetInt16(i);
+        }
+        public static int? ReadInteger(IDataRecord r, int i, string name = null)
+        {
+            CheckName(r, i, name);
+            if (r.IsDBNull(i)) return null;
+            return r.GetInt32(i);
+        }
+        public static long? ReadLong(IDataRecord r, int i, string name = null)
+        {
+            CheckName(r, i, name);
+            if (r.IsDBNull(i)) return null;
+            return r.GetInt64(i);
+        }
+        public static double? ReadDouble(IDataRecord r, int i, string name = null)
+        {
+            CheckName(r, i, name);
+            if (r.IsDBNull(i)) return null;
+            return r.GetDouble(i);
+        }
+        public static DateTime? ReadDateTime(IDataRecord r, int i, string name = null)
+        {
+            CheckName(r, i, name);
+            if (r.IsDBNull(i)) return null;
+            var d = r.GetDateTime(i);
+            return d;
+        }
+        public static bool? ReadBool(IDataRecord r, int i, string name = null)
+        {
+            CheckName(r, i, name);
+            if (r.IsDBNull(i)) return null;
+            var d = r.GetBoolean(i);
+            return d;
+        }
+        public static IEnumerable<RowType> Select<RowType>(IDbTransaction trans, string sql, Action<RowType, IDataRecord> reader, Action<IDbCommand> commander = null)
+            where RowType : class, new()
+        {
+            return Select(trans.Connection, sql, reader, commander, trans);
+        }
+        public static IEnumerable<RowType> Select<RowType>(IDbConnection con, string sql, Action<RowType, IDataRecord> reader, Action<IDbCommand> commander = null, IDbTransaction trans = null)
+            where RowType : class, new()
+        {
+            using (var cmd = con.CreateCommand())
+            {
+                if (trans != null) cmd.Transaction = trans;
+                cmd.CommandText = sql;
+                if (commander != null) commander(cmd);
+                using (var r = cmd.ExecuteReader())
+                {
+                    while (r.Read())
+                    {
+                        var o = new RowType();
+                        reader(o, r);
+                        yield return o;
+                    }
+                }
+            }
+        }
+        public static RowType GetFirst<RowType>(IDbTransaction trans, string sql, Action<RowType, IDataRecord> reader, Action<IDbCommand> commander = null)
+            where RowType : class, new()
+        {
+            return GetFirst(trans.Connection, sql, reader, commander, trans);
+        }
+        public static RowType GetFirst<RowType>(IDbConnection con, string sql, Action<RowType, IDataRecord> reader, Action<IDbCommand> commander = null, IDbTransaction trans = null)
+            where RowType : class, new()
+        {
+            using (var cmd = con.CreateCommand())
+            {
+                if (trans != null) cmd.Transaction = trans;
+                cmd.CommandText = sql;
+                {
+                    if (commander != null) commander(cmd);
+                    using (var r = cmd.ExecuteReader())
+                    {
+                        if (!r.Read()) return null;
+                        var result = new RowType();
+                        reader(result, r);
+                        return result;
+                    }
+                }
+            }
+        }
+        public static object GetFirstValue(IDbTransaction trans, string sql, Action<IDbCommand> commander = null)
+        {
+            return GetFirstValue(trans.Connection, sql, commander, trans);
+        }
+        public static object GetFirstValue(IDbConnection con, string sql, Action<IDbCommand> commander = null, IDbTransaction trans = null)
+        {
+            using (var cmd = con.CreateCommand())
+            {
+                if (trans != null) cmd.Transaction = trans;
+                cmd.CommandText = sql;
+                {
+                    if (commander != null) commander(cmd);
+                    using (var r = cmd.ExecuteReader())
+                    {
+                        if (!r.Read()) return null;
+                        var result = r.GetValue(0);
+                        if (result == DBNull.Value) return null;
+                        return result;
+                    }
+                }
+            }
+        }
+        public interface IDataDialect
+        {
+            bool SqlParam(IDbConnection connection, string name, out string sql);
+            bool AddParamWithValue(IDbCommand cmd, string name, object value);
+            bool Generate(IDbTransaction trans, string name, out object value);
+        }
+        private static List<IDataDialect> dialects = new List<IDataDialect>();
+        public static void AddDialect(IDataDialect dialect)
+        {
+            dialects.Insert(0, dialect);
+        }
+        public static void RemoveDialect(IDataDialect dialect)
+        {
+            dialects.Remove(dialect);
+        }
+        public static string SqlParam(IDbConnection connection, string name)
+        {
+            foreach (var d in dialects)
+            {
+                string sql = null;
+                if (d.SqlParam(connection, name, out sql)) return sql;
+            }
+            return ":" + name;
+        }
+        public static void AddParamWithValue(IDbCommand cmd, string name, object value)
+        {
+            foreach (var d in dialects)
+            {
+                if (d.AddParamWithValue(cmd, name, value)) return;
+            }
+            var p = cmd.CreateParameter();
+            p.ParameterName = name;
+            p.Value = value;
+            cmd.Parameters.Add(p);
+        }
+        public static object Generate(IDbTransaction trans, string name)
+        {
+            foreach (var d in dialects)
+            {
+                object value = null;
+                if (d.Generate(trans, name, out value)) return value;
+            }
+            throw new Exception("Cannot generate: " + name);
+        }
+        public static Dictionary<string, object> Insert(IDbTransaction trans, InsertRecord ins)
+        {
+            Dictionary<string, object> r = null;
+            var items = ins.Values.ToArray();
+            var sqlFields = String.Join(", ", from x in items select x.Key);
+            var sqlParams = String.Join(", ", from x in items select (SqlParam(trans.Connection, x.Key)));
+            foreach (var i in items)
+            {
+                var f = i.Value as Func<string>;
+                if (f != null)
+                {
+                    if (r == null) r = new Dictionary<string, object>();
+                    r.Add(i.Key, Generate(trans, f()));
+                }
+            }
+            using (var cmd = CreateCommand(trans))
+            {
+                cmd.CommandText = String.Format("insert into {0} ({1}) values ({2})", ins.TableName, sqlFields, sqlParams);
+                foreach (var i in items)
+                {
+                    object value = null;
+                    if (r == null || !r.TryGetValue(i.Key, out value)) value = i.Value;
+                    AddParamWithValue(cmd, i.Key, value);
+                }
+                cmd.ExecuteNonQuery();
+            }
+            return r;
+        }
+        public static string UniqueName(HashSet<string> used, string proposal)
+        {
+            if (!used.Contains(proposal)) return proposal;
+            string newproposal;
+            var i = 1;
+            do
+            {
+                i++;
+                newproposal = proposal + i.ToString();
+            }
+            while (used.Contains(newproposal));
+            return newproposal;
+        }
+        public static int _Update(IDbTransaction trans, UpdateRecord upd)
+        {
+            if (!upd.Values.Any()) return 0;
+            var keyItems = upd.Keys.ToArray();
+            var valueItems = upd.Values.ToArray();
+            var paramsUsed = new HashSet<string>();
+            var keyParams = new List<string>();
+            var keyClauses = new List<string>();
+            var valueParams = new List<string>();
+            var valueClauses = new List<string>();
+            foreach (var i in valueItems)
+            {
+                var p = UniqueName(paramsUsed, i.Key);
+                valueParams.Add(p);
+                valueClauses.Add(" " + i.Key + " = " + SqlParam(trans.Connection, p));
+                paramsUsed.Add(p);
+            }
+            foreach (var i in keyItems)
+            {
+                var p = UniqueName(paramsUsed, i.Key);
+                keyParams.Add(p);
+                keyClauses.Add(" " + i.Key + " = " + SqlParam(trans.Connection, p));
+                paramsUsed.Add(p);
+            }
+            using (var cmd = CreateCommand(trans))
+            {
+                cmd.CommandText = String.Format(
+                    "update {0} set\n{1}\nwhere\n{2}",
+                    upd.TableName,
+                    String.Join(",\n", valueClauses),
+                    String.Join(" and\n", keyClauses));
+                for (int i = 0; i < valueItems.Length; i++)
+                {
+                    AddParamWithValue(cmd, valueParams[i], valueItems[i].Value);
+                }
+                for (int i = 0; i < keyItems.Length; i++)
+                {
+                    AddParamWithValue(cmd, keyParams[i], keyItems[i].Value);
+                }
+                return cmd.ExecuteNonQuery();
+            }
+        }
+        public static void CheckTransactionResult(int c, bool atLeastOne, bool atMostOne)
+        {
+            if (atLeastOne && c == 0) throw new Exception("ไม่พบข้อมูลที่ต้องการ");
+            if (atMostOne && c > 1) throw new Exception("พบข้อมูลมากกว่าที่คาดไว้");
+        }
+        public static int Update(IDbTransaction trans, UpdateRecord upd)
+        {
+            var c = _Update(trans, upd);
+            CheckTransactionResult(c, upd.AtLeastOne, upd.AtMostOne);
+            return c;
+        }
+        public static int _Delete(IDbTransaction trans, DeleteRecord del)
+        {
+            var items = del.Keys.ToArray();
+            var sqlPredicates = String.Join(" and ", from x in items select (x.Key + " = :" + x.Key));
+            using (var cmd = CreateCommand(trans))
+            {
+                cmd.CommandText = String.Format("delete from {0} where {1}", del.TableName, sqlPredicates);
+                foreach (var i in items) AddParamWithValue(cmd, i.Key, i.Value);
+                return cmd.ExecuteNonQuery();
+            }
+        }
+        public static int Delete(IDbTransaction trans, DeleteRecord del)
+        {
+            var c = _Delete(trans, del);
+            CheckTransactionResult(c, del.AtLeastOne, del.AtMostOne);
+            return c;
+        }
+        public static bool UpdateOrInsert(IDbTransaction trans, UpdateRecord upd, InsertRecord ins = null)
+        {
+            var atLeastOne = upd.AtLeastOne;
+            upd.AtLeastOne = false;
+            int c;
+            try
+            {
+                c = Update(trans, upd);
+            }
+            finally
+            {
+                upd.AtLeastOne = atLeastOne;
+            }
+            if (c > 0) return false;
+            if (ins != null) Insert(trans, ins);
+            else
+            {
+                ins = new InsertRecord();
+                ins.TableName = upd.TableName;
+                foreach (var k in upd.Values) ins.Values[k.Key] = k.Value;
+                foreach (var v in upd.Values) ins.Values[v.Key] = v.Value;
+            }
+            return true;
+        }
+        public static IEnumerable<TransactionRecord> Reconcile<T>(Meta<T> meta, IEnumerable<T> olds, IEnumerable<T> news)
+        {
+            var r = new Reconciler<T>();
+            meta(r);
+            return r.Reconcile(olds, news);
+        }
+    }
+    public abstract class ResolverItem
+    {
+        public abstract ResolverItem Add<T>(Meta<T> meta, IEnumerable<T> t, object masterKeyGetter, object detailKeyGetter);
+    }
+    public abstract class Resolver : ResolverItem
+    {
+        public abstract ResolverItem Add<T>(Meta<T> meta, IEnumerable<T> olds, IEnumerable<T> news);
+    }
+    public abstract class TransactionRecord
+    {
+        public string TableName = null;
+        protected TransactionRecord() : this(null) { }
+        protected TransactionRecord(string tableName)
+        {
+            this.TableName = @tableName;
+        }
+        public abstract object Execute(IDbTransaction trans);
+        public abstract TransactionRecord CloneRecord();
+    }
+    public class InsertRecord : TransactionRecord
+    {
+        public InsertRecord() : base() { }
+        public InsertRecord(string tableName) : base(tableName) { }
+        public Dictionary<string, object> Values = new Dictionary<string, object>();
+        public override object Execute(IDbTransaction trans)
+        {
+            return Data.Insert(trans, this);
+        }
+        public InsertRecord CloneInsert()
+        {
+            var r = new InsertRecord();
+            r.TableName = this.TableName;
+            foreach (var i in this.Values) r.Values[i.Key] = i.Value;
+            return r;
+        }
+        public override TransactionRecord CloneRecord()
+        {
+            return this.CloneInsert();
+        }
+    }
+    public class UpdateRecord : TransactionRecord
+    {
+        public UpdateRecord() : base() { }
+        public UpdateRecord(string tableName) : base(tableName) { }
+        public bool AtLeastOne = false;
+        public bool AtMostOne = false;
+        public void TheOne()
+        {
+            this.AtLeastOne = true;
+            this.AtMostOne = true;
+        }
+        public Dictionary<string, object> Keys = new Dictionary<string, object>();
+        public Dictionary<string, object> Values = new Dictionary<string, object>();
+        public override object Execute(IDbTransaction trans)
+        {
+            return Data.Update(trans, this);
+        }
+        public UpdateRecord CloneUpdate()
+        {
+            var r = new UpdateRecord();
+            r.TableName = this.TableName;
+            r.AtLeastOne = this.AtLeastOne;
+            r.AtMostOne = this.AtMostOne;
+            foreach (var i in this.Keys) r.Keys[i.Key] = i.Value;
+            foreach (var i in this.Values) r.Values[i.Key] = i.Value;
+            return r;
+        }
+        public override TransactionRecord CloneRecord()
+        {
+            return this.CloneUpdate();
+        }
+    }
+    public class DeleteRecord : TransactionRecord
+    {
+        public DeleteRecord() : base() { }
+        public DeleteRecord(string tableName) : base(tableName) { }
+        public bool AtLeastOne = false;
+        public bool AtMostOne = false;
+        public void TheOne()
+        {
+            this.AtLeastOne = true;
+            this.AtMostOne = true;
+        }
+        public Dictionary<string, object> Keys = new Dictionary<string, object>();
+        public override object Execute(IDbTransaction trans)
+        {
+            return Data.Delete(trans, this);
+        }
+        public UpdateRecord CloneDelete()
+        {
+            var r = new UpdateRecord();
+            r.TableName = this.TableName;
+            r.AtLeastOne = this.AtLeastOne;
+            r.AtMostOne = this.AtMostOne;
+            foreach (var i in this.Keys) r.Keys[i.Key] = i.Value;
+            return r;
+        }
+        public override TransactionRecord CloneRecord()
+        {
+            return this.CloneDelete();
+        }
+    }
+    public interface IMetaBuilder<T>
+    {
+        void Table(string name);
+        void Key(string name, Func<T, object> getkey, Func<object, string> generator = null);
+        void Value(string name, Func<T, object> getvalue);
+    }
+    public static class MetaBuilderExtension
+    {
+        public static void Key<T>(this IMetaBuilder<T> m, string name, Func<T, object> getkey, string sequence, Func<object, bool> isDummy)
+        {
+            m.Key(name, getkey, (x) => (isDummy(x) ? sequence : null));
+        }
+        public static void IntKey<T>(this IMetaBuilder<T> m, string name, Func<T, int?> getkey, string sequence, Func<int, bool> isDummy)
+        {
+            m.Key(name, (x) => getkey(x), sequence, (x) => isDummy((int)x));
+        }
+        public static void IntKey<T>(this IMetaBuilder<T> m, string name, Func<T, int?> getkey, string sequence, int dummy = -1)
+        {
+            m.IntKey(name, getkey, sequence, (x) => x <= dummy);
+        }
+        public static void DecimalKey<T>(this IMetaBuilder<T> m, string name, Func<T, decimal?> getkey, string sequence, Func<decimal, bool> isDummy)
+        {
+            m.Key(name, (x) => getkey(x), sequence, (x) => isDummy((decimal)x));
+        }
+        public static void DecimalKey<T>(this IMetaBuilder<T> m, string name, Func<T, decimal?> getkey, string sequence, decimal dummy = -1)
+        {
+            m.DecimalKey(name, getkey, sequence, (x) => x <= dummy);
+        }
+        public static void StringKey<T>(this IMetaBuilder<T> m, string name, Func<T, object> getkey, string sequence, Func<string, bool> isDummy)
+        {
+            m.Key(name, getkey, sequence, (x) => isDummy(x as string));
+        }
+        public static void StringKey<T>(this IMetaBuilder<T> m, string name, Func<T, object> getkey, string sequence, string prefix = "new:")
+        {
+            m.StringKey(name, getkey, sequence, (s) => s.StartsWith(prefix));
+        }
+    }
+    public class Reconciler<T> : IMetaBuilder<T>
+    {
+        private string tableName;
+        private Dictionary<string, Func<T, object>> keys = new Dictionary<string, Func<T, object>>();
+        private List<string> keyList = new List<string>();
+        private Dictionary<string, Func<object, string>> generators = new Dictionary<string, Func<object, string>>();
+        private Dictionary<string, Func<T, object>> values = new Dictionary<string, Func<T, object>>();
+        private List<string> valueList = new List<string>();
+        public Reconciler()
+        {
+        }
+        public void Table(string name)
+        {
+            this.tableName = name;
+        }
+        public void Key(string name, Func<T, object> getkey, Func<object, string> generator = null)
+        {
+            if (this.keys.ContainsKey(name)) throw new Exception("Duplicate key name: " + name);
+            this.keys.Add(name, getkey);
+            this.keyList.Add(name);
+            if (generator != null) this.generators.Add(name, generator);
+        }
+        public void Value(string name, Func<T, object> getvalue)
+        {
+            if (this.values.ContainsKey(name)) throw new Exception("Duplicate value name: " + name);
+            this.values.Add(name, getvalue);
+            this.valueList.Add(name);
+        }
+        public object[] GetKey(T obj)
+        {
+            var keys = new object[this.keyList.Count];
+            for (int i = 0; i < this.keyList.Count; i++)
+                keys[i] = this.keys[this.keyList[i]](obj);
+            return keys;
+        }
+        public void ForEachKey(T obj, Action<string, object> action)
+        {
+            for (int i = 0; i < this.keyList.Count; i++)
+            {
+                action(this.keyList[i], this.keys[this.keyList[i]](obj));
+            }
+        }
+        public void ForEachValue(T obj, Action<string, object> action)
+        {
+            for (int i = 0; i < this.valueList.Count; i++)
+            {
+                action(this.valueList[i], this.values[this.valueList[i]](obj));
+            }
+        }
+        private void EmitNewValue(T obj, string name, object value, Action<string, object> action)
+        {
+            Func<object, string> g = null;
+            if (this.generators.TryGetValue(name, out g))
+            {
+                var seq = g(value);
+                if (!String.IsNullOrEmpty(seq))
+                {
+                    Func<string> f = () => seq;
+                    action(name, f);
+                    return;
+                }
+            }
+            action(name, value);
+        }
+        public void ForEachNewValue(T obj, Action<string, object> action)
+        {
+            var done = new HashSet<string>();
+            this.ForEachKey(obj, (k, v) =>
+            {
+                this.EmitNewValue(obj, k, v, action);
+                done.Add(k);
+            });
+            this.ForEachValue(obj, (k, v) =>
+            {
+                if (done.Contains(k)) return;
+                this.EmitNewValue(obj, k, v, action);
+            });
+        }
+        public void ForEachValueChange(T o, T n, Action<string, object, object> action)
+        {
+            for (int i = 0; i < this.valueList.Count; i++)
+            {
+                var ov = this.values[this.valueList[i]](o);
+                var nv = this.values[this.valueList[i]](n);
+                if (!EqualityComparer<object>.Default.Equals(ov, nv))
+                    action(this.valueList[i], ov, nv);
+            }
+        }
+        public IEnumerable<TransactionRecord> Reconcile(IEnumerable<T> olds, IEnumerable<T> news)
+        {
+            var oldLookup = new Dictionary<object[], T>(JonSkeet.ArrayEqualityComparer<object>.Default);
+            foreach (var o in olds) oldLookup.Add(this.GetKey(o), o);
+            var newLookup = new Dictionary<object[], T>(JonSkeet.ArrayEqualityComparer<object>.Default);
+            foreach (var n in news) newLookup.Add(this.GetKey(n), n);
+            if (oldLookup.Count != olds.Count()) throw new Exception("Duplicate keys");
+            if (newLookup.Count != news.Count()) throw new Exception("Duplicate keys");
+            foreach (var n in news)
+            {
+                var key = this.GetKey(n);
+                T o = default(T);
+                if (!oldLookup.TryGetValue(key, out o))
+                {
+                    var r = new InsertRecord(this.tableName);
+                    this.ForEachNewValue(n, (k, v) => r.Values[k] = v);
+                    if (r.Values.Any()) yield return r;
+                }
+                else
+                {
+                    var r = new UpdateRecord(this.tableName);
+                    r.AtMostOne = true;
+                    this.ForEachKey(n, (k, v) => r.Keys[k] = v);
+                    this.ForEachValueChange(o, n, (k, ov, nv) => r.Values[k] = nv);
+                    if (r.Keys.Any() && r.Values.Any()) yield return r;
+                }
+            }
+            foreach (var o in olds)
+            {
+                var key = this.GetKey(o);
+                T n = default(T);
+                if (!newLookup.TryGetValue(key, out n))
+                {
+                    var r = new DeleteRecord(this.tableName);
+                    r.AtMostOne = true;
+                    this.ForEachKey(o, (k, v) => r.Keys[k] = v);
+                    if (r.Keys.Any()) yield return r;
+                }
+            }
+        }
+    }
+    public delegate void Meta<T>(IMetaBuilder<T> m);
+    public class Item
+    {
+        public int? ItemId { get; set; }
+        public string ItemName { get; set; }
+        public decimal? Price { get; set; }
+        public static void Meta(IMetaBuilder<Item> m)
+        {
+            m.Table("ITEM");
+            m.DecimalKey("ITEM_ID", (x) => x.ItemId, "ITEM_S");
+            m.Value("ITEM_NAME", (x) => x.ItemName);
+            m.Value("ITEM_PRICE", (x) => x.Price);
+        }
+    }
+}

File Usually/JonSkeet.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Usually
+{
+    class JonSkeet
+    {
+        /* http://stackoverflow.com/questions/486749/compare-two-net-array-objects */
+        public static bool AreEqual<T>(T[] a, T[] b)
+        {
+            return AreEqual(a, b, EqualityComparer<T>.Default);
+        }
+        public static bool AreEqual<T>(T[] a, T[] b, IEqualityComparer<T> comparer)
+        {
+            if (a.Length != b.Length)
+            {
+                return false;
+            }
+            for (int i = 0; i < a.Length; i++)
+            {
+                if (!comparer.Equals(a[i], b[i]))
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /* http://stackoverflow.com/questions/4598368/compare-objects */
+        public static class ArrayEqualityComparer
+        {
+            public static IEqualityComparer<T[]> Create<T>(IEqualityComparer<T> comparer)
+            {
+                return new ArrayEqualityComparer<T>(comparer);
+            }
+        }
+        public sealed class ArrayEqualityComparer<T> : IEqualityComparer<T[]>
+        {
+            private static readonly IEqualityComparer<T[]> defaultInstance = new ArrayEqualityComparer<T>();
+            public static IEqualityComparer<T[]> Default
+            {
+                get { return defaultInstance; }
+            }
+            private readonly IEqualityComparer<T> elementComparer;
+            public ArrayEqualityComparer()
+                : this(EqualityComparer<T>.Default)
+            {
+            }
+            public ArrayEqualityComparer(IEqualityComparer<T> elementComparer)
+            {
+                this.elementComparer = elementComparer;
+            }
+            public bool Equals(T[] x, T[] y)
+            {
+                if (x == y)
+                {
+                    return true;
+                }
+                if (x == null || y == null)
+                {
+                    return false;
+                }
+                if (x.Length != y.Length)
+                {
+                    return false;
+                }
+                for (int i = 0; i < x.Length; i++)
+                {
+                    if (!elementComparer.Equals(x[i], y[i]))
+                    {
+                        return false;
+                    }
+                }
+                return true;
+            }
+            public int GetHashCode(T[] array)
+            {
+                if (array == null)
+                {
+                    return 0;
+                }
+                int hash = 23;
+                foreach (T item in array)
+                {
+                    hash = hash * 31 + elementComparer.GetHashCode(item);
+                }
+                return hash;
+            }
+        }
+    }
+}

File Usually/Properties/AssemblyInfo.cs

+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Usually")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Usually")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("47c3c07c-8138-41e8-af77-1b9d9498be81")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

File Usually/Use.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.IO;
+using System.Runtime.Serialization.Formatters.Binary;
+
+namespace Usually
+{
+    public class Use
+    {
+        public static bool IsNone(object obj)
+        {
+            if ((obj == null) || (obj == DBNull.Value)) return true;
+            return false;
+        }
+        public static bool IsNotNone(object obj)
+        {
+            return !IsNone(obj);
+        }
+        public static string DisplayText(object obj)
+        {
+            return (IsNone(obj) ? "" : obj.ToString());
+        }
+        public static string SerializeObject<T>(T objectToSerialize)
+        {
+            BinaryFormatter bf = new BinaryFormatter();
+            MemoryStream memStr = new MemoryStream();
+            try
+            {
+                bf.Serialize(memStr, objectToSerialize);
+                memStr.Position = 0;
+                return Convert.ToBase64String(memStr.ToArray());
+            }
+            finally
+            {
+                memStr.Close();
+            }
+        }
+        public static T DeserializeToObject<T>(string s) where T : class
+        {
+            BinaryFormatter bf = new BinaryFormatter();
+            MemoryStream memStr = new MemoryStream(Convert.FromBase64String(s));
+            try
+            {
+                memStr.Position = 0;
+                return bf.Deserialize(memStr) as T;
+            }
+            finally
+            {
+                memStr.Close();
+            }
+        }
+        public static T SerializeClone<T>(T obj) where T: class
+        {
+            if (obj == null) return null;
+            BinaryFormatter bf = new BinaryFormatter();
+            MemoryStream memStr = new MemoryStream();
+            try
+            {
+                bf.Serialize(memStr, obj);
+                memStr.Position = 0;
+                return bf.Deserialize(memStr) as T;
+            }
+            finally
+            {
+                memStr.Close();
+            }
+        }
+    }
+}

File Usually/Usually.csproj

+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{AB4B3A1B-3915-4198-96C3-A02B089E8878}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>Usually</RootNamespace>
+    <AssemblyName>Usually</AssemblyName>
+    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Data.cs" />
+    <Compile Include="JonSkeet.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Use.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>

File UsuallyTests/Program.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace UsuallyTests
+{
+    static class Program
+    {
+        /// <summary>
+        /// The main entry point for the application.
+        /// </summary>
+        [STAThread]
+        static void Main()
+        {
+            NUnit.Gui.AppEntry.Main(new string[] { "/run", "/noload", System.Reflection.Assembly.GetExecutingAssembly().Location });
+        }
+    }
+}

File UsuallyTests/Properties/AssemblyInfo.cs

+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following 
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("UsuallyTests")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("UsuallyTests")]
+[assembly: AssemblyCopyright("Copyright ©  2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible 
+// to COM components.  If you need to access a type in this assembly from 
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("7bfb757d-3640-4a0a-a617-ef14ac1ce557")]
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version 
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers 
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

File UsuallyTests/TestData.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using Usually;
+
+namespace UsuallyTests
+{
+    [TestFixture]
+    public class TestData
+    {
+        [Test]
+        public void TestEscapeSqlString()
+        {
+            Assert.AreEqual("", Data.EscapeSqlString(""));
+            Assert.AreEqual("x", Data.EscapeSqlString("x"));
+            Assert.AreEqual("That''s right", Data.EscapeSqlString("That's right"));
+            Assert.AreEqual("C:\\\\", Data.EscapeSqlString("C:\\"));
+        }
+        [Test]
+        public void TestCleanUserEntry()
+        {
+            Assert.AreEqual(null, Data.CleanUserEntry(null));
+            Assert.AreEqual(null, Data.CleanUserEntry(""));
+            Assert.AreEqual(null, Data.CleanUserEntry(" "));
+            Assert.AreEqual(0, Data.CleanUserEntry(0));
+            Assert.AreEqual(1, Data.CleanUserEntry(1));
+            Assert.AreEqual("Hello", Data.CleanUserEntry(" Hello"));
+            Assert.AreEqual("Hello", Data.CleanUserEntry("Hello "));
+            Assert.AreEqual("Hello", Data.CleanUserEntry(" Hello  "));
+        }
+        [Test]
+        public void TestReconcile()
+        {
+            var olds = new OrderLine[] {
+                new OrderLine { LineId=1, OrderId=101, ItemId=1001, ItemName="Thousand One", Quantity=1, Unit="Each" },
+                new OrderLine { LineId=2, OrderId=101, ItemId=1002, ItemName="Thousand Two", Quantity=3, Unit="Pair" },
+                new OrderLine { LineId=3, OrderId=101, ItemId=1003, ItemName="Thousand Three", Quantity=5, Unit="Box" },
+                new OrderLine { LineId=4, OrderId=101, ItemId=1004, ItemName="Thousand Four", Quantity=7, Unit="Dozen" },
+                new OrderLine { LineId=5, OrderId=101, ItemId=1005, ItemName="Thousand Five", Quantity=9, Unit="Metre" },
+                new OrderLine { LineId=6, OrderId=101, ItemId=1006, ItemName="Thousand Six", Quantity=6, Unit="Kg" },
+                new OrderLine { LineId=7, OrderId=101, ItemId=1007, ItemName="Thousand Seven", Quantity=4, Unit="Litre" },
+            }.ToList();
+            var news = Use.SerializeClone(olds);
+            Meta<OrderLine> meta = (m) =>
+            {
+                m.Table("ORDER_LINES");
+                m.IntKey("LINE_ID", (x) => x.LineId, "SEQ_ORDER_LINES");
+                m.Value("ORDER_ID", (x) => x.OrderId);
+                m.Value("ITEM_ID", (x) => x.ItemId);
+                m.Value("ITEM_NAME", (x) => x.ItemName);
+                m.Value("QUANTITY", (x) => x.Quantity);
+                m.Value("UNIT", (x) => x.Unit);
+            };
+            news[2 - 1].ItemId = 2002;
+            news[2 - 1].ItemName = "Two Oo Oo Two";
+            news[5 - 1].Quantity = null;
+            news[7 - 1].Unit = "Gallon";
+            news.RemoveAt(6 - 1);
+            news.RemoveAt(3 - 1);
+            Assert.AreEqual(5, news.Count);
+            news.Insert(2 - 1, new OrderLine { LineId = 11, OrderId = 101, ItemId = 2001, ItemName = "Two Oo Oo One", Quantity = 2, Unit = null });
+            news.Add(new OrderLine { LineId = -1, OrderId = 101, ItemId = 8008, ItemName = "Eight Thousand Eight", Quantity = 8, Unit = "Piece" });
+            news.Add(new OrderLine { LineId = 9, OrderId = 101, ItemId = 9009, ItemName = "Nine Thousand Nine", Quantity = 9, Unit = "Square Metre" });
+            Assert.AreEqual(8, news.Count);
+            var changes = Data.Reconcile(meta, olds, news);
+            Assert.AreEqual(8, changes.Count());
+            var e = changes.GetEnumerator();
+            Assert.True(e.MoveNext());
+            Assert.IsInstanceOf(typeof(InsertRecord), e.Current);
+            {
+                var t = e.Current as InsertRecord;
+                Assert.AreEqual("ORDER_LINES", t.TableName);
+                Assert.AreEqual(6, t.Values.Count);
+                Assert.AreEqual(11, t.Values["LINE_ID"]);
+                Assert.AreEqual(101, t.Values["ORDER_ID"]);
+                Assert.AreEqual(2001, t.Values["ITEM_ID"]);
+                Assert.AreEqual("Two Oo Oo One", t.Values["ITEM_NAME"]);
+                Assert.AreEqual(2, t.Values["QUANTITY"]);
+                Assert.AreEqual(null, t.Values["UNIT"]);
+            }
+            Assert.True(e.MoveNext());
+            Assert.IsInstanceOf(typeof(UpdateRecord), e.Current);
+            {
+                var t = e.Current as UpdateRecord;
+                Assert.True(t.AtMostOne);
+                Assert.AreEqual("ORDER_LINES", t.TableName);
+                Assert.AreEqual(1, t.Keys.Count);
+                Assert.AreEqual(2, t.Keys["LINE_ID"]);
+                Assert.AreEqual(2, t.Values.Count);
+                Assert.AreEqual(2002, t.Values["ITEM_ID"]);
+                Assert.AreEqual("Two Oo Oo Two", t.Values["ITEM_NAME"]);
+            }
+            Assert.True(e.MoveNext());
+            Assert.IsInstanceOf(typeof(UpdateRecord), e.Current);
+            {
+                var t = e.Current as UpdateRecord;
+                Assert.True(t.AtMostOne);
+                Assert.AreEqual("ORDER_LINES", t.TableName);
+                Assert.AreEqual(1, t.Keys.Count);
+                Assert.AreEqual(5, t.Keys["LINE_ID"]);
+                Assert.AreEqual(1, t.Values.Count);
+                Assert.AreEqual(null, t.Values["QUANTITY"]);
+            }
+            Assert.True(e.MoveNext());
+            Assert.IsInstanceOf(typeof(UpdateRecord), e.Current);
+            {
+                var t = e.Current as UpdateRecord;
+                Assert.True(t.AtMostOne);
+                Assert.AreEqual("ORDER_LINES", t.TableName);
+                Assert.AreEqual(1, t.Keys.Count);
+                Assert.AreEqual(7, t.Keys["LINE_ID"]);
+                Assert.AreEqual(1, t.Values.Count);
+                Assert.AreEqual("Gallon", t.Values["UNIT"]);
+            }
+            Assert.True(e.MoveNext());
+            Assert.IsInstanceOf(typeof(InsertRecord), e.Current);
+            {
+                var t = e.Current as InsertRecord;
+                Assert.AreEqual("ORDER_LINES", t.TableName);
+                Assert.AreEqual(6, t.Values.Count);
+                Assert.IsInstanceOf(typeof(Func<string>), t.Values["LINE_ID"]);
+                var f = t.Values["LINE_ID"] as Func<string>;
+                Assert.AreEqual("SEQ_ORDER_LINES", f());
+                Assert.AreEqual(101, t.Values["ORDER_ID"]);
+                Assert.AreEqual(8008, t.Values["ITEM_ID"]);
+                Assert.AreEqual("Eight Thousand Eight", t.Values["ITEM_NAME"]);
+                Assert.AreEqual(8, t.Values["QUANTITY"]);
+                Assert.AreEqual("Piece", t.Values["UNIT"]);
+            }
+            Assert.True(e.MoveNext());
+            Assert.IsInstanceOf(typeof(InsertRecord), e.Current);
+            {
+                var t = e.Current as InsertRecord;
+                Assert.AreEqual("ORDER_LINES", t.TableName);
+                Assert.AreEqual(6, t.Values.Count);
+                Assert.AreEqual(9, t.Values["LINE_ID"]);
+                Assert.AreEqual(101, t.Values["ORDER_ID"]);
+                Assert.AreEqual(9009, t.Values["ITEM_ID"]);
+                Assert.AreEqual("Nine Thousand Nine", t.Values["ITEM_NAME"]);
+                Assert.AreEqual(9, t.Values["QUANTITY"]);
+                Assert.AreEqual("Square Metre", t.Values["UNIT"]);
+            }
+            Assert.True(e.MoveNext());
+            Assert.IsInstanceOf(typeof(DeleteRecord), e.Current);
+            {
+                var t = e.Current as DeleteRecord;
+                Assert.True(t.AtMostOne);
+                Assert.AreEqual("ORDER_LINES", t.TableName);
+                Assert.AreEqual(1, t.Keys.Count);
+                Assert.AreEqual(3, t.Keys["LINE_ID"]);
+            }
+            Assert.True(e.MoveNext());
+            Assert.IsInstanceOf(typeof(DeleteRecord), e.Current);
+            {
+                var t = e.Current as DeleteRecord;
+                Assert.True(t.AtMostOne);
+                Assert.AreEqual("ORDER_LINES", t.TableName);
+                Assert.AreEqual(1, t.Keys.Count);
+                Assert.AreEqual(6, t.Keys["LINE_ID"]);
+            }
+            Assert.False(e.MoveNext());
+        }
+    }
+
+    [Serializable]
+    public class Order
+    {
+        public int? OrderId { get; set; }
+        public string OrderNumber { get; set; }
+        public int? CustomerId { get; set; }
+        public DateTime? OrderDate { get; set; }
+        public string Status { get; set; }
+    }
+
+    [Serializable]
+    public class OrderLine
+    {
+        public int? LineId { get; set; }
+        public int? OrderId { get; set; }
+        public int? ItemId { get; set; }
+        public string ItemName { get; set; }
+        public string Unit { get; set; }
+        public decimal? Quantity { get; set; }
+    }
+
+    [Serializable]
+    public class Shipment
+    {
+        public int? ShipmentId { get; set; }
+        public int? OrderLineId { get; set; }
+        public decimal? Quantity { get; set; }
+        public DateTime? ShipDate { get; set; }
+    }
+
+}

File UsuallyTests/TestUse.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using Usually;
+
+namespace UsuallyTests
+{
+    [TestFixture]
+    public class TestUse
+    {
+        [Test]
+        public void TestIsNone()
+        {
+            Assert.True(Use.IsNone(null));
+            Assert.True(Use.IsNone(DBNull.Value));
+            Assert.False(Use.IsNone(0));
+            Assert.False(Use.IsNone(1));
+            Assert.False(Use.IsNone(""));
+            Assert.False(Use.IsNone("x"));
+        }
+        [Test]
+        public void TestIsNotNone()
+        {
+            Assert.False(Use.IsNotNone(null));
+            Assert.False(Use.IsNotNone(DBNull.Value));
+            Assert.True(Use.IsNotNone(0));
+            Assert.True(Use.IsNotNone(1));
+            Assert.True(Use.IsNotNone(""));
+            Assert.True(Use.IsNotNone("x"));
+        }
+        [Test]
+        public void TestDisplayText()
+        {
+            Assert.AreEqual("", Use.DisplayText(null));
+            Assert.AreEqual("0", Use.DisplayText(0));
+            Assert.AreEqual("1", Use.DisplayText(1));
+            Assert.AreEqual("", Use.DisplayText(""));
+            Assert.AreEqual("x", Use.DisplayText("x"));
+        }
+        [Test]
+        public void TestSearializeAndDeserialize()
+        {
+            var source = Sample.CreateSamples();
+            var serializedText = Use.SerializeObject(source);
+            var target = Use.DeserializeToObject<List<Sample>>(serializedText);
+            Sample.AssertEqualButNotSame(source, target);
+        }
+        [Test]
+        public void TestSerializeClone()
+        {
+            var source = new List<Sample>();
+            var target = Use.SerializeClone(source);
+            Sample.AssertEqualButNotSame(source, target);
+            var sourceArray = source.ToArray();
+            var targetArray = Use.SerializeClone(sourceArray);
+            Sample.AssertEqualButNotSame(sourceArray, targetArray);
+        }
+    }
+    [Serializable]
+    class Sample
+    {
+        public int? Iden { get; set; }
+        public string Name { get; set; }
+        public decimal? Quantity { get; set; }
+        public static List<Sample> CreateSamples()
+        {
+            var samples = new List<Sample>();
+            samples.Add(new Sample { Iden = 1, Name = "One", Quantity = 100 });
+            samples.Add(new Sample { Iden = 2, Name = null, Quantity = 200 });
+            samples.Add(new Sample { Iden = 3, Name = "Three", Quantity = null });
+            samples.Add(new Sample { Iden = 4, Name = "Four", Quantity = 400 });
+            samples.Add(new Sample { Iden = null, Name = "Null", Quantity = 500 });
+            return samples;
+        }
+        public static void AssertEqualButNotSame(IEnumerable<Sample> a, IEnumerable<Sample> b)
+        {
+            Assert.AreNotSame(a, b);
+            var arrayA = a.ToArray();
+            var arrayB = b.ToArray();
+            Assert.AreEqual(arrayA.Length, arrayB.Length);
+            for (int i = 0; i < arrayA.Length; i++)
+            {
+                Assert.AreNotSame(arrayA[i], arrayB[i]);
+                Assert.AreEqual(arrayA[i].Iden, arrayB[i].Iden);
+                Assert.AreEqual(arrayA[i].Name, arrayB[i].Name);
+                Assert.AreEqual(arrayA[i].Quantity, arrayB[i].Quantity);
+            }
+        }
+    }
+}

File UsuallyTests/UsuallyTests.csproj

+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>8.0.30703</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{082C43E4-ADAB-49B0-B103-B9FB76659734}</ProjectGuid>
+    <OutputType>WinExe</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>UsuallyTests</RootNamespace>
+    <AssemblyName>UsuallyTests</AssemblyName>
+    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup>
+    <StartupObject />
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="nunit-gui-runner">
+      <HintPath>nunit-dlls\nunit-gui-runner.dll</HintPath>
+    </Reference>
+    <Reference Include="nunit.framework, Version=2.6.0.12051, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>nunit-dlls\nunit.framework.dll</HintPath>
+    </Reference>
+    <Reference Include="System" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Program.cs" />
+    <Compile Include="TestData.cs" />
+    <Compile Include="TestUse.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\Usually\Usually.csproj">
+      <Project>{AB4B3A1B-3915-4198-96C3-A02B089E8878}</Project>
+      <Name>Usually</Name>
+    </ProjectReference>
+  </ItemGroup>
+  <ItemGroup>
+    <Folder Include="nunit-dlls\" />
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
+       Other similar extension points exist, see Microsoft.Common.targets.
+  <Target Name="BeforeBuild">
+  </Target>
+  <Target Name="AfterBuild">
+  </Target>
+  -->
+</Project>