Commits

Anonymous committed 5d31866

Add ExtraKey & many refactoring

  • Participants
  • Parent commits 6a7f0ce

Comments (0)

Files changed (7)

File Usually/Data.cs

 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Text;
 using System.Data;
 
 namespace Usually
             }
             return userEntry;
         }
-        public static T AddNewWithIntegerKey<T>(List<T> list, Func<T, int> getter, Action<T, int> setter, int dummy = -1) where T: new()
+        public static T AddNewWithIntegerKey<T>(List<T> list, Func<T, int> getter, Action<T, int> setter, int dummy = Resolver.DEFAULT_DUMMY_START) where T: new()
         {
             var next = list.Any()
                 ? Math.Min(dummy, (from t in list select getter(t)).Min() - 1)
             list.Add(r);
             return r;
         }
-        public static T AddNewWithDecimalKey<T>(List<T> list, Func<T, int> getter, Action<T, int> setter, int dummy = -1) where T : new()
+        public static T AddNewWithDecimalKey<T>(List<T> list, Func<T, int> getter, Action<T, int> setter, int dummy = Resolver.DEFAULT_DUMMY_START) where T : new()
         {
             var next = list.Any()
                 ? Math.Min(dummy, (from t in list select getter(t)).Min() - 1)
         {
             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)
+        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())
         {
             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);
+            bool Generate(IDbTransaction trans, string expr, out object value);
         }
         private static List<IDataDialect> dialects = new List<IDataDialect>();
         public static void AddDialect(IDataDialect dialect)
             p.Value = value;
             cmd.Parameters.Add(p);
         }
-        public static object Generate(IDbTransaction trans, string name)
+        public static object Generate(IDbTransaction trans, string expr)
         {
             foreach (var d in dialects)
             {
                 object value = null;
-                if (d.Generate(trans, name, out value)) return value;
+                if (d.Generate(trans, expr, out value)) return value;
             }
-            throw new Exception("Cannot generate: " + name);
+            throw new Exception("Cannot generate: " + expr);
         }
         public static Dictionary<string, object> Insert(IDbTransaction trans, InsertRecord ins)
         {
             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)
+                var g = i.Value as GeneratingValue;
+                if (g != null)
                 {
                     if (r == null) r = new Dictionary<string, object>();
-                    r.Add(i.Key, Generate(trans, f()));
+                    r.Add(i.Key, Generate(trans, g.Expression));
                 }
             }
             using (var cmd = CreateCommand(trans))
             }
             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 interface IResolverItem
