diff --git a/CodeOnlyStoredProcedure/PropertyInfoExtensions.cs b/CodeOnlyStoredProcedure/PropertyInfoExtensions.cs index 8bd5d6d..7de4e61 100644 --- a/CodeOnlyStoredProcedure/PropertyInfoExtensions.cs +++ b/CodeOnlyStoredProcedure/PropertyInfoExtensions.cs @@ -1,5 +1,7 @@ -using System.ComponentModel.DataAnnotations.Schema; +using System; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace CodeOnlyStoredProcedure @@ -29,5 +31,11 @@ public static string GetSqlColumnName(this PropertyInfo pi) return pi.Name; } + + public static Func CompileGetter(this PropertyInfo prop) + { + var i = Expression.Parameter(typeof(TItem), "i"); + return Expression.Lambda>(Expression.Property(i, prop), i).Compile(); + } } } diff --git a/CodeOnlyStoredProcedure/RowFactory/HierarchicalTypeRowFactory.cs b/CodeOnlyStoredProcedure/RowFactory/HierarchicalTypeRowFactory.cs index 5bcb286..48a7ac3 100644 --- a/CodeOnlyStoredProcedure/RowFactory/HierarchicalTypeRowFactory.cs +++ b/CodeOnlyStoredProcedure/RowFactory/HierarchicalTypeRowFactory.cs @@ -18,30 +18,25 @@ namespace CodeOnlyStoredProcedure.RowFactory { internal class HierarchicalTypeRowFactory : RowFactory { + private static readonly MethodInfo toArray = typeof(Enumerable).GetMethod(nameof(Enumerable.ToArray)); + private static readonly MethodInfo toList = typeof(Enumerable).GetMethod(nameof(Enumerable.ToList)); private static readonly IRowFactory mainResultFactory = RowFactory.Create(false); private static readonly IEnumerable rowInfos; + private static readonly Type resultType; private readonly ReadOnlyCollection resultTypesInOrder; static HierarchicalTypeRowFactory() { object falseObj = false; + if (!GlobalSettings.Instance.InterfaceMap.TryGetValue(typeof(T), out resultType)) + resultType = typeof(T); + var infos = new List(); var types = new Queue>(); var added = new HashSet(); + var info = Activator.CreateInstance(typeof(RootHierarchicalRowInfo<>).MakeGenericType(typeof(T), resultType)) as HierarchicalRowInfo; - var whereMethod = typeof(Enumerable).GetMethods() - .Where(mi => mi.Name == "Where" && - mi.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>)) - .Single(); - var cast = typeof(Enumerable).GetMethod("Cast"); - var toArray = typeof(Enumerable).GetMethod("ToArray"); - var toList = typeof(Enumerable).GetMethod("ToList"); - - var info = new HierarchicalRowInfo( - childType: typeof(T), - rowFactory: RowFactory.Create(false) - ); types.Enqueue(Tuple.Create(typeof(T), info)); infos.Add(info); @@ -54,14 +49,15 @@ static HierarchicalTypeRowFactory() if (!added.Add(t)) continue; - Type implType; IEnumerable interfaceProperties = null; + Type implType; if (GlobalSettings.Instance.InterfaceMap.TryGetValue(t, out implType)) { interfaceProperties = t.GetMappedProperties(); t = implType; } + // get all the properties, not just the writeable ones, that way the key property can be a calculated key var props = t.GetMappedProperties(); var key = GetKeyProperty(t, props, interfaceProperties); @@ -85,54 +81,8 @@ static HierarchicalTypeRowFactory() else if (fk.PropertyType != key.PropertyType) throw new NotSupportedException("Key types are not matched for " + implType.Name + ". Key on parent type: " + key.PropertyType + ".\nForeign key type on child: " + fk.PropertyType); - var childEnumerable = typeof(IEnumerable<>).MakeGenericType(childType); - var parent = Expression.Parameter(t); - var possible = Expression.Parameter(childEnumerable); - var keyExpr = Expression.Property(parent, key); - - Expression children; - if (implType == childType) - { - // possible.Where(c => c.ParentKey == key) - var c = Expression.Parameter(childType); - var funcType = typeof(Func<,>).MakeGenericType(childType, typeof(bool)); - children = Expression.Call(whereMethod.MakeGenericMethod(childType), - possible, - Expression.Lambda(funcType, Expression.Equal(keyExpr, Expression.Property(c, fk)), c)); - } - else - { - // possible.Cast().Where(c => c.ParentKey == key) - var c = Expression.Parameter(implType); - var funcType = typeof(Func<,>).MakeGenericType(implType, typeof(bool)); - children = Expression.Call(cast.MakeGenericMethod(implType), possible); - children = Expression.Call(whereMethod.MakeGenericMethod(implType), - children, - Expression.Lambda(funcType, Expression.Equal(keyExpr, Expression.Property(c, fk)), c)); - } - - if (child.PropertyType.IsArray) // .ToArray() - children = Expression.Call(toArray.MakeGenericMethod(childType), children); - else // .ToList() - children = Expression.Call(toList.MakeGenericMethod(childType), children); - - var assign = Expression.Assign(Expression.Property(parent, child), children); - - - var childInfo = new HierarchicalRowInfo - ( - parent: info, - parentType: t, - childType: implType, - childKeyColumnName: key.GetSqlColumnName(), - parentKeyColumnName: foreignKeyName, - isOptional: child.IsOptional(), - isArray: child.PropertyType.IsArray, - assigner: Expression.Lambda(assign, parent, possible).Compile(), - rowFactory: typeof(RowFactory<>).MakeGenericType(implType) - .GetMethod("Create", BindingFlags.Public | BindingFlags.Static) - .Invoke(null, new[] { falseObj }) as IRowFactory - ); + var hriType = typeof(HierarchicalRowInfo<,,>).MakeGenericType(typeof(T), t, implType, key.PropertyType); + var childInfo = Activator.CreateInstance(hriType, key, fk, child, info) as HierarchicalRowInfo; types.Enqueue(Tuple.Create(childType, childInfo)); infos.Add(childInfo); } @@ -216,11 +166,9 @@ public override IEnumerable ParseRows(IDataReader reader, IEnumerable rf.ParseRows(reader, dataTransformers, token))) - results.AddResults(t.Item1.ChildType, t.Item2); + results.AddResults(t.Item1.ItemType, t.Item2); - BuildHierarchy(results); - - return (IEnumerable)results[typeof(T)]; + return BuildHierarchy(results); } #if !NET40 @@ -229,11 +177,9 @@ public override async Task> ParseRowsAsync(DbDataReader reader, I var results = new ResultHolder(); foreach (var t in ParseRows(reader, token, rf => rf.ParseRowsAsync(reader, dataTransformers, token))) - results.AddResults(t.Item1.ChildType, await t.Item2); - - BuildHierarchy(results); + results.AddResults(t.Item1.ItemType, await t.Item2); - return (IEnumerable)results[typeof(T)]; + return BuildHierarchy(results); } #endif @@ -276,7 +222,7 @@ private HierarchicalRowInfo GetNextBestRowInfo(IDataReader reader, List 0 && !reader.NextResult()) { if (toRead.Any(f => f.ShouldThrowIfNotFound(toRead))) - throw new StoredProcedureResultsException(typeof(T), toRead.Select(f => f.ChildType).ToArray()); + throw new StoredProcedureResultsException(typeof(T), toRead.Select(f => f.ItemType).ToArray()); return HierarchicalRowInfo.Empty; } @@ -289,7 +235,7 @@ private HierarchicalRowInfo GetNextBestRowInfo(IDataReader reader, List f.ChildType == type); + return toRead.First(f => f.ItemType == type); } HierarchicalRowInfo result = null; @@ -312,42 +258,21 @@ private HierarchicalRowInfo GetNextBestRowInfo(IDataReader reader, List BuildHierarchy(ResultHolder results) { + Contract.Requires(results != null); + Contract.Ensures(Contract.Result>() != null); + // the first one is the actual result row, so it won't have the parent/child relationship // defined foreach (var hri in rowInfos.Skip(1)) - { - IEnumerable children, parents; + hri.AssignItemsToParents(results); - if (results.TryGetValue(hri.ParentType, out parents)) - { - if (!results.TryGetValue(hri.ChildType, out children)) - { - if (hri.IsOptional) - { - object empty; - - if (hri.IsArray) - empty = Array.CreateInstance(hri.ChildType, 0); - else // this needs to be an empty generic list - empty = Activator.CreateInstance(typeof(List<>).MakeGenericType(hri.ChildType)); + IEnumerable res; + if (!results.TryGetValue(out res, resultType)) + throw new StoredProcedureException("Unexpected error trying to retrieve the results for the hierarchy with root type " + typeof(T)); - foreach (var o in parents) - hri.Assigner.DynamicInvoke(o, empty); - } - else - { - - } - } - else - { - foreach (var o in parents) - hri.Assigner.DynamicInvoke(o, children); - } - } - } + return res; } public override bool MatchesColumns(IEnumerable columnNames, out int leftoverColumns) @@ -367,33 +292,27 @@ private class HierarchicalRowInfo public static readonly HierarchicalRowInfo Empty = new HierarchicalRowInfo(); private readonly HierarchicalRowInfo parent; - public Type ParentType { get; } - public Type ChildType { get; } + public Type ItemType { get; } public string ParentKeyColumnName { get; } - public string ChildKeyColumnName { get; } + public string ItemKeyColumnName { get; } public IRowFactory RowFactory { get; } - public Delegate Assigner { get; } public bool IsArray { get; } public bool IsOptional { get; } public HierarchicalRowInfo( HierarchicalRowInfo parent = null, - Type parentType = null, - Type childType = null, + Type itemType = null, string parentKeyColumnName = null, - string childKeyColumnName = null, + string itemKeyColumnName = null, IRowFactory rowFactory = null, - Delegate assigner = null, bool isOptional = false, bool isArray = false) { this.parent = parent; - ParentType = parentType; - ChildType = childType; + ItemType = itemType; ParentKeyColumnName = parentKeyColumnName; - ChildKeyColumnName = childKeyColumnName; + ItemKeyColumnName = itemKeyColumnName; RowFactory = rowFactory; - Assigner = assigner; IsOptional = isOptional; IsArray = isArray; } @@ -404,7 +323,7 @@ public bool MatchesColumns(IEnumerable columnNames, out int unMatchedCol if (!string.IsNullOrEmpty(ParentKeyColumnName) && columnNames.Contains(ParentKeyColumnName)) --unMatchedColumnCount; - if (!string.IsNullOrEmpty(ChildKeyColumnName) && columnNames.Contains(ChildKeyColumnName)) + if (!string.IsNullOrEmpty(ItemKeyColumnName) && columnNames.Contains(ItemKeyColumnName)) --unMatchedColumnCount; return success; @@ -420,40 +339,113 @@ public bool ShouldThrowIfNotFound(IEnumerable remaining) return true; } + + public virtual void AssignItemsToParents(ResultHolder results) + { + // the base class shouldn't be called, as only the top level item uses the non-generic type, + // and it is skipped when assigning the hierarchies + } } - private class ResultHolder + private class RootHierarchicalRowInfo : HierarchicalRowInfo { - private readonly ConcurrentDictionary> resultMap = new ConcurrentDictionary>(); - private readonly MethodInfo yielder = typeof(ResultHolder).GetMethod(nameof(ResultHolder.YieldResults), BindingFlags.NonPublic | BindingFlags.Instance); + public RootHierarchicalRowInfo() : base(itemType: typeof(TItem), rowFactory: RowFactory.Create(false)) { } + } - public void AddResults(Type t, IEnumerable results) + private class HierarchicalRowInfo : HierarchicalRowInfo + { + private readonly Action> assigner; + private readonly Func keyFunc; + private readonly Func foreignKeyFunc; + + public HierarchicalRowInfo( + PropertyInfo parentKeyProperty, + PropertyInfo childParentKeyProperty, + PropertyInfo parentChildrenProperty, + HierarchicalRowInfo parent) + : base(parent: parent, + itemType: typeof(TItem), + parentKeyColumnName: parentKeyProperty.GetSqlColumnName(), + itemKeyColumnName: childParentKeyProperty.GetSqlColumnName(), + rowFactory: RowFactory.Create(false), + isOptional: parentChildrenProperty.IsOptional(), + isArray: parentChildrenProperty.PropertyType.IsArray) { - resultMap.GetOrAdd(t, _ => new List()) - .Add(results); + keyFunc = childParentKeyProperty.CompileGetter(); + foreignKeyFunc = parentKeyProperty .CompileGetter(); + + var value = Expression.Parameter(typeof(IEnumerable), "value"); + Expression toSet = value; + + if (parentChildrenProperty.PropertyType.IsArray) + toSet = Expression.Call(toArray.MakeGenericMethod(typeof(TItem)), toSet); + else + toSet = Expression.Call(toList.MakeGenericMethod(typeof(TItem)), toSet); + + var p = Expression.Parameter(typeof(TParent), "p"); + var assign = Expression.Assign(Expression.Property(p, parentChildrenProperty), toSet); + assigner = Expression.Lambda>>(assign, p, value).Compile(); } - public IEnumerable this[Type t] + public override void AssignItemsToParents(ResultHolder results) { - get + IEnumerable parents; + IEnumerable toSet = null; + + if (results.TryGetValue(out parents)) { - var res = resultMap[t]; - if (res.Count == 1) - return res[0]; + if (!results.TryGetValue(out toSet)) + { + if (IsOptional) + { + if (IsArray) + toSet = new TItem[0]; + else // this needs to be an empty generic list + toSet = new List(); + } + } + } - return (IEnumerable)yielder.MakeGenericMethod(t).Invoke(this, new[] { res }); + if (toSet != null) + { + if (toSet.Any()) + { + var lookup = toSet.ToLookup(keyFunc); + foreach (TParent p in parents) + { + var key = foreignKeyFunc(p); + var items = lookup[key]; + assigner(p, items); + } + } + else + { + foreach (TParent p in parents) + assigner(p, toSet); + } } } + } + + private class ResultHolder + { + private readonly ConcurrentDictionary> resultMap = new ConcurrentDictionary>(); + + public void AddResults(Type t, IEnumerable results) + { + resultMap.GetOrAdd(t, _ => new List()) + .Add(results); + } - public bool TryGetValue(Type t, out IEnumerable results) + public bool TryGetValue(out IEnumerable results, Type keyType = null) { List res; - if (resultMap.TryGetValue(t, out res)) + if (resultMap.TryGetValue(keyType ?? typeof(TRes), out res)) { if (res.Count == 1) - results = res[0]; + results = (IEnumerable)res[0]; else - results = (IEnumerable)yielder.MakeGenericMethod(t).Invoke(this, new[] { res }); + results = YieldResults(res); return true; } diff --git a/CodeOnlyStoredProcedures.nuspec b/CodeOnlyStoredProcedures.nuspec index 8010b35..7687390 100644 --- a/CodeOnlyStoredProcedures.nuspec +++ b/CodeOnlyStoredProcedures.nuspec @@ -11,6 +11,7 @@ Code Only Stored Procedures will not create any Stored Procedures on your database. Instead, its aim is to make it easy to call your existing stored procedures by writing simple code. 2.3.0 Fixed bug where hierarchical result sets could not be marked as optional +Hierarchies are now much faster to build, especially with large data sets. 2.2.6 Fixed bug where dynamic stored procedures wouldn't dispose of the IDbCommand objects they created. diff --git a/CodeOnlyTests/RowFactory/HierarchicalTypeRowFactoryTests.cs b/CodeOnlyTests/RowFactory/HierarchicalTypeRowFactoryTests.cs index e63c4b8..5b6f167 100644 --- a/CodeOnlyTests/RowFactory/HierarchicalTypeRowFactoryTests.cs +++ b/CodeOnlyTests/RowFactory/HierarchicalTypeRowFactoryTests.cs @@ -706,6 +706,45 @@ public void Throws_If_OptionalChildren_Returned_But_RequiredChildren_Of_Optional .ShouldThrow("because a required result set was missing"); } } + + [TestMethod] + public void Builds_Hierarchy_When_All_Items_Retrieved_By_Interface() + { + using (GlobalSettings.UseTestInstance()) + { + GlobalSettings.Instance.InterfaceMap.TryAdd(typeof(IParent), typeof(Parent)); + GlobalSettings.Instance.InterfaceMap.TryAdd(typeof(IChild), typeof(Child)); + + var reader = SetupDataReader( + new Dictionary + { + { "ParentId", 42 } + }, + new Dictionary + { + { "ParentId", 42 }, + { "Name", "Foo" } + }); + + var toTest = new HierarchicalTypeRowFactory(); + var res = toTest.ParseRows(reader, new IDataTransformer[0], CancellationToken.None); + + res.Should().ContainSingle("because only one row was setup").Which + .ShouldBeEquivalentTo( + new Parent + { + ParentId = 42, + Children = new List + { + new Child + { + ParentId = 42, + Name = "Foo" + } + } + }); + } + } } private static IDataReader SetupDataReader(params Dictionary[] values) @@ -951,5 +990,29 @@ public class OptionalRequired [OptionalResult, ForeignKey("Level2Id")] public List Children { get; set; } } + + public interface IParent + { + int ParentId { get; } + IEnumerable Children { get; } + } + + public class Parent : IParent + { + public int ParentId { get; set; } + public IEnumerable Children { get; set; } + } + + public interface IChild + { + int ParentId { get; } + string Name { get; } + } + + public class Child : IChild + { + public int ParentId { get; set; } + public string Name { get; set; } + } } }