Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow anonymous object for selecting fields/properties at Exclude and Include #2488

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 26 additions & 5 deletions Src/FluentAssertions/Common/ExpressionExtensions.cs
Expand Up @@ -38,13 +38,14 @@ internal static class ExpressionExtensions
/// </example>
/// <exception cref="ArgumentNullException"><paramref name="expression"/> is <see langword="null"/>.</exception>
#pragma warning disable MA0051
public static MemberPath GetMemberPath<TDeclaringType, TPropertyType>(
public static IEnumerable<MemberPath> GetMemberPaths<TDeclaringType, TPropertyType>(
this Expression<Func<TDeclaringType, TPropertyType>> expression)
#pragma warning restore MA0051
{
Guard.ThrowIfArgumentIsNull(expression, nameof(expression), "Expected an expression, but found <null>.");

var segments = new List<string>();
var selectors = new List<string>();
var declaringTypes = new List<Type>();
Expression node = expression;

Expand Down Expand Up @@ -95,21 +96,41 @@ internal static class ExpressionExtensions
node = methodCallExpression.Object;
segments.Add("[" + argumentExpression.Value + "]");
break;
case ExpressionType.New:
var newExpression = (NewExpression)node;

foreach (var member in newExpression.Arguments)
{
selectors.Add(string.Join(".", member.ToString().Split('.').Skip(1)));
declaringTypes.Add(((MemberExpression)member).Member.DeclaringType);
}

node = null;
break;

default:
throw new ArgumentException(GetUnsupportedExpressionMessage(expression.Body), nameof(expression));
}
}

// If any members were accessed in the expression, the first one found is the last member.
Type declaringType = declaringTypes.FirstOrDefault() ?? typeof(TDeclaringType);

IEnumerable<string> reversedSegments = segments.AsEnumerable().Reverse();
string segmentPath = string.Join(".", reversedSegments);
if (selectors.Count == 0)
{
// If any members were accessed in the expression, the first one found is the last member.
IEnumerable<string> reversedSegments = segments.AsEnumerable().Reverse();
string segmentPath = string.Join(".", reversedSegments);

return new List<MemberPath> { new(typeof(TDeclaringType), declaringType, segmentPath.Replace(".[", "[", StringComparison.Ordinal)) };
}

return new MemberPath(typeof(TDeclaringType), declaringType, segmentPath.Replace(".[", "[", StringComparison.Ordinal));
return selectors.Select(selector => new MemberPath(typeof(TDeclaringType), declaringType, selector.Replace(".[", "[", StringComparison.Ordinal)));
}

public static MemberPath GetMemberPath<TDeclaringType, TPropertyType>(
this Expression<Func<TDeclaringType, TPropertyType>> expression) =>
expression.GetMemberPaths().First();

/// <summary>
/// Validates that the expression can be used to construct a <see cref="MemberPath"/>.
/// </summary>
Expand Down
19 changes: 14 additions & 5 deletions Src/FluentAssertions/Equivalency/EquivalencyOptions.cs
Expand Up @@ -29,7 +29,11 @@ public EquivalencyOptions(IEquivalencyOptions defaults)
/// </summary>
public EquivalencyOptions<TExpectation> Excluding(Expression<Func<TExpectation, object>> expression)
{
AddSelectionRule(new ExcludeMemberByPathSelectionRule(expression.GetMemberPath()));
foreach (var memberPath in expression.GetMemberPaths())
{
AddSelectionRule(new ExcludeMemberByPathSelectionRule(memberPath));
}

return this;
}

Expand All @@ -40,9 +44,8 @@ public EquivalencyOptions<TExpectation> Excluding(Expression<Func<TExpectation,
public NestedExclusionOptionBuilder<TExpectation, TNext> For<TNext>(
Expression<Func<TExpectation, IEnumerable<TNext>>> expression)
{
var selectionRule = new ExcludeMemberByPathSelectionRule(expression.GetMemberPath());
AddSelectionRule(selectionRule);
return new NestedExclusionOptionBuilder<TExpectation, TNext>(this, selectionRule);
return new NestedExclusionOptionBuilder<TExpectation, TNext>(
this, new ExcludeMemberByPathSelectionRule(expression.GetMemberPath()));
}

/// <summary>
Expand All @@ -53,7 +56,11 @@ public EquivalencyOptions<TExpectation> Excluding(Expression<Func<TExpectation,
/// </remarks>
public EquivalencyOptions<TExpectation> Including(Expression<Func<TExpectation, object>> expression)
{
AddSelectionRule(new IncludeMemberByPathSelectionRule(expression.GetMemberPath()));
foreach (var memberPath in expression.GetMemberPaths())
{
AddSelectionRule(new IncludeMemberByPathSelectionRule(memberPath));
}

return this;
}

Expand All @@ -77,10 +84,12 @@ public EquivalencyOptions<TExpectation> Including(Expression<Func<TExpectation,
Expression<Func<TExpectation, object>> expression)
{
string expressionMemberPath = expression.GetMemberPath().ToString();

OrderingRules.Add(new PathBasedOrderingRule(expressionMemberPath)
{
Invert = true
});

return this;
}

Expand Down
10 changes: 8 additions & 2 deletions Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs
Expand Up @@ -27,8 +27,14 @@ public class NestedExclusionOptionBuilder<TExpectation, TCurrent>
/// </summary>
public EquivalencyOptions<TExpectation> Exclude(Expression<Func<TCurrent, object>> expression)
{
var nextPath = expression.GetMemberPath();
currentPathSelectionRule.AppendPath(nextPath);
var currentSelectionPath = currentPathSelectionRule.CurrentPath;

foreach (var path in expression.GetMemberPaths())
{
var newPath = currentSelectionPath.AsParentCollectionOf(path);
capturedOptions.AddSelectionRule(new ExcludeMemberByPathSelectionRule(newPath));
}

return capturedOptions;
}

Expand Down
Expand Up @@ -27,6 +27,8 @@ public void AppendPath(MemberPath nextPath)
memberToExclude = memberToExclude.AsParentCollectionOf(nextPath);
}

public MemberPath CurrentPath => memberToExclude;

public override string ToString()
{
return "Exclude member " + memberToExclude;
Expand Down
Expand Up @@ -803,7 +803,7 @@ private void RemoveSelectionRule<T>()
selectionRules.RemoveAll(selectionRule => selectionRule is T);
}

protected TSelf AddSelectionRule(IMemberSelectionRule selectionRule)
protected internal TSelf AddSelectionRule(IMemberSelectionRule selectionRule)
{
selectionRules.Add(selectionRule);
return (TSelf)this;
Expand Down