-    {
-    }
-    public class ResolverItem<T>: IResolverItem
-    {
-        public ResolverItem(Meta<T> meta, IEnumerable<T> olds, IEnumerable<T> news)
-        {
-        }
-        public ResolverItem<TT> Add<TT>(Meta<TT> meta, Func<T, object> masterKey, Func<TT, object> detailKey, IEnumerable<TT> olds, IEnumerable<TT> news)
-        {
-            return new ResolverItem<TT>(meta, olds, news);
-        }
-    }
-    public class Resolver
-    {
-        private List<IResolverItem> items = new List<IResolverItem>();
-        public Resolver()
-        {
-        }
-        public ResolverItem<T> Add<T>(Meta<T> meta, IEnumerable<T> olds, IEnumerable<T> news)
-        {
-            return new ResolverItem<T>(meta, olds, 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);
 }

File Usually/Resolve.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Usually
+{
+    public sealed class GeneratingValue
+    {
+        public string _expression = null;
+        public int _id = -1;
+        public GeneratingValue(string expr, int id) 
+        { 
+            this._expression = expr;
+            this._id = id;
+        }
+        public string Expression { get { return this._expression; } }
+        public int Id { get { return this._id; } }
+        public GeneratedValue CreateRef()
+        {
+            return new GeneratedValue(this._id);
+        }
+    }
+    public sealed class GeneratedValue
+    {
+        private int _key;
+        public GeneratedValue(int key)
+        {
+            this._key = key;
+        }
+        public int Key { get { return this._key; } }
+    }
+    public interface ITransactor
+    {
+        void Insert(InsertRecord ins);
+        void Update(UpdateRecord upd);
+        void Delete(DeleteRecord del);
+    }
+    public abstract class TransactionRecord
+    {
+        public string TableName = null;
+        protected TransactionRecord() : this(null) { }
+        protected TransactionRecord(string tableName)
+        {
+            this.TableName = @tableName;
+        }
+        public abstract TransactionRecord CloneRecord();
+        public abstract void Execute(ITransactor actor);
+    }
+    public sealed class InsertRecord : TransactionRecord
+    {
+        public InsertRecord() : base() { }
+        public InsertRecord(string tableName) : base(tableName) { }
+        public Dictionary<string, object> Values = new Dictionary<string, object>();
+        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 override void Execute(ITransactor actor)
+        {
+            actor.Insert(this);
+        }
+    }
+    public sealed 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 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 override void Execute(ITransactor actor)
+        {
+            actor.Update(this);
+        }
+    }
+    public sealed 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 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 override void Execute(ITransactor actor)
+        {
+            actor.Delete(this);
+        }
+    }
+    public interface IKeyBuilder<T>
+    {
+        void Key(string name, Func<T, object> getKey, Func<object, string> generator = null);
+    }
+    public interface IMetaBuilder<T>: IKeyBuilder<T>
+    {
+        void Table(string name);
+        void Value(string name, Func<T, object> getValue);
+        IKeyBuilder<T> ExtraKey(string group);
+    }
+    public interface IRelationBuilder<Master, Detail>
+    {
+        void Link(Func<Master, object> masterKey, Func<Detail, object> detalKey);
+    }
+    public delegate void RelMeta<Master, Detail>(IRelationBuilder<Master, Detail> r);
+    public static class MetaBuilderExtension
+    {
+        public static void Key<T>(this IKeyBuilder<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 IntegerKey<T>(this IKeyBuilder<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 IntegerKey<T>(this IKeyBuilder<T> m, string name, Func<T, int?> getKey, string sequence, int dummy = Resolver.DEFAULT_DUMMY_START)
+        {
+            m.IntegerKey(name, getKey, sequence, (x) => x <= dummy);
+        }
+        public static void DecimalKey<T>(this IKeyBuilder<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 IKeyBuilder<T> m, string name, Func<T, decimal?> getKey, string sequence, decimal dummy = Resolver.DEFAULT_DUMMY_START)
+        {
+            m.DecimalKey(name, getKey, sequence, (x) => x <= dummy);
+        }
+        public static void StringKey<T>(this IKeyBuilder<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 IKeyBuilder<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>
+    {
+        class ColumnsDef
+        {
+            private List<string> names = new List<string>();
+            private Dictionary<string, Func<T, object>> getters = new Dictionary<string, Func<T, object>>();
+            protected void Add(string name, Func<T, object> getter)
+            {
+                if (this.getters.ContainsKey(name)) throw new Exception("Duplicate name: " + name);
+                this.getters.Add(name, getter);
+                this.names.Add(name);
+            }
+            public void ForEach(T obj, Action<string, object> action)
+            {
+                for (int i = 0; i < this.names.Count; i++)
+                    action(this.names[i], this.getters[this.names[i]](obj));
+            }
+            public void ForEachChange(T o, T n, Action<string, object, object> action)
+            {
+                for (int i = 0; i < this.names.Count; i++)
+                {
+                    var ov = this.getters[this.names[i]](o);
+                    var nv = this.getters[this.names[i]](n);
+                    if (!EqualityComparer<object>.Default.Equals(ov, nv))
+                        action(this.names[i], ov, nv);
+                }
+            }
+            public object[] GetFrom(T obj)
+            {
+                var values = new object[this.names.Count];
+                for (int i = 0; i < this.names.Count; i++)
+                    values[i] = this.getters[this.names[i]](obj);
+                return values;
+            }
+        }
+        class KeysDef: ColumnsDef, IKeyBuilder<T>
+        {
+            private Dictionary<string, Func<object, string>> generators = new Dictionary<string, Func<object, string>>();
+            public void Add(string name, Func<T, object> getKey, Func<object, string> generator = null)
+            {
+                base.Add(name, getKey);
+                if (generator != null) this.generators.Add(name, generator);
+            }
+            void IKeyBuilder<T>.Key(string name, Func<T, object> getKey, Func<object, string> generator)
+            {
+                this.Add(name, getKey, generator);
+            }
+            public string GeneratingExpr(string name, object value)
+            {
+                Func<object, string> g = null;
+                if (! this.generators.TryGetValue(name, out g)) return null;
+                return g(value);
+            }
+        }
+        class ValuesDef : ColumnsDef
+        {
+            public new void Add(string name, Func<T, object> getValue)
+            {
+                base.Add(name, getValue);
+            }
+        }
+        private int nextGenId = 1;
+        private string tableName;
+        private KeysDef keys = new KeysDef();
+        private ValuesDef values = new ValuesDef();
+        private Dictionary<string, KeysDef> extraKeys = new Dictionary<string, KeysDef>();
+        public void Table(string name) { this.tableName = name; }
+        public void Key(string name, Func<T, object> getKey, Func<object, string> generator = null)
+        {
+            this.keys.Add(name, getKey, generator);
+        }
+        public IKeyBuilder<T> ExtraKey(string group)
+        {
+            KeysDef r = null;
+            if (this.extraKeys.TryGetValue(group, out r)) return r;
+            r = new KeysDef();
+            this.extraKeys.Add(group, r);
+            return r;
+        }
+        public void Value(string name, Func<T, object> getValue)
+        {
+            this.values.Add(name, getValue);
+        }
+        public object[] GetKey(T obj)
+        {
+            return this.keys.GetFrom(obj);
+        }
+        private GeneratingValue NewGeneratingValue(string expr)
+        {
+            var r = new GeneratingValue(expr, this.nextGenId);
+            this.nextGenId++;
+            return r;
+        }
+        public void ForEachInsertValue(T obj, Action<string, object> action)
+        {
+            var done = new HashSet<string>();
+            Action<KeysDef, string, object> addKey = (keys, k, v) =>
+            {
+                if (!done.Contains(k))
+                {
+                    var expr = keys.GeneratingExpr(k, v);
+                    action(k, string.IsNullOrEmpty(expr) ? v : this.NewGeneratingValue(expr));
+                    done.Add(k);
+                }
+            };
+            this.keys.ForEach(obj, (k, v) => addKey(this.keys, k, v));
+            foreach (var keys in this.extraKeys.Values) 
+                keys.ForEach(obj, (k, v) => addKey(keys, k, v));
+            this.values.ForEach(obj, (k, v) =>
+            {
+                if (!done.Contains(k)) action(k, v);
+            });
+        }
+        public void BuildLookups(IEnumerable<T> olds, IEnumerable<T> news, ref Dictionary<object[], T> oldLookup, ref Dictionary<object[], T> newLookup)
+        {
+            if (oldLookup == null)
+            {
+                oldLookup = new Dictionary<object[], T>(JonSkeet.ArrayEqualityComparer<object>.Default);
+                foreach (var o in olds) oldLookup.Add(this.GetKey(o), o);
+                if (oldLookup.Count != olds.Count()) throw new Exception("Duplicate keys");
+            }
+            if (newLookup == null)
+            {
+                newLookup = new Dictionary<object[], T>(JonSkeet.ArrayEqualityComparer<object>.Default);
+                foreach (var n in news) newLookup.Add(this.GetKey(n), n);
+                if (newLookup.Count != news.Count()) throw new Exception("Duplicate keys");
+            }
+        }
+        public IEnumerable<TransactionRecord> ReconcileInsert(IEnumerable<T> olds, IEnumerable<T> news, Dictionary<object[], T> oldLookup, Dictionary<object[], T> newLookup)
+        {
+            this.BuildLookups(olds, news, ref oldLookup, ref newLookup);
+            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.ForEachInsertValue(n, (k, v) => r.Values[k] = v);
+                    if (r.Values.Any()) yield return r;
+                }
+            }
+        }
+        public IEnumerable<TransactionRecord> ReconcileUpdateAndDelete(IEnumerable<T> olds, IEnumerable<T> news, Dictionary<object[], T> oldLookup, Dictionary<object[], T> newLookup)
+        {
+            this.BuildLookups(olds, news, ref oldLookup, ref newLookup);
+            foreach (var o in olds)
+            {
+                var key = this.GetKey(o);
+                T n = default(T);
+                if (newLookup.TryGetValue(key, out n))
+                {
+                    var r = new UpdateRecord(this.tableName);
+                    r.AtMostOne = true;
+                    this.keys.ForEach(n, (k, v) => r.Keys[k] = v);
+                    this.values.ForEachChange(o, n, (k, ov, nv) => r.Values[k] = nv);
+                    if (r.Keys.Any() && r.Values.Any()) yield return r;
+                }
+                else
+                {
+                    var r = new DeleteRecord(this.tableName);
+                    r.AtMostOne = true;
+                    this.keys.ForEach(o, (k, v) => r.Keys[k] = v);
+                    if (r.Keys.Any()) yield return r;
+                }
+            }
+        }
+
+        public IEnumerable<TransactionRecord> Reconcile(IEnumerable<T> olds, IEnumerable<T> news)
+        {
+            Dictionary<object[], T> oldLookup = null;
+            Dictionary<object[], T> newLookup = null;
+            this.BuildLookups(olds, news, ref oldLookup, ref newLookup);
+            foreach (var t in this.ReconcileInsert(olds, news, oldLookup, newLookup)) yield return t;
+            foreach (var t in this.ReconcileUpdateAndDelete(olds, news, oldLookup, newLookup)) yield return t;
+        }
+    }
+    public delegate void Meta<T>(IMetaBuilder<T> m);
+    public class Resolver
+    {
+        public const int DEFAULT_DUMMY_START = -1;
+        private List<IResolverItem> items = new List<IResolverItem>();
+        public ResolverItem<T> Add<T>(Meta<T> meta, IEnumerable<T> olds, IEnumerable<T> news)
+        {
+            var r = new ResolverItem<T>(meta, null, null, olds, news);
+            this.items.Add(r);
+            return r;
+        }
+        public IEnumerable<TransactionRecord> Resolve()
+        {
+            foreach (var i in this.items)
+                foreach (var r in i.Resolve())
+                    yield return r;
+        }
+        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 interface IResolverItem
+    {
+        IEnumerable<TransactionRecord> Resolve();
+    }
+    public class ResolverItem<T> : IResolverItem
+    {
+        private List<IResolverItem> items = new List<IResolverItem>();
+        private Meta<T> meta = null;
+        private string group = null;
+        private Func<T, object> getRef = null;
+        private IEnumerable<T> olds = null;
+        private IEnumerable<T> news = null;
+        public ResolverItem(Meta<T> meta, string group, Func<T, object> getRef, IEnumerable<T> olds, IEnumerable<T> news)
+        {
+            this.meta = @meta;
+            this.group = @group;
+            this.getRef = @getRef;
+            this.olds = @olds;
+            this.news = @news;
+        }
+        public ResolverItem<D> Add<D>(Meta<D> meta, Func<D, object> getRef, IEnumerable<D> olds, IEnumerable<D> news)
+        {
+            return this.Add(meta, null, getRef, olds, news);
+        }
+        public ResolverItem<D> Add<D>(Meta<D> meta, string group, Func<D, object> getRef, IEnumerable<D> olds, IEnumerable<D> news)
+        {
+            var i = new ResolverItem<D>(meta, group, getRef, olds, news);
+            this.items.Add(i);
+            return i;
+        }
+        public IEnumerable<TransactionRecord> Resolve()
+        {
+            return null;
+        }
+    }
+}

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;
 

File Usually/Usually.csproj

     <Compile Include="Data.cs" />
     <Compile Include="JonSkeet.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Resolve.cs" />
     <Compile Include="Use.cs" />
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

File UsuallyTests/TestData.cs

         [Test]
         public void TestAddNewIntegerKey()
         {
-            var list = new List<Order>();
-            var o1 = Data.AddNewWithIntegerKey(list, (o) => o.OrderId.Value, (o, v) => o.OrderId = v);
+            var list = new List<Item>();
+            var o1 = Data.AddNewWithIntegerKey(list, (o) => o.ItemId.Value, (o, v) => o.ItemId = v);
             Assert.AreEqual(1, list.Count);
-            Assert.AreEqual(-1, o1.OrderId);
-            var o2 = Data.AddNewWithIntegerKey(list, (o) => o.OrderId.Value, (o, v) => o.OrderId = v);
+            Assert.AreEqual(-1, o1.ItemId);
+            var o2 = Data.AddNewWithIntegerKey(list, (o) => o.ItemId.Value, (o, v) => o.ItemId = v);
             Assert.AreEqual(2, list.Count);
-            Assert.AreEqual(-2, o2.OrderId);
-            var o3 = Data.AddNewWithIntegerKey(list, (o) => o.OrderId.Value, (o, v) => o.OrderId = v);
+            Assert.AreEqual(-2, o2.ItemId);
+            var o3 = Data.AddNewWithIntegerKey(list, (o) => o.ItemId.Value, (o, v) => o.ItemId = v);
             Assert.AreEqual(3, list.Count);
-            Assert.AreEqual(-3, o3.OrderId);
-        }
-        [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);
-            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(OrderLine.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());
-        }
-        [Test]
-        public void TestResolver()
-        {
-            var r = new Resolver();
-            var orders = new List<Order>();
-            var lines = new List<OrderLine>();
-            var order = r.Add(Order.Meta, orders, orders);
-            var line = order.Add(OrderLine.Meta, (x) => x.OrderId, (x) => x.OrderId, lines, lines);
+            Assert.AreEqual(-3, o3.ItemId);
         }
     }
