diff --git a/Src/FluentAssertions/Common/MemberPath.cs b/Src/FluentAssertions/Common/MemberPath.cs
index 505f7f0ab6..5d62ddf12f 100644
--- a/Src/FluentAssertions/Common/MemberPath.cs
+++ b/Src/FluentAssertions/Common/MemberPath.cs
@@ -17,6 +17,8 @@ internal class MemberPath
private string[] segments;
+ private static readonly MemberPathSegmentEqualityComparer MemberPathSegmentEqualityComparer = new();
+
public MemberPath(IMember member, string parentPath)
: this(member.ReflectedType, member.DeclaringType, parentPath.Combine(member.Name))
{
@@ -53,7 +55,7 @@ public bool IsSameAs(MemberPath candidate)
{
string[] candidateSegments = candidate.Segments;
- return candidateSegments.SequenceEqual(Segments);
+ return candidateSegments.SequenceEqual(Segments, MemberPathSegmentEqualityComparer);
}
return false;
@@ -64,7 +66,7 @@ private bool IsParentOf(MemberPath candidate)
string[] candidateSegments = candidate.Segments;
return candidateSegments.Length > Segments.Length &&
- candidateSegments.Take(Segments.Length).SequenceEqual(Segments);
+ candidateSegments.Take(Segments.Length).SequenceEqual(Segments, MemberPathSegmentEqualityComparer);
}
private bool IsChildOf(MemberPath candidate)
@@ -72,7 +74,14 @@ private bool IsChildOf(MemberPath candidate)
string[] candidateSegments = candidate.Segments;
return candidateSegments.Length < Segments.Length
- && candidateSegments.SequenceEqual(Segments.Take(candidateSegments.Length));
+ && candidateSegments.SequenceEqual(Segments.Take(candidateSegments.Length),
+ MemberPathSegmentEqualityComparer);
+ }
+
+ public MemberPath AsParentCollectionOf(MemberPath nextPath)
+ {
+ var extendedDottedPath = dottedPath.Combine(nextPath.dottedPath, "[]");
+ return new MemberPath(declaringType, nextPath.reflectedType, extendedDottedPath);
}
///
@@ -86,7 +95,7 @@ public bool IsEquivalentTo(string path)
public bool HasSameParentAs(MemberPath path)
{
return Segments.Length == path.Segments.Length
- && GetParentSegments().SequenceEqual(path.GetParentSegments());
+ && GetParentSegments().SequenceEqual(path.GetParentSegments(), MemberPathSegmentEqualityComparer);
}
private IEnumerable GetParentSegments() => Segments.Take(Segments.Length - 1);
@@ -96,6 +105,11 @@ public bool HasSameParentAs(MemberPath path)
///
public bool GetContainsSpecificCollectionIndex() => dottedPath.ContainsSpecificCollectionIndex();
+ private string[] Segments =>
+ segments ??= dottedPath
+ .Replace("[]", "[*]", StringComparison.Ordinal)
+ .Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
+
///
/// Returns a copy of the current object as if it represented an un-indexed item in a collection.
///
@@ -104,8 +118,6 @@ public MemberPath WithCollectionAsRoot()
return new MemberPath(reflectedType, declaringType, "[]." + dottedPath);
}
- private string[] Segments => segments ??= dottedPath.Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
-
///
/// Returns the name of the member the current path points to without its parent path.
///
diff --git a/Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs b/Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs
new file mode 100644
index 0000000000..10c1a43215
--- /dev/null
+++ b/Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace FluentAssertions.Common
+{
+ ///
+ /// Compares two segments of a .
+ /// Sets the equal with any numeric index qualifier.
+ /// All other comparisons are default string equality.
+ ///
+ internal class MemberPathSegmentEqualityComparer : IEqualityComparer
+ {
+ private const string AnyIndexQualifier = "*";
+ private static readonly Regex IndexQualifierRegex = new(@"^\d+$");
+
+ ///
+ /// Compares two segments of a .
+ ///
+ /// Left part of the comparison.
+ /// Right part of the comparison.
+ /// True if segments are equal, false if not.
+ public bool Equals(string x, string y)
+ {
+ if (x == AnyIndexQualifier)
+ {
+ return IsIndexQualifier(y);
+ }
+
+ if (y == AnyIndexQualifier)
+ {
+ return IsIndexQualifier(x);
+ }
+
+ return x == y;
+ }
+
+ private static bool IsIndexQualifier(string segment)
+ => segment == AnyIndexQualifier || IndexQualifierRegex.IsMatch(segment);
+
+ public int GetHashCode(string obj)
+ {
+#if NETCOREAPP2_1_OR_GREATER
+ return obj.GetHashCode(System.StringComparison.Ordinal);
+#else
+ return obj.GetHashCode();
+#endif
+ }
+ }
+}
diff --git a/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs
index 0b16cdf241..14fbc86abc 100644
--- a/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs
+++ b/Src/FluentAssertions/Equivalency/EquivalencyAssertionOptions.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using FluentAssertions.Common;
using FluentAssertions.Equivalency.Execution;
@@ -40,6 +39,17 @@ public EquivalencyAssertionOptions Excluding(Expression
+ /// Selects a collection to define exclusions at.
+ /// Allows to navigate deeper by using .
+ ///
+ public NestedExclusionOptionBuilder For(Expression>> expression)
+ {
+ var selectionRule = new ExcludeMemberByPathSelectionRule(expression.GetMemberPath());
+ AddSelectionRule(selectionRule);
+ return new NestedExclusionOptionBuilder(this, selectionRule);
+ }
+
///
/// Includes the specified member in the equality check.
///
diff --git a/Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs b/Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs
new file mode 100644
index 0000000000..02821424f1
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using FluentAssertions.Common;
+using FluentAssertions.Equivalency.Selection;
+
+namespace FluentAssertions.Equivalency
+{
+ public class NestedExclusionOptionBuilder
+ {
+ ///
+ /// The selected path starting at the first .
+ ///
+ private readonly ExcludeMemberByPathSelectionRule currentPathSelectionRule;
+
+ private readonly EquivalencyAssertionOptions capturedAssertionOptions;
+
+ internal NestedExclusionOptionBuilder(EquivalencyAssertionOptions capturedAssertionOptions,
+ ExcludeMemberByPathSelectionRule currentPathSelectionRule)
+ {
+ this.capturedAssertionOptions = capturedAssertionOptions;
+ this.currentPathSelectionRule = currentPathSelectionRule;
+ }
+
+ ///
+ /// Selects a nested property to exclude. This ends the chain.
+ ///
+ public EquivalencyAssertionOptions Exclude(Expression> expression)
+ {
+ var nextPath = expression.GetMemberPath();
+ currentPathSelectionRule.AppendPath(nextPath);
+ return capturedAssertionOptions;
+ }
+
+ ///
+ /// Adds the selected collection to the chain.
+ ///
+ public NestedExclusionOptionBuilder For(
+ Expression>> expression)
+ {
+ var nextPath = expression.GetMemberPath();
+ currentPathSelectionRule.AppendPath(nextPath);
+ return new NestedExclusionOptionBuilder(capturedAssertionOptions, currentPathSelectionRule);
+ }
+ }
+}
diff --git a/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPathSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPathSelectionRule.cs
index 3b4c263837..0d68882444 100644
--- a/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPathSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPathSelectionRule.cs
@@ -8,7 +8,7 @@ namespace FluentAssertions.Equivalency.Selection
///
internal class ExcludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule
{
- private readonly MemberPath memberToExclude;
+ private MemberPath memberToExclude;
public ExcludeMemberByPathSelectionRule(MemberPath pathToExclude)
: base(pathToExclude.ToString())
@@ -23,6 +23,12 @@ public ExcludeMemberByPathSelectionRule(MemberPath pathToExclude)
memberToExclude.IsSameAs(new MemberPath(member, parentPath)));
}
+ public void AppendPath(MemberPath nextPath)
+ {
+ memberToExclude = memberToExclude.AsParentCollectionOf(nextPath);
+ SetSelectedPath(memberToExclude.ToString());
+ }
+
public override string ToString()
{
return "Exclude member " + memberToExclude;
diff --git a/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs
index 8a32b13ea3..753b59818a 100644
--- a/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/SelectMemberByPathSelectionRule.cs
@@ -7,7 +7,7 @@ namespace FluentAssertions.Equivalency.Selection
{
internal abstract class SelectMemberByPathSelectionRule : IMemberSelectionRule
{
- private readonly string selectedPath;
+ private string selectedPath;
protected SelectMemberByPathSelectionRule(string selectedPath)
{
@@ -16,6 +16,11 @@ protected SelectMemberByPathSelectionRule(string selectedPath)
public virtual bool IncludesMembers => false;
+ protected void SetSelectedPath(string path)
+ {
+ this.selectedPath = path;
+ }
+
public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers,
MemberSelectionContext context)
{
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
index 8e6e5e5a1d..a86a4adf5e 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
@@ -791,6 +791,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationMemberPath, System.Linq.Expressions.Expression> subjectMemberPath) { }
@@ -959,6 +960,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
+ public class NestedExclusionOptionBuilder
+ {
+ public FluentAssertions.Equivalency.EquivalencyAssertionOptions Exclude(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
+ }
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt
index 48dce352e8..45386470fb 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt
@@ -803,6 +803,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationMemberPath, System.Linq.Expressions.Expression> subjectMemberPath) { }
@@ -971,6 +972,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
+ public class NestedExclusionOptionBuilder
+ {
+ public FluentAssertions.Equivalency.EquivalencyAssertionOptions Exclude(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
+ }
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
index e0c1d1a59c..32f7082f3f 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
@@ -791,6 +791,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationMemberPath, System.Linq.Expressions.Expression> subjectMemberPath) { }
@@ -959,6 +960,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
+ public class NestedExclusionOptionBuilder
+ {
+ public FluentAssertions.Equivalency.EquivalencyAssertionOptions Exclude(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
+ }
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
index 48cf7bfda4..cd16c72a15 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
@@ -791,6 +791,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationMemberPath, System.Linq.Expressions.Expression> subjectMemberPath) { }
@@ -959,6 +960,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
+ public class NestedExclusionOptionBuilder
+ {
+ public FluentAssertions.Equivalency.EquivalencyAssertionOptions Exclude(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
+ }
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
index 80b123a3d3..e7ef120517 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
@@ -784,6 +784,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationMemberPath, System.Linq.Expressions.Expression> subjectMemberPath) { }
@@ -952,6 +953,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
+ public class NestedExclusionOptionBuilder
+ {
+ public FluentAssertions.Equivalency.EquivalencyAssertionOptions Exclude(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
+ }
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
index 618a9a052d..f17275a93c 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
@@ -791,6 +791,7 @@ namespace FluentAssertions.Equivalency
public EquivalencyAssertionOptions(FluentAssertions.Equivalency.IEquivalencyAssertionOptions defaults) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions> AsCollection() { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Excluding(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions Including(System.Linq.Expressions.Expression> expression) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(string expectationMemberPath, string subjectMemberPath) { }
public FluentAssertions.Equivalency.EquivalencyAssertionOptions WithMapping(System.Linq.Expressions.Expression> expectationMemberPath, System.Linq.Expressions.Expression> subjectMemberPath) { }
@@ -959,6 +960,11 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
}
+ public class NestedExclusionOptionBuilder
+ {
+ public FluentAssertions.Equivalency.EquivalencyAssertionOptions Exclude(System.Linq.Expressions.Expression> expression) { }
+ public FluentAssertions.Equivalency.NestedExclusionOptionBuilder For(System.Linq.Expressions.Expression>> expression) { }
+ }
public class Node : FluentAssertions.Equivalency.INode
{
public Node() { }
diff --git a/Tests/FluentAssertions.Equivalency.Specs/CollectionSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/CollectionSpecs.cs
index 6e228e27d1..0d5856bec0 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/CollectionSpecs.cs
+++ b/Tests/FluentAssertions.Equivalency.Specs/CollectionSpecs.cs
@@ -558,6 +558,373 @@ public void When_a_deeply_nested_property_of_a_collection_with_an_invalid_value_
act.Should().NotThrow();
}
+ public class For
+ {
+ [Fact]
+ public void When_property_in_collection_is_excluded_it_should_not_throw()
+ {
+ // Arrange
+ var subject = new
+ {
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ Text = "Text"
+ },
+ new
+ {
+ Number = 2,
+ Text = "Actual"
+ }
+ }
+ }
+ };
+
+ var expected = new
+ {
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ Text = "Text"
+ },
+ new
+ {
+ Number = 3,
+ Text = "Actual"
+ }
+ }
+ }
+ };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected,
+ options => options
+ .For(x => x.Level.Collection)
+ .Exclude(x => x.Number));
+ }
+
+ [Fact]
+ public void When_collection_in_collection_is_excluded_it_should_not_throw()
+ {
+ // Arrange
+ var subject = new
+ {
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ NextCollection = new[]
+ {
+ new
+ {
+ Text = "Text"
+ }
+ }
+ },
+ new
+ {
+ Number = 2,
+ NextCollection = new[]
+ {
+ new
+ {
+ Text = "Actual"
+ }
+ }
+ }
+ }
+ }
+ };
+
+ var expected = new
+ {
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ NextCollection = new[]
+ {
+ new
+ {
+ Text = "Text"
+ }
+ }
+ },
+ new
+ {
+ Number = 2,
+ NextCollection = new[]
+ {
+ new
+ {
+ Text = "Expected"
+ }
+ }
+ }
+ }
+ }
+ };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected,
+ options => options
+ .For(x => x.Level.Collection)
+ .Exclude(x => x.NextCollection));
+ }
+
+ [Fact]
+ public void When_property_in_collection_in_collection_is_excluded_it_should_not_throw()
+ {
+ // Arrange
+ var subject = new
+ {
+ Text = "Text",
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ NextCollection = new[]
+ {
+ new
+ {
+ Text = "Text"
+ }
+ }
+ },
+ new
+ {
+ Number = 2,
+ NextCollection = new[]
+ {
+ new
+ {
+ Text = "Actual"
+ }
+ }
+ }
+ }
+ }
+ };
+
+ var expected = new
+ {
+ Text = "Text",
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ NextCollection = new[]
+ {
+ new
+ {
+ Text = "Text"
+ }
+ }
+ },
+ new
+ {
+ Number = 2,
+ NextCollection = new[]
+ {
+ new
+ {
+ Text = "Expected"
+ }
+ }
+ }
+ }
+ }
+ };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected,
+ options => options
+ .For(x => x.Level.Collection)
+ .For(x => x.NextCollection)
+ .Exclude(x => x.Text)
+ );
+ }
+
+ [Fact]
+ public void A_nested_exclusion_can_be_followed_by_a_root_level_exclusion()
+ {
+ // Arrange
+ var subject = new
+ {
+ Text = "Actual",
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ Text = "Text"
+ },
+ new
+ {
+ Number = 2,
+ Text = "Actual"
+ }
+ }
+ }
+ };
+
+ var expected = new
+ {
+ Text = "Expected",
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ Text = "Text"
+ },
+ new
+ {
+ Number = 2,
+ Text = "Expected"
+ }
+ }
+ }
+ };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected,
+ options => options
+ .For(x => x.Level.Collection).Exclude(x => x.Text)
+ .Excluding(x => x.Text));
+ }
+
+ [Fact]
+ public void A_nested_exclusion_can_be_preceded_by_a_root_level_exclusion()
+ {
+ // Arrange
+ var subject = new
+ {
+ Text = "Actual",
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ Text = "Text"
+ },
+ new
+ {
+ Number = 2,
+ Text = "Actual"
+ }
+ }
+ }
+ };
+
+ var expected = new
+ {
+ Text = "Expected",
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ Text = "Text"
+ },
+ new
+ {
+ Number = 2,
+ Text = "Expected"
+ }
+ }
+ }
+ };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected,
+ options => options
+ .Excluding(x => x.Text)
+ .For(x => x.Level.Collection).Exclude(x => x.Text));
+ }
+
+ [Fact]
+ public void A_nested_exclusion_can_be_followed_by_a_nested_exclusion()
+ {
+ // Arrange
+ var subject = new
+ {
+ Text = "Actual",
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ Text = "Text"
+ },
+ new
+ {
+ Number = 2,
+ Text = "Actual"
+ }
+ }
+ }
+ };
+
+ var expected = new
+ {
+ Text = "Actual",
+ Level = new
+ {
+ Collection = new[]
+ {
+ new
+ {
+ Number = 1,
+ Text = "Text"
+ },
+ new
+ {
+ Number = 3,
+ Text = "Expected"
+ }
+ }
+ }
+ };
+
+ // Act / Assert
+ subject.Should().BeEquivalentTo(expected,
+ options => options
+ .For(x => x.Level.Collection).Exclude(x => x.Text)
+ .For(x => x.Level.Collection).Exclude(x => x.Number));
+ }
+ }
+
[Fact]
public void When_a_dictionary_property_is_detected_it_should_ignore_the_order_of_the_pairs()
{
diff --git a/docs/_pages/objectgraphs.md b/docs/_pages/objectgraphs.md
index a21abb76ed..fc37bf3496 100644
--- a/docs/_pages/objectgraphs.md
+++ b/docs/_pages/objectgraphs.md
@@ -130,6 +130,23 @@ orderDto.Should().BeEquivalentTo(order, options =>
options.Excluding(o => o.Products[1].Status));
```
+You can use `For` and `Exclude` if you want to exclude a member on each nested object regardless of its index.
+
+```csharp
+orderDto.Should().BeEquivalentTo(order, options =>
+ options.For(o => o.Products)
+ .Exclude(o => o.Status));
+```
+
+Using `For` you can navigate arbitrarily deep. Consider a `Product` has a collection of `Part`s and a `Part` has a name. Using `For` your can also exclude the `Name` of all `Part`s of all `Product`s.
+
+```csharp
+orderDto.Should().BeEquivalentTo(order, options =>
+ options.For(o => o.Products)
+ .For(o => o.Parts)
+ .Exclude(o => o.Name));
+```
+
Of course, `Excluding()` and `ExcludingMissingMembers()` can be combined.
You can also take a different approach and explicitly tell Fluent Assertions which members to include. You can directly specify a property expression or use a predicate that acts on the provided `ISubjectInfo`.
diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md
index ff728d4bb2..4953f2f24f 100644
--- a/docs/_pages/releases.md
+++ b/docs/_pages/releases.md
@@ -13,6 +13,7 @@ sidebar:
* Add `BeDefined` and `NotBeDefined` to assert on existence of an enum value - [#1888](https://github.com/fluentassertions/fluentassertions/pull/1888)
* Added the ability to exclude fields & properties marked as non-browsable in the code editor from structural equality comparisons - [#1807](https://github.com/fluentassertions/fluentassertions/pull/1807) & [#1812](https://github.com/fluentassertions/fluentassertions/pull/1812)
* Assertions on the collection types in System.Data (`DataSet.Tables`, `DataTable.Columns`, `DataTable.Rows`) have been restored - [#1812](https://github.com/fluentassertions/fluentassertions/pull/1812)
+* Add `For`/`Exclude` to allow exclusion of members inside a collection - [#1782](https://github.com/fluentassertions/fluentassertions/pull/1782)
## 6.6.0