diff --git a/src/NPoco/ColumnInfo.cs b/src/NPoco/ColumnInfo.cs index 6432d270..ef8572cb 100644 --- a/src/NPoco/ColumnInfo.cs +++ b/src/NPoco/ColumnInfo.cs @@ -26,6 +26,10 @@ public class ColumnInfo public string ReferenceMemberName { get; set; } public MemberInfo MemberInfo { get; internal set; } public bool ExactColumnNameMatch { get; set; } + /// + /// If this is set, this column's members will not be mapped beyond [HardDepthLimit] iterations (useful for complex, long-looping or aggregate data structures) + /// + public int? HardDepthLimit {get;set;} public static ColumnInfo FromMemberInfo(MemberInfo mi) { @@ -38,6 +42,7 @@ public static ColumnInfo FromMemberInfo(MemberInfo mi) var serializedColumnAttributes = attrs.OfType().ToArray(); var reference = attrs.OfType().ToArray(); var aliasColumn = attrs.OfType().FirstOrDefault(); + var depthLimit = attrs.OfType().FirstOrDefault(); // Check if declaring poco has [ExplicitColumns] attribute var explicitColumns = mi.DeclaringType.GetTypeInfo().GetCustomAttributes(typeof(ExplicitColumnsAttribute), true).Any(); @@ -50,6 +55,9 @@ public static ColumnInfo FromMemberInfo(MemberInfo mi) ci.IgnoreColumn = true; } + if (depthLimit != null ) + ci.HardDepthLimit = depthLimit.DepthLimit; + if (complexMapping.Any()) { ci.ComplexMapping = true; diff --git a/src/NPoco/DepthLimitAttribute.cs b/src/NPoco/DepthLimitAttribute.cs new file mode 100644 index 00000000..363c8492 --- /dev/null +++ b/src/NPoco/DepthLimitAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace NPoco +{ + /// + /// Use to decorate columns, usually of lists, that have a chance of looping on themselves while being mapped + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class DepthLimitAttribute : Attribute + { + /// + /// Default constructor sets the depth limit to 1, which is - do not map beyond one iteration. + /// + public DepthLimitAttribute() + { + DepthLimit = 1; + } + public DepthLimitAttribute(int maxDepth) + { + if (maxDepth < 1) throw new ArgumentOutOfRangeException("maxDepth", "Hard depth limit should be at least equal to 1"); + DepthLimit = maxDepth; + } + public int DepthLimit { get; set; } + } +} \ No newline at end of file diff --git a/src/NPoco/FluentMappings/ColumnConfigurationBuilder.cs b/src/NPoco/FluentMappings/ColumnConfigurationBuilder.cs index efc5a60e..d5282009 100644 --- a/src/NPoco/FluentMappings/ColumnConfigurationBuilder.cs +++ b/src/NPoco/FluentMappings/ColumnConfigurationBuilder.cs @@ -61,6 +61,7 @@ public interface IManyColumnBuilder IManyColumnBuilder WithDbType(Type type); IManyColumnBuilder WithDbType(); IManyColumnBuilder Reference(Expression> member); + IManyColumnBuilder LimitMappingDepth(int limit = 1); } public class ManyColumnBuilder : IManyColumnBuilder @@ -96,6 +97,20 @@ public IManyColumnBuilder Reference(Expression> mem _columnDefinition.ReferenceMember = MemberHelper.GetMembers(member).Last(); return this; } + /// + /// Sets the hard mapping depth limit for this column's definition + /// + /// integer hard depth limit. Defaults to and can't be less than 1 + /// The ManyColumnBuilder + public IManyColumnBuilder LimitMappingDepth(int limit = 1) + { + if ( limit < 1 ) + throw new ArgumentOutOfRangeException("limit", "The mapping depth limit must always be at least equal to 1"); + + _columnDefinition.HardDepthLimit = limit; + + return this; + } } public interface IColumnBuilder @@ -118,6 +133,7 @@ public interface IColumnBuilder IColumnBuilder ValueObject(); IColumnBuilder ValueObject(Expression> member); IColumnBuilder ForceToUtc(bool enabled); + IColumnBuilder LimitMappingDepth(int limit = 1); } public class ColumnBuilder : IColumnBuilder @@ -253,5 +269,19 @@ public IColumnBuilder ForceToUtc(bool enabled) _columnDefinition.ForceUtc = enabled; return this; } + /// + /// Sets the hard mapping depth limit for this column's definition + /// + /// integer hard depth limit. Defaults to and can't be less than 1 + /// The ColumnBuilder + public IColumnBuilder LimitMappingDepth(int limit = 1) + { + if ( limit < 1 ) + throw new ArgumentOutOfRangeException("limit", "The mapping depth limit must always be at least equal to 1"); + + _columnDefinition.HardDepthLimit = limit; + + return this; + } } } \ No newline at end of file diff --git a/src/NPoco/FluentMappings/ColumnDefinition.cs b/src/NPoco/FluentMappings/ColumnDefinition.cs index 3a9b064e..0fdddd6c 100644 --- a/src/NPoco/FluentMappings/ColumnDefinition.cs +++ b/src/NPoco/FluentMappings/ColumnDefinition.cs @@ -26,5 +26,9 @@ public class ColumnDefinition public bool? ValueObjectColumn { get; set; } public string ValueObjectColumnName { get; set; } public bool? ExactColumnNameMatch { get; set; } + /// + /// If this is set, this column's members will not be mapped beyond [HardDepthLimit] iterations (useful for complex, long-looping or aggregate data structures) + /// + public int? HardDepthLimit {get;set;} } } \ No newline at end of file diff --git a/src/NPoco/FluentMappings/ConventionScannerSettings.cs b/src/NPoco/FluentMappings/ConventionScannerSettings.cs index f87de9c7..15a94107 100644 --- a/src/NPoco/FluentMappings/ConventionScannerSettings.cs +++ b/src/NPoco/FluentMappings/ConventionScannerSettings.cs @@ -31,6 +31,10 @@ public ConventionScannerSettings() public Func DbColumnsNamed { get; set; } public Func AliasNamed { get; set; } public Func DbColumnTypesAs { get; set; } + /// + /// Set the hard depth limit for the columns. Null leaves the limit unset. + /// + public Func HardDepthLimitAs {get;set;} public List> IgnorePropertiesWhere { get; set; } public Func VersionPropertiesWhere { get; set; } public Func VersionPropertyTypeAs { get; set; } diff --git a/src/NPoco/FluentMappings/FluentMappingConfiguration.cs b/src/NPoco/FluentMappings/FluentMappingConfiguration.cs index 21894af9..827057aa 100644 --- a/src/NPoco/FluentMappings/FluentMappingConfiguration.cs +++ b/src/NPoco/FluentMappings/FluentMappingConfiguration.cs @@ -113,10 +113,14 @@ private static IEnumerable GetColumnDefinitions(ConventionScan foreach (var columnDefinition in columnDefinitions) { + + if ( columnDefinition.HardDepthLimit >= members.Count) + continue; + yield return columnDefinition; } - var referenceDbColumnsNamed = scannerSettings.ReferenceDbColumnsNamed(member); + var referenceDbColumnsNamed = scannerSettings.ReferenceDbColumnsNamed(member); yield return new ColumnDefinition() { @@ -128,6 +132,7 @@ private static IEnumerable GetColumnDefinitions(ConventionScan ReferenceMember = null, ResultColumn = scannerSettings.ResultPropertiesWhere(member), DbColumnName = referenceProperty ? referenceDbColumnsNamed : null, + HardDepthLimit = scannerSettings.HardDepthLimitAs(member) }; } else @@ -151,6 +156,7 @@ private static IEnumerable GetColumnDefinitions(ConventionScan columnDefinition.Serialized = scannerSettings.SerializedWhere(member); columnDefinition.IsComplexMapping = scannerSettings.ComplexPropertiesWhere(member); columnDefinition.ValueObjectColumn = scannerSettings.ValueObjectColumnWhere(member); + columnDefinition.HardDepthLimit = scannerSettings.HardDepthLimitAs(member); yield return columnDefinition; } } @@ -182,7 +188,8 @@ private static ConventionScannerSettings ProcessSettings(Action ReflectionUtils.GetCustomAttributes(x, typeof(ColumnAttribute)).Any(), ValueObjectColumnWhere = x => x.GetMemberInfoType().GetInterfaces().Any(y => y == typeof(IValueObject)), Lazy = false, - MapNestedTypesWhen = x => false + MapNestedTypesWhen = x => false, + HardDepthLimitAs = x => null //no depth limit by default }; scanner.Invoke(new ConventionScanner(defaultScannerSettings)); return defaultScannerSettings; @@ -227,6 +234,7 @@ private static void MergeAttributeOverrides(Dictionary con columnDefinition.Value.ForceUtc = columnInfo.ForceToUtc; columnDefinition.Value.Serialized = columnInfo.SerializedColumn; columnDefinition.Value.ValueObjectColumn = columnInfo.ValueObjectColumn; + columnDefinition.Value.HardDepthLimit = columnInfo.HardDepthLimit; } } } @@ -278,6 +286,7 @@ private static void MergeOverrides(Dictionary config, Mapp convColDefinition.ValueObjectColumn = overrideColumnDefinition.Value.ValueObjectColumn ?? convColDefinition.ValueObjectColumn; convColDefinition.ValueObjectColumnName = overrideColumnDefinition.Value.ValueObjectColumnName ?? convColDefinition.ValueObjectColumnName; convColDefinition.ExactColumnNameMatch = overrideColumnDefinition.Value.ExactColumnNameMatch ?? convColDefinition.ExactColumnNameMatch; + convColDefinition.HardDepthLimit = overrideColumnDefinition.Value.HardDepthLimit ?? convColDefinition.HardDepthLimit; } } } diff --git a/src/NPoco/FluentMappings/FluentMappingsPocoDataBuilder.cs b/src/NPoco/FluentMappings/FluentMappingsPocoDataBuilder.cs index cf4b7bc4..2d9043c5 100644 --- a/src/NPoco/FluentMappings/FluentMappingsPocoDataBuilder.cs +++ b/src/NPoco/FluentMappings/FluentMappingsPocoDataBuilder.cs @@ -116,6 +116,8 @@ protected override ColumnInfo GetColumnInfo(MemberInfo mi, Type type) if (isColumnDefined && (typeConfig.ColumnConfiguration[key].IgnoreColumn.HasValue && typeConfig.ColumnConfiguration[key].IgnoreColumn.Value)) columnInfo.IgnoreColumn = true; + if (isColumnDefined) columnInfo.HardDepthLimit = typeConfig.ColumnConfiguration[key].HardDepthLimit; + // Work out the DB column name if (isColumnDefined) { diff --git a/src/NPoco/PocoDataBuilder.cs b/src/NPoco/PocoDataBuilder.cs index f59e8f7e..441f761b 100644 --- a/src/NPoco/PocoDataBuilder.cs +++ b/src/NPoco/PocoDataBuilder.cs @@ -123,10 +123,34 @@ private static IEnumerable GetPocoColumns(IEnumerable me } } - public IEnumerable GetPocoMembers(ColumnInfo[] columnInfos, List memberInfos, string prefix = null) + private int _LongestUnbrokenTypeSequence(IEnumerable input) { + var iterator = input.GetEnumerator(); + Dictionary lastPositions = new Dictionary(); + int maxLen = 0; + int windowStart = 0; + int windowEnd = 0; + + while (iterator.MoveNext()) + { + if (lastPositions.ContainsKey(iterator.Current)) + { + windowStart = Math.Max(windowStart, lastPositions[iterator.Current] + 1); + } + + lastPositions[iterator.Current] = windowEnd; + maxLen = Math.Max(maxLen, windowEnd - windowStart + 1); + windowEnd++; + } + + return maxLen; + } + + public IEnumerable GetPocoMembers(ColumnInfo[] columnInfos, List memberInfos, string prefix = null) + { var capturedMembers = memberInfos.ToArray(); var capturedPrefix = prefix; + foreach (var columnInfo in columnInfos) { if (columnInfo.IgnoreColumn) @@ -145,6 +169,14 @@ public IEnumerable GetPocoMembers(ColumnInfo[] columnInfos, List TableInfoPlan childTableInfoPlan = null; var members = new List(capturedMembers) { columnInfo.MemberInfo }; + // Never map anything beyond the hard set limit, if one has been set + if ( columnInfo.HardDepthLimit.HasValue) + { + if (_LongestUnbrokenTypeSequence(members.Select(m => m.GetMemberInfoType())) > columnInfo.HardDepthLimit.Value) + continue; + } + + if (columnInfo.ComplexMapping || columnInfo.ReferenceType != ReferenceType.None) { if (capturedMembers.GroupBy(x => x.GetMemberInfoType()).Any(x => x.Count() >= 2))