-
     [Serializable]
-    public class Order
+    public class Item
     {
-        public int? OrderId { get; set; }
-        public string OrderNumber { get; set; }
-        public int? CustomerId { get; set; }
-        public DateTime? OrderDate { get; set; }
+        public int? ItemId { get; set; }
+        public string ItemNumber { get; set; }
+        public string ItemName { get; set; }
         public string Status { get; set; }
-        public static void Meta(IMetaBuilder<Order> m)
-        {
-            m.Table("ORDERS");
-            m.IntKey("ORDER_ID", (x) => x.OrderId, "SEQ_ORDERS");
-            m.Value("ORDER_NUMBER", (x) => x.OrderNumber);
-            m.Value("CUSTOMER_ID", (x) => x.CustomerId);
-            m.Value("ORDER_DATE", (x) => x.OrderDate);
-            m.Value("STATUS", (x) => x.Status);
-        }
     }
-
-    [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; }
-        public static void Meta(IMetaBuilder<OrderLine> 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);
-        }
-    }
-
-    [Serializable]
-    public class Shipment
-    {
-        public int? ShipmentId { get; set; }
-        public int? OrderLineId { get; set; }
-        public decimal? Quantity { get; set; }
-        public DateTime? ShipDate { get; set; }
-        public static void Meta(IMetaBuilder<Shipment> m)
-        {
-            m.Table("ORDER_SHIPMENTS");
-            m.IntKey("SHIPMENT_ID", (x) => x.ShipmentId, "SEQ_ORDER_SHIPMENTS");
-            m.Value("ORDER_LINE_ID", (x) => x.OrderLineId);
-            m.Value("QUANTITY", (x) => x.Quantity);
-            m.Value("SHIP_DATE", (x) => x.ShipDate);
-        }
-    }
-
 }

