diff --git a/Src/Core/Core.projitems b/Src/Core/Core.projitems
index 3e2a71a122..6f45d1209a 100644
--- a/Src/Core/Core.projitems
+++ b/Src/Core/Core.projitems
@@ -66,6 +66,7 @@
+
diff --git a/Src/Core/Equivalency/CollectionMemberAssertionOptionsDecorator.cs b/Src/Core/Equivalency/CollectionMemberAssertionOptionsDecorator.cs
index cd51676bd7..e657bfed9c 100644
--- a/Src/Core/Equivalency/CollectionMemberAssertionOptionsDecorator.cs
+++ b/Src/Core/Equivalency/CollectionMemberAssertionOptionsDecorator.cs
@@ -16,14 +16,13 @@ public CollectionMemberAssertionOptionsDecorator(IEquivalencyAssertionOptions in
this.inner = inner;
}
- public IEnumerable SelectionRules
+ public IEnumerable GetActiveSelectionRules(IEquivalencyValidationContext context)
{
- get
- {
- return inner.SelectionRules.Select(rule => new CollectionMemberSelectionRuleDecorator(rule)).ToArray();
- }
+ return inner.GetActiveSelectionRules(context).Select(rule => new CollectionMemberSelectionRuleDecorator(rule)).ToArray();
}
+ public IEnumerable SelectionRules => inner.SelectionRules;
+
public IEnumerable MatchingRules
{
get { return inner.MatchingRules.Select(rule => new CollectionMemberMatchingRuleDecorator(rule)).ToArray(); }
diff --git a/Src/Core/Equivalency/EquivalencyValidator.cs b/Src/Core/Equivalency/EquivalencyValidator.cs
index 3e3f0e9599..dd311d5669 100644
--- a/Src/Core/Equivalency/EquivalencyValidator.cs
+++ b/Src/Core/Equivalency/EquivalencyValidator.cs
@@ -50,9 +50,18 @@ public void AssertEqualityUsing(IEquivalencyValidationContext context)
if (!objectTracker.IsCyclicReference(new ObjectReference(context.Subject, context.SelectedMemberPath)))
{
- bool wasHandled = AssertionOptions.EquivalencySteps
- .Where(s => s.CanHandle(context, config))
- .Any(step => step.Handle(context, this, config));
+ bool wasHandled = false;
+ foreach (var step in AssertionOptions.EquivalencySteps)
+ {
+ if (step.CanHandle(context, config))
+ {
+ if (step.Handle(context, this, config))
+ {
+ wasHandled = true;
+ break;
+ }
+ }
+ }
if (!wasHandled)
{
diff --git a/Src/Core/Equivalency/IConditionalRule.cs b/Src/Core/Equivalency/IConditionalRule.cs
new file mode 100644
index 0000000000..8c4dff97cd
--- /dev/null
+++ b/Src/Core/Equivalency/IConditionalRule.cs
@@ -0,0 +1,10 @@
+namespace FluentAssertions.Equivalency
+{
+ ///
+ /// Allows a to indicate it applies only in a certain context.
+ ///
+ public interface IConditionalRule
+ {
+ bool Applies(IEquivalencyValidationContext context);
+ }
+}
\ No newline at end of file
diff --git a/Src/Core/Equivalency/IEquivalencyAssertionOptions.cs b/Src/Core/Equivalency/IEquivalencyAssertionOptions.cs
index 14aae4fd62..b0486debb6 100644
--- a/Src/Core/Equivalency/IEquivalencyAssertionOptions.cs
+++ b/Src/Core/Equivalency/IEquivalencyAssertionOptions.cs
@@ -9,7 +9,12 @@ namespace FluentAssertions.Equivalency
public interface IEquivalencyAssertionOptions
{
///
- /// Gets an ordered collection of selection rules that define what properties are included.
+ /// Gets an ordered collection of the selection rules that are relevant given the provided context.
+ ///
+ IEnumerable GetActiveSelectionRules(IEquivalencyValidationContext context);
+
+ ///
+ /// Gets an ordered collection of all selection rules that have been configured.
///
IEnumerable SelectionRules { get; }
diff --git a/Src/Core/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs b/Src/Core/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs
index 8fe395db07..ae3d504a21 100644
--- a/Src/Core/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs
+++ b/Src/Core/Equivalency/Selection/CollectionMemberSelectionRuleDecorator.cs
@@ -11,10 +11,7 @@ public CollectionMemberSelectionRuleDecorator(IMemberSelectionRule selectionRule
this.selectionRule = selectionRule;
}
- public bool IncludesMembers
- {
- get { return selectionRule.IncludesMembers; }
- }
+ public bool IncludesMembers => selectionRule.IncludesMembers;
public IEnumerable SelectMembers(IEnumerable selectedMembers,
ISubjectInfo context, IEquivalencyAssertionOptions config)
diff --git a/Src/Core/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs b/Src/Core/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
index 641981b48b..bf59f3303c 100644
--- a/Src/Core/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
+++ b/Src/Core/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
@@ -7,7 +7,7 @@ namespace FluentAssertions.Equivalency.Selection
///
/// Selection rule that includes a particular property in the structural comparison.
///
- internal class IncludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule
+ internal class IncludeMemberByPathSelectionRule : SelectMemberByPathSelectionRule, IConditionalRule
{
private readonly MemberPath pathToInclude;
@@ -18,6 +18,11 @@ public IncludeMemberByPathSelectionRule(string pathToInclude) : base(pathToInclu
public override bool IncludesMembers => true;
+ public bool Applies(IEquivalencyValidationContext context)
+ {
+ return context.IsRoot || pathToInclude.IsDeepPath;
+ }
+
protected override IEnumerable OnSelectMembers(IEnumerable selectedMembers,
string currentPath, ISubjectInfo context)
{
diff --git a/Src/Core/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs b/Src/Core/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs
index 41786a3b77..9dd272ef96 100644
--- a/Src/Core/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs
+++ b/Src/Core/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs
@@ -20,10 +20,7 @@ public IncludeMemberByPredicateSelectionRule(Expression
this.predicate = predicate.Compile();
}
- public bool IncludesMembers
- {
- get { return true; }
- }
+ public bool IncludesMembers => true;
public IEnumerable SelectMembers(IEnumerable selectedMembers, ISubjectInfo context, IEquivalencyAssertionOptions config)
{
diff --git a/Src/Core/Equivalency/Selection/MemberPath.cs b/Src/Core/Equivalency/Selection/MemberPath.cs
index 87499b5e2d..9fc908323b 100644
--- a/Src/Core/Equivalency/Selection/MemberPath.cs
+++ b/Src/Core/Equivalency/Selection/MemberPath.cs
@@ -20,5 +20,7 @@ public bool StartsWith(string subPath)
string[] subPathSegments = subPath.Split('.');
return segments.Take(subPathSegments.Length).SequenceEqual(subPathSegments);
}
+
+ public bool IsDeepPath => segments.Count > 1;
}
}
\ No newline at end of file
diff --git a/Src/Core/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs b/Src/Core/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
index cb75ced0f2..7aee8638db 100644
--- a/Src/Core/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
+++ b/Src/Core/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
@@ -88,95 +88,71 @@ protected SelfReferenceEquivalencyAssertionOptions(IEquivalencyAssertionOptions
///
/// Gets an ordered collection of selection rules that define what members are included.
///
- IEnumerable IEquivalencyAssertionOptions.SelectionRules
+ ///
+ IEnumerable IEquivalencyAssertionOptions.GetActiveSelectionRules(IEquivalencyValidationContext context)
{
- get
+ IEnumerable activeRules =
+ from rule in selectionRules
+ let conditionRule = rule as IConditionalRule
+ where (conditionRule == null) || conditionRule.Applies(context)
+ select rule;
+
+ bool hasConflictingRules = activeRules.ToArray().Any(rule => rule.IncludesMembers);
+
+ if (includeProperties && !hasConflictingRules)
{
- bool hasConflictingRules = selectionRules.Any(rule => rule.IncludesMembers);
-
- if (includeProperties && !hasConflictingRules)
- {
- yield return new AllPublicPropertiesSelectionRule();
- }
+ yield return new AllPublicPropertiesSelectionRule();
+ }
- if (includeFields && !hasConflictingRules)
- {
- yield return new AllPublicFieldsSelectionRule();
- }
+ if (includeFields && !hasConflictingRules)
+ {
+ yield return new AllPublicFieldsSelectionRule();
+ }
- foreach (IMemberSelectionRule rule in selectionRules)
- {
- yield return rule;
- }
+ foreach (IMemberSelectionRule rule in activeRules.ToArray())
+ {
+ yield return rule;
}
}
+ public IEnumerable SelectionRules => selectionRules;
+
///
/// Gets an ordered collection of matching rules that determine which subject members are matched with which
/// expectation members.
///
- IEnumerable IEquivalencyAssertionOptions.MatchingRules
- {
- get { return matchingRules; }
- }
+ IEnumerable IEquivalencyAssertionOptions.MatchingRules => matchingRules;
///
/// Gets an ordered collection of Equivalency steps how a subject is compared with the expectation.
///
- IEnumerable IEquivalencyAssertionOptions.UserEquivalencySteps
- {
- get { return userEquivalencySteps; }
- }
+ IEnumerable IEquivalencyAssertionOptions.UserEquivalencySteps => userEquivalencySteps;
///
/// Gets an ordered collection of rules that determine whether or not the order of collections is important. By default,
/// ordering is irrelevant.
///
- OrderingRuleCollection IEquivalencyAssertionOptions.OrderingRules
- {
- get { return orderingRules; }
- }
+ OrderingRuleCollection IEquivalencyAssertionOptions.OrderingRules => orderingRules;
///
/// Gets value indicating whether the equality check will include nested collections and complex types.
///
- bool IEquivalencyAssertionOptions.IsRecursive
- {
- get { return isRecursive; }
- }
+ bool IEquivalencyAssertionOptions.IsRecursive => isRecursive;
- bool IEquivalencyAssertionOptions.AllowInfiniteRecursion
- {
- get { return allowInfiniteRecursion; }
- }
+ bool IEquivalencyAssertionOptions.AllowInfiniteRecursion => allowInfiniteRecursion;
///
/// Gets value indicating how cyclic references should be handled. By default, it will throw an exception.
///
- CyclicReferenceHandling IEquivalencyAssertionOptions.CyclicReferenceHandling
- {
- get { return cyclicReferenceHandling; }
- }
+ CyclicReferenceHandling IEquivalencyAssertionOptions.CyclicReferenceHandling => cyclicReferenceHandling;
- EnumEquivalencyHandling IEquivalencyAssertionOptions.EnumEquivalencyHandling
- {
- get { return enumEquivalencyHandling; }
- }
+ EnumEquivalencyHandling IEquivalencyAssertionOptions.EnumEquivalencyHandling => enumEquivalencyHandling;
- bool IEquivalencyAssertionOptions.UseRuntimeTyping
- {
- get { return useRuntimeTyping; }
- }
+ bool IEquivalencyAssertionOptions.UseRuntimeTyping => useRuntimeTyping;
- bool IEquivalencyAssertionOptions.IncludeProperties
- {
- get { return includeProperties; }
- }
+ bool IEquivalencyAssertionOptions.IncludeProperties => includeProperties;
- bool IEquivalencyAssertionOptions.IncludeFields
- {
- get { return includeFields; }
- }
+ bool IEquivalencyAssertionOptions.IncludeFields => includeFields;
///
/// Gets a value indicating whether the should be treated as having value semantics.
@@ -360,7 +336,6 @@ public TSelf IgnoringCyclicReferences()
return (TSelf)this;
}
-
///
/// Disables limitations on recursion depth when the structural equality check is configured to include nested objects
///
@@ -409,7 +384,7 @@ public TSelf Using(IMemberMatchingRule matchingRule)
public TSelf Using(IAssertionRule assertionRule)
{
userEquivalencySteps.Insert(0, new AssertionRuleEquivalencyStepAdapter(assertionRule));
- return (TSelf) this;
+ return (TSelf)this;
}
///
@@ -462,7 +437,7 @@ public TSelf WithoutStrictOrderingFor(Expression> predi
public TSelf ComparingEnumsByName()
{
enumEquivalencyHandling = EnumEquivalencyHandling.ByName;
- return (TSelf) this;
+ return (TSelf)this;
}
///
@@ -474,7 +449,7 @@ public TSelf ComparingEnumsByName()
public TSelf ComparingEnumsByValue()
{
enumEquivalencyHandling = EnumEquivalencyHandling.ByValue;
- return (TSelf) this;
+ return (TSelf)this;
}
///
@@ -484,7 +459,7 @@ public TSelf ComparingEnumsByValue()
public TSelf ComparingByValue()
{
valueTypes.Add(typeof(T));
- return (TSelf) this;
+ return (TSelf)this;
}
#region Non-fluent API
@@ -505,19 +480,19 @@ private void ClearMatchingRules()
protected TSelf AddSelectionRule(IMemberSelectionRule selectionRule)
{
selectionRules.Add(selectionRule);
- return (TSelf) this;
+ return (TSelf)this;
}
private TSelf AddMatchingRule(IMemberMatchingRule matchingRule)
{
matchingRules.Insert(0, matchingRule);
- return (TSelf) this;
+ return (TSelf)this;
}
private TSelf AddEquivalencyStep(IEquivalencyStep equivalencyStep)
{
userEquivalencySteps.Add(equivalencyStep);
- return (TSelf) this;
+ return (TSelf)this;
}
#endregion
@@ -612,4 +587,4 @@ public TSelf When(Expression> predicate)
}
}
}
-}
+}
\ No newline at end of file
diff --git a/Src/Core/Equivalency/StructuralEqualityEquivalencyStep.cs b/Src/Core/Equivalency/StructuralEqualityEquivalencyStep.cs
index 899278f68a..598af4f815 100644
--- a/Src/Core/Equivalency/StructuralEqualityEquivalencyStep.cs
+++ b/Src/Core/Equivalency/StructuralEqualityEquivalencyStep.cs
@@ -78,7 +78,7 @@ private static SelectedMemberInfo FindMatchFor(SelectedMemberInfo selectedMember
{
IEnumerable members = Enumerable.Empty();
- foreach (var selectionRule in config.SelectionRules)
+ foreach (var selectionRule in config.GetActiveSelectionRules(context))
{
members = selectionRule.SelectMembers(members, context, config);
}
diff --git a/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs b/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs
index 51771a4bdd..a0e8187e29 100644
--- a/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs
+++ b/Tests/FluentAssertions.Shared.Specs/BasicEquivalencySpecs.cs
@@ -574,6 +574,49 @@ public void When_including_a_property_it_should_exactly_match_the_property()
act.ShouldNotThrow();
}
+ public class CustomType
+ {
+ public string Name { get; set; }
+ }
+
+ public class ClassA
+ {
+ public List ListOfCustomTypes { get; set; }
+ }
+
+ [TestMethod]
+ public void When_including_a_property_using_an_expression_it_should_only_apply_it_to_the_root_level()
+ {
+ //-----------------------------------------------------------------------------------------------------------
+ // Arrange
+ //-----------------------------------------------------------------------------------------------------------
+ var list1 = new List
+ {
+ new CustomType {Name = "A"},
+ new CustomType {Name = "B"}
+ };
+
+ var list2 = new List
+ {
+ new CustomType {Name = "C"},
+ new CustomType {Name = "D"}
+ };
+
+ var objectA = new ClassA { ListOfCustomTypes = list1 };
+ var objectB = new ClassA { ListOfCustomTypes = list2 };
+
+ //-----------------------------------------------------------------------------------------------------------
+ // Act
+ //-----------------------------------------------------------------------------------------------------------
+ Action act = () => objectA.ShouldBeEquivalentTo(objectB, options => options.Including(x => x.ListOfCustomTypes));
+
+ //-----------------------------------------------------------------------------------------------------------
+ // Assert
+ //-----------------------------------------------------------------------------------------------------------
+ act.ShouldThrow().
+ WithMessage("*C*but*A*D*but*B*");
+ }
+
[TestMethod]
public void When_null_is_provided_as_property_expression_it_should_throw()
{