File UsuallyTests/TestResolve.cs

+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using Usually;
+
+namespace UsuallyTests
+{
+    [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; }
+        public static void Meta(IMetaBuilder<Order> m)
+        {
+            m.Table("ORDERS");
+            m.IntegerKey("ORDER_ID", (x) => x.OrderId, "SEQ_ORDERS");
+            m.Value("ORDER_NUMBER", (x) => x.OrderNumber);
+            m.Value("CUSTOMER_ID", (x) => x.CustomerId);
+            m.Value("ORDER_DATE", (x) => x.OrderDate);
+            m.Value("STATUS", (x) => x.Status);
+        }
+    }
+
+    [Serializable]
+    public class OrderLine
+    {
+        public int? LineId { get; set; }
+        public decimal? UniqueId { 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; }
+        public static void Meta(IMetaBuilder<OrderLine> m)
+        {
+            m.Table("ORDER_LINES");
+            m.IntegerKey("LINE_ID", (x) => x.LineId, "SEQ_ORDER_LINES");
+            m.ExtraKey("x").DecimalKey("LINE_UNIQUE_ID", (x) => x.UniqueId, "SEQ_ORDER_LINES_UNQ");
+            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);
+        }
+    }
+
+    [Serializable]
+    public class Shipment
+    {
+        public int? ShipmentId { get; set; }
+        public int? OrderLineId { get; set; }
+        public decimal? Quantity { get; set; }
+        public DateTime? ShipDate { get; set; }
+        public static void Meta(IMetaBuilder<Shipment> m)
+        {
+            m.Table("ORDER_SHIPMENTS");
+            m.IntegerKey("SHIPMENT_ID", (x) => x.ShipmentId, "SEQ_ORDER_SHIPMENTS");
+            m.Value("ORDER_LINE_ID", (x) => x.OrderLineId);
+            m.Value("QUANTITY", (x) => x.Quantity);
+            m.Value("SHIP_DATE", (x) => x.ShipDate);
+        }
+    }
+
+    class TestResolve
+    {
+        [Test]
+        public void TestReconcile()
+        {
+            var olds = new OrderLine[] {
+                new OrderLine { LineId=1, UniqueId=100, OrderId=101, ItemId=1001, ItemName="Thousand One", Quantity=1, Unit="Each" },
+                new OrderLine { LineId=2, UniqueId=200, OrderId=101, ItemId=1002, ItemName="Thousand Two", Quantity=3, Unit="Pair" },
+                new OrderLine { LineId=3, UniqueId=300, OrderId=101, ItemId=1003, ItemName="Thousand Three", Quantity=5, Unit="Box" },
+                new OrderLine { LineId=4, UniqueId=400, OrderId=101, ItemId=1004, ItemName="Thousand Four", Quantity=7, Unit="Dozen" },
+                new OrderLine { LineId=5, UniqueId=500, OrderId=101, ItemId=1005, ItemName="Thousand Five", Quantity=9, Unit="Metre" },
+                new OrderLine { LineId=6, UniqueId=600, OrderId=101, ItemId=1006, ItemName="Thousand Six", Quantity=6, Unit="Kg" },
+                new OrderLine { LineId=7, UniqueId=700, OrderId=101, ItemId=1007, ItemName="Thousand Seven", Quantity=4, Unit="Litre" },
+            }.ToList();
+            var news = Use.SerializeClone(olds);
+            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, UniqueId=800, OrderId = 101, ItemId = 2001, ItemName = "Two Oo Oo One", Quantity = 2, Unit = null });
+            news.Add(new OrderLine { LineId = -1, UniqueId=900, OrderId = 101, ItemId = 8008, ItemName = "Eight Thousand Eight", Quantity = 8, Unit = "Piece" });
+            news.Add(new OrderLine { LineId = 9, UniqueId=-7, OrderId = 101, ItemId = 9009, ItemName = "Nine Thousand Nine", Quantity = 9, Unit = "Square Metre" });
+            Assert.AreEqual(8, news.Count);
+            var changes = Resolver.Reconcile(OrderLine.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(7, t.Values.Count);
+                Assert.AreEqual(11, t.Values["LINE_ID"]);
+                Assert.AreEqual(800, t.Values["LINE_UNIQUE_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(InsertRecord), e.Current);
+            {
+                var t = e.Current as InsertRecord;
+                Assert.AreEqual("ORDER_LINES", t.TableName);
+                Assert.AreEqual(7, t.Values.Count);
+                Assert.IsInstanceOf(typeof(GeneratingValue), t.Values["LINE_ID"]);
+                var g = t.Values["LINE_ID"] as GeneratingValue;
+                Assert.AreEqual("SEQ_ORDER_LINES", g.Expression);
+                Assert.AreEqual(900, t.Values["LINE_UNIQUE_ID"]);
+                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(7, t.Values.Count);
+                Assert.AreEqual(9, t.Values["LINE_ID"]);
+                Assert.IsInstanceOf(typeof(GeneratingValue), t.Values["LINE_UNIQUE_ID"]);
+                var g = t.Values["LINE_UNIQUE_ID"] as GeneratingValue;
+                Assert.AreEqual("SEQ_ORDER_LINES_UNQ", g.Expression);
+                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(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(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(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(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.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.False(e.MoveNext());
+        }
+        [Test]
+        public void TestResolverObject()
+        {
+            var r = new Resolver();
+            var orders = new List<Order>();
+            var lines = new List<OrderLine>();
+            var order = r.Add(Order.Meta, orders, orders);            
+            var line = order.Add(OrderLine.Meta, (ln) => ln.OrderId, lines, lines);
+        }
+    }
+}

File UsuallyTests/UsuallyTests.csproj

   <ItemGroup>
     <Compile Include="Program.cs" />
     <Compile Include="TestData.cs" />
+    <Compile Include="TestResolve.cs" />
     <Compile Include="TestUse.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
   </ItemGroup>