diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
index 0cc64a64ed..09b8a34c49 100644
--- a/.nuke/build.schema.json
+++ b/.nuke/build.schema.json
@@ -1,53 +1,99 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
- "$ref": "#/definitions/build",
- "title": "Build Schema",
+ "properties": {
+ "Configuration": {
+ "type": "string",
+ "description": "The solution configuration to build. Default is 'Debug' (local) or 'CI' (server)",
+ "enum": [
+ "CI",
+ "Debug"
+ ]
+ },
+ "GenerateBinLog": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Use this parameter if you encounter build problems in any way, to generate a .binlog file which holds some useful information"
+ },
+ "NuGetApiKey": {
+ "type": "string",
+ "description": "The key to push to Nuget",
+ "default": "Secrets must be entered via 'nuke :secrets [profile]'"
+ },
+ "Solution": {
+ "type": "string",
+ "description": "Path to a solution file that is automatically loaded"
+ }
+ },
"definitions": {
- "build": {
- "type": "object",
+ "Host": {
+ "type": "string",
+ "enum": [
+ "AppVeyor",
+ "AzurePipelines",
+ "Bamboo",
+ "Bitbucket",
+ "Bitrise",
+ "GitHubActions",
+ "GitLab",
+ "Jenkins",
+ "Rider",
+ "SpaceAutomation",
+ "TeamCity",
+ "Terminal",
+ "TravisCI",
+ "VisualStudio",
+ "VSCode"
+ ]
+ },
+ "ExecutableTarget": {
+ "type": "string",
+ "enum": [
+ "ApiChecks",
+ "CalculateNugetVersion",
+ "Clean",
+ "CodeCoverage",
+ "Compile",
+ "InstallNode",
+ "Pack",
+ "Push",
+ "Restore",
+ "SpellCheck",
+ "TestFrameworks",
+ "UnitTests",
+ "UnitTestsNet47",
+ "UnitTestsNet6OrGreater"
+ ]
+ },
+ "Verbosity": {
+ "type": "string",
+ "description": "",
+ "enum": [
+ "Verbose",
+ "Normal",
+ "Minimal",
+ "Quiet"
+ ]
+ },
+ "NukeBuild": {
"properties": {
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
},
- "GenerateBinLog": {
- "type": "boolean",
- "description": "Use this parameter if you encounter build problems in any way, to generate a .binlog file which holds some useful information"
- },
"Help": {
"type": "boolean",
"description": "Shows the help text for this build assembly"
},
"Host": {
- "type": "string",
"description": "Host for execution. Default is 'automatic'",
- "enum": [
- "AppVeyor",
- "AzurePipelines",
- "Bamboo",
- "Bitbucket",
- "Bitrise",
- "GitHubActions",
- "GitLab",
- "Jenkins",
- "Rider",
- "SpaceAutomation",
- "TeamCity",
- "Terminal",
- "TravisCI",
- "VisualStudio",
- "VSCode"
- ]
+ "$ref": "#/definitions/Host"
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
},
- "NuGetApiKey": {
- "type": "string",
- "description": "The key to push to Nuget",
- "default": "Secrets must be entered via 'nuke :secrets [profile]'"
- },
"Partition": {
"type": "string",
"description": "Partition to use on CI"
@@ -71,61 +117,22 @@
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
- "type": "string",
- "enum": [
- "ApiChecks",
- "CalculateNugetVersion",
- "Clean",
- "CodeCoverage",
- "Compile",
- "Pack",
- "Push",
- "Restore",
- "SpellCheck",
- "TestFrameworks",
- "UnitTests",
- "UnitTestsNetCore",
- "UnitTestsNetFramework"
- ]
+ "$ref": "#/definitions/ExecutableTarget"
}
},
- "Solution": {
- "type": "string",
- "description": "Path to a solution file that is automatically loaded"
- },
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
- "type": "string",
- "enum": [
- "ApiChecks",
- "CalculateNugetVersion",
- "Clean",
- "CodeCoverage",
- "Compile",
- "Pack",
- "Push",
- "Restore",
- "SpellCheck",
- "TestFrameworks",
- "UnitTests",
- "UnitTestsNetCore",
- "UnitTestsNetFramework"
- ]
+ "$ref": "#/definitions/ExecutableTarget"
}
},
"Verbosity": {
- "type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
- "enum": [
- "Minimal",
- "Normal",
- "Quiet",
- "Verbose"
- ]
+ "$ref": "#/definitions/Verbosity"
}
}
}
- }
+ },
+ "$ref": "#/definitions/NukeBuild"
}
diff --git a/Build/Build.cs b/Build/Build.cs
index 261ee82390..e32b4f662c 100644
--- a/Build/Build.cs
+++ b/Build/Build.cs
@@ -259,6 +259,7 @@ void ReportTestOutcome(params string[] globFilters)
ReportTypes.lcov,
ReportTypes.HtmlInline_AzurePipelines_Dark)
.AddFileFilters("-*.g.cs")
+ .AddFileFilters("-*/.nuget/*.cs")
.SetAssemblyFilters("+FluentAssertions"));
string link = TestResultsDirectory / "reports" / "index.html";
diff --git a/Build/_build.csproj.DotSettings b/Build/_build.csproj.DotSettings
index 9aac7d8e8d..28494fb0c6 100644
--- a/Build/_build.csproj.DotSettings
+++ b/Build/_build.csproj.DotSettings
@@ -13,6 +13,8 @@
False
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy>
True
True
True
@@ -21,4 +23,5 @@
True
True
True
- True
+ True
+ True
diff --git a/FluentAssertions.sln.DotSettings b/FluentAssertions.sln.DotSettings
index 692c8c68da..c20c8d78f0 100644
--- a/FluentAssertions.sln.DotSettings
+++ b/FluentAssertions.sln.DotSettings
@@ -104,6 +104,8 @@
UseExplicitType
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy>
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
@@ -154,26 +156,29 @@
True
True
True
+ True
D:\Workspaces\FluentAssertions\Default.testsettings
4
False
True
- True
- 1
- True
- 0
+ False
+ True
+ 0
+
+ False
+
aaa
Arrange-Act-Assert
- [TestMethod]
-public void When_$scenario$_it_should_$behavior$()
-{
- // Arrange
- $END$
-
- // Act
-
-
- // Assert
+ [Fact]
+public void $Fact$()
+{
+ // Arrange
+ $END$
+
+ // Act
+
+
+ // Assert
}
True
True
diff --git a/Src/FluentAssertions/Common/TypeExtensions.cs b/Src/FluentAssertions/Common/TypeExtensions.cs
index a84af752ad..5999054d7a 100644
--- a/Src/FluentAssertions/Common/TypeExtensions.cs
+++ b/Src/FluentAssertions/Common/TypeExtensions.cs
@@ -7,6 +7,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using FluentAssertions.Equivalency;
+using Reflectify;
namespace FluentAssertions.Common;
@@ -22,9 +23,6 @@ internal static class TypeExtensions
private static readonly ConcurrentDictionary TypeIsRecordCache = new();
private static readonly ConcurrentDictionary TypeIsCompilerGeneratedCache = new();
- private static readonly ConcurrentDictionary<(Type Type, MemberVisibility Visibility), TypeMemberReflector>
- TypeMemberReflectorsCache = new();
-
public static bool IsDecoratedWith(this Type type)
where TAttribute : Attribute
{
@@ -184,7 +182,7 @@ public static bool OverridesEquals(this Type type)
///
public static PropertyInfo FindProperty(this Type type, string propertyName, MemberVisibility memberVisibility)
{
- var properties = type.GetProperties(memberVisibility);
+ var properties = type.GetProperties(memberVisibility.ToMemberKind());
return Array.Find(properties, p =>
p.Name == propertyName || p.Name.EndsWith("." + propertyName, StringComparison.Ordinal));
@@ -198,32 +196,11 @@ public static PropertyInfo FindProperty(this Type type, string propertyName, Mem
///
public static FieldInfo FindField(this Type type, string fieldName, MemberVisibility memberVisibility)
{
- var fields = type.GetFields(memberVisibility);
+ var fields = type.GetFields(memberVisibility.ToMemberKind());
return Array.Find(fields, p => p.Name == fieldName);
}
- public static MemberInfo[] GetMembers(this Type typeToReflect, MemberVisibility visibility)
- {
- return GetTypeReflectorFor(typeToReflect, visibility).Members;
- }
-
- public static PropertyInfo[] GetProperties(this Type typeToReflect, MemberVisibility visibility)
- {
- return GetTypeReflectorFor(typeToReflect, visibility).Properties;
- }
-
- public static FieldInfo[] GetFields(this Type typeToReflect, MemberVisibility visibility)
- {
- return GetTypeReflectorFor(typeToReflect, visibility).Fields;
- }
-
- private static TypeMemberReflector GetTypeReflectorFor(Type typeToReflect, MemberVisibility visibility)
- {
- return TypeMemberReflectorsCache.GetOrAdd((typeToReflect, visibility),
- static key => new TypeMemberReflector(key.Type, key.Visibility));
- }
-
///
/// Check if the type is declared as abstract.
///
diff --git a/Src/FluentAssertions/Common/TypeMemberReflector.cs b/Src/FluentAssertions/Common/TypeMemberReflector.cs
deleted file mode 100644
index ea65969cdf..0000000000
--- a/Src/FluentAssertions/Common/TypeMemberReflector.cs
+++ /dev/null
@@ -1,169 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using FluentAssertions.Equivalency;
-
-namespace FluentAssertions.Common;
-
-///
-/// Helper class to get all the public and internal fields and properties from a type.
-///
-internal sealed class TypeMemberReflector
-{
- private const BindingFlags AllInstanceMembersFlag =
- BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
-
- public TypeMemberReflector(Type typeToReflect, MemberVisibility visibility)
- {
- Properties = LoadProperties(typeToReflect, visibility);
- Fields = LoadFields(typeToReflect, visibility);
- Members = Properties.Concat(Fields).ToArray();
- }
-
- public MemberInfo[] Members { get; }
-
- public PropertyInfo[] Properties { get; }
-
- public FieldInfo[] Fields { get; }
-
- private static PropertyInfo[] LoadProperties(Type typeToReflect, MemberVisibility visibility)
- {
- List query = GetPropertiesFromHierarchy(typeToReflect, visibility);
-
- return query.ToArray();
- }
-
- private static List GetPropertiesFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility)
- {
- bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal);
- bool includeExplicitlyImplemented = memberVisibility.HasFlag(MemberVisibility.ExplicitlyImplemented);
-
- return GetMembersFromHierarchy(typeToReflect, type =>
- {
- return
- from p in type.GetProperties(AllInstanceMembersFlag | BindingFlags.DeclaredOnly)
- where p.GetMethod is { } getMethod
- && (IsPublic(getMethod) || (includeExplicitlyImplemented && IsExplicitlyImplemented(getMethod)))
- && (includeInternal || !IsInternal(getMethod))
- && !p.IsIndexer()
- orderby IsExplicitImplementation(p)
- select p;
- });
- }
-
- private static bool IsPublic(MethodBase getMethod) =>
- !getMethod.IsPrivate && !getMethod.IsFamily && !getMethod.IsFamilyAndAssembly;
-
- private static bool IsExplicitlyImplemented(MethodBase getMethod) =>
- getMethod.IsPrivate && getMethod.IsFinal;
-
- private static bool IsInternal(MethodBase getMethod) =>
- getMethod.IsAssembly || getMethod.IsFamilyOrAssembly;
-
- private static bool IsExplicitImplementation(PropertyInfo property)
- {
- return property.GetMethod!.IsPrivate &&
- property.SetMethod?.IsPrivate != false &&
- property.Name.Contains('.', StringComparison.Ordinal);
- }
-
- private static FieldInfo[] LoadFields(Type typeToReflect, MemberVisibility visibility)
- {
- List query = GetFieldsFromHierarchy(typeToReflect, visibility);
-
- return query.ToArray();
- }
-
- private static List GetFieldsFromHierarchy(Type typeToReflect, MemberVisibility memberVisibility)
- {
- bool includeInternal = memberVisibility.HasFlag(MemberVisibility.Internal);
-
- return GetMembersFromHierarchy(typeToReflect, type =>
- {
- return type
- .GetFields(AllInstanceMembersFlag)
- .Where(field => IsPublic(field))
- .Where(field => includeInternal || !IsInternal(field));
- });
- }
-
- private static bool IsPublic(FieldInfo field) =>
- !field.IsPrivate && !field.IsFamily && !field.IsFamilyAndAssembly;
-
- private static bool IsInternal(FieldInfo field)
- {
- return field.IsAssembly || field.IsFamilyOrAssembly;
- }
-
- private static List GetMembersFromHierarchy(
- Type typeToReflect,
- Func> getMembers)
- where TMemberInfo : MemberInfo
- {
- if (typeToReflect.IsInterface)
- {
- return GetInterfaceMembers(typeToReflect, getMembers);
- }
-
- return GetClassMembers(typeToReflect, getMembers);
- }
-
- private static List GetInterfaceMembers(Type typeToReflect,
- Func> getMembers)
- where TMemberInfo : MemberInfo
- {
- List members = new();
-
- var considered = new List();
- var queue = new Queue();
- considered.Add(typeToReflect);
- queue.Enqueue(typeToReflect);
-
- while (queue.Count > 0)
- {
- Type subType = queue.Dequeue();
-
- foreach (Type subInterface in subType.GetInterfaces())
- {
- if (considered.Contains(subInterface))
- {
- continue;
- }
-
- considered.Add(subInterface);
- queue.Enqueue(subInterface);
- }
-
- IEnumerable typeMembers = getMembers(subType);
-
- IEnumerable newPropertyInfos = typeMembers.Where(x => !members.Contains(x));
-
- members.InsertRange(0, newPropertyInfos);
- }
-
- return members;
- }
-
- private static List GetClassMembers(Type typeToReflect,
- Func> getMembers)
- where TMemberInfo : MemberInfo
- {
- List members = new();
-
- while (typeToReflect != null)
- {
- foreach (var memberInfo in getMembers(typeToReflect))
- {
- if (members.TrueForAll(mi => mi.Name != memberInfo.Name))
- {
- members.Add(memberInfo);
- }
- }
-
- typeToReflect = typeToReflect.BaseType;
- }
-
- return members;
- }
-}
diff --git a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
index 62a68d4fb9..52c2cacff2 100644
--- a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
+++ b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
@@ -17,7 +17,7 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
{
PropertyInfo propertyInfo = subject.GetType().FindProperty(
expectedMember.Name,
- options.IncludedProperties | MemberVisibility.ExplicitlyImplemented);
+ options.IncludedProperties | MemberVisibility.ExplicitlyImplemented | MemberVisibility.DefaultInterfaceProperties);
subjectMember = propertyInfo is not null && !propertyInfo.IsIndexer() ? new Property(propertyInfo, parent) : null;
}
diff --git a/Src/FluentAssertions/Equivalency/MemberVisibility.cs b/Src/FluentAssertions/Equivalency/MemberVisibility.cs
index e64b798a81..a0eddebf20 100644
--- a/Src/FluentAssertions/Equivalency/MemberVisibility.cs
+++ b/Src/FluentAssertions/Equivalency/MemberVisibility.cs
@@ -12,5 +12,6 @@ public enum MemberVisibility
None = 0,
Internal = 1,
Public = 2,
- ExplicitlyImplemented = 4
+ ExplicitlyImplemented = 4,
+ DefaultInterfaceProperties = 8
}
diff --git a/Src/FluentAssertions/Equivalency/MemberVisibilityExtensions.cs b/Src/FluentAssertions/Equivalency/MemberVisibilityExtensions.cs
new file mode 100644
index 0000000000..df035de556
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/MemberVisibilityExtensions.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Concurrent;
+using Reflectify;
+
+namespace FluentAssertions.Equivalency;
+
+internal static class MemberVisibilityExtensions
+{
+ private static readonly ConcurrentDictionary Cache = new();
+
+ public static MemberKind ToMemberKind(this MemberVisibility visibility)
+ {
+ return Cache.GetOrAdd(visibility, v =>
+ {
+ MemberKind result = MemberKind.None;
+
+ foreach (MemberVisibility flag in Enum.GetValues(typeof(MemberVisibility)))
+ {
+ if (v.HasFlag(flag))
+ {
+ var convertedFlag = flag switch
+ {
+ MemberVisibility.None => MemberKind.None,
+ MemberVisibility.Internal => MemberKind.Internal,
+ MemberVisibility.Public => MemberKind.Public,
+ MemberVisibility.ExplicitlyImplemented => MemberKind.ExplicitlyImplemented,
+ MemberVisibility.DefaultInterfaceProperties => MemberKind.DefaultInterfaceProperties,
+ _ => throw new ArgumentOutOfRangeException(nameof(v), v, null)
+ };
+
+ result |= convertedFlag;
+ }
+ }
+
+ return result;
+ });
+ }
+}
diff --git a/Src/FluentAssertions/Equivalency/Selection/AllFieldsSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/AllFieldsSelectionRule.cs
index fadd165609..505f71cce7 100644
--- a/Src/FluentAssertions/Equivalency/Selection/AllFieldsSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/AllFieldsSelectionRule.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
-using FluentAssertions.Common;
+using Reflectify;
namespace FluentAssertions.Equivalency.Selection;
@@ -15,7 +15,7 @@ public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedFields = context.Type
- .GetFields(context.IncludedFields)
+ .GetFields(context.IncludedFields.ToMemberKind())
.Select(info => new Field(info, currentNode));
return selectedMembers.Union(selectedFields).ToList();
diff --git a/Src/FluentAssertions/Equivalency/Selection/AllPropertiesSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/AllPropertiesSelectionRule.cs
index ba61049c62..803bbbf219 100644
--- a/Src/FluentAssertions/Equivalency/Selection/AllPropertiesSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/AllPropertiesSelectionRule.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Linq;
-using FluentAssertions.Common;
+using Reflectify;
namespace FluentAssertions.Equivalency.Selection;
@@ -14,8 +14,11 @@ internal class AllPropertiesSelectionRule : IMemberSelectionRule
public IEnumerable SelectMembers(INode currentNode, IEnumerable selectedMembers,
MemberSelectionContext context)
{
+ MemberVisibility visibility = context.IncludedProperties | MemberVisibility.ExplicitlyImplemented |
+ MemberVisibility.DefaultInterfaceProperties;
+
IEnumerable selectedProperties = context.Type
- .GetProperties(context.IncludedProperties)
+ .GetProperties(visibility.ToMemberKind())
.Select(info => new Property(context.Type, info, currentNode));
return selectedMembers.Union(selectedProperties).ToList();
diff --git a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
index 7646069aa2..4ed440b5a5 100644
--- a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPathSelectionRule.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Reflection;
using FluentAssertions.Common;
+using Reflectify;
namespace FluentAssertions.Equivalency.Selection;
@@ -21,7 +22,7 @@ public IncludeMemberByPathSelectionRule(MemberPath pathToInclude)
protected override void AddOrRemoveMembersFrom(List selectedMembers, INode parent, string parentPath,
MemberSelectionContext context)
{
- foreach (MemberInfo memberInfo in context.Type.GetMembers(MemberVisibility.Public | MemberVisibility.Internal))
+ foreach (MemberInfo memberInfo in context.Type.GetMembers(MemberKind.Public | MemberKind.Internal))
{
var memberPath = new MemberPath(context.Type, memberInfo.DeclaringType, parentPath.Combine(memberInfo.Name));
diff --git a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs
index 97000aad92..ac484eff03 100644
--- a/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs
+++ b/Src/FluentAssertions/Equivalency/Selection/IncludeMemberByPredicateSelectionRule.cs
@@ -3,6 +3,7 @@
using System.Linq.Expressions;
using System.Reflection;
using FluentAssertions.Common;
+using Reflectify;
namespace FluentAssertions.Equivalency.Selection;
@@ -27,8 +28,8 @@ public IEnumerable SelectMembers(INode currentNode, IEnumerable(selectedMembers);
- foreach (MemberInfo memberInfo in currentNode.Type.GetMembers(MemberVisibility.Public |
- MemberVisibility.Internal))
+ foreach (MemberInfo memberInfo in currentNode.Type.GetMembers(MemberKind.Public |
+ MemberKind.Internal))
{
IMember member = MemberFactory.Create(memberInfo, currentNode);
diff --git a/Src/FluentAssertions/FluentAssertions.csproj b/Src/FluentAssertions/FluentAssertions.csproj
index 48a22d046e..161a1145c1 100644
--- a/Src/FluentAssertions/FluentAssertions.csproj
+++ b/Src/FluentAssertions/FluentAssertions.csproj
@@ -12,7 +12,7 @@
true
true
-
+
Dennis Doomen;Jonas Nyrup
@@ -31,7 +31,7 @@
See https://fluentassertions.com/releases/
Copyright Dennis Doomen 2010-$([System.DateTime]::Now.ToString(yyyy))
-
+
<_Parameter1>FluentAssertions.Specs, PublicKey=00240000048000009400000006020000002400005253413100040000010001002d25ff515c85b13ba08f61d466cff5d80a7f28ba197bbf8796085213e7a3406f970d2a4874932fed35db546e89af2da88c194bf1b7f7ac70de7988c78406f7629c547283061282a825616eb7eb48a9514a7570942936020a9bb37dca9ff60b778309900851575614491c6d25018fadb75828f4c7a17bf2d7dc86e7b6eafc5d8f
@@ -53,6 +53,10 @@
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
@@ -120,5 +124,5 @@
-
+
diff --git a/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs b/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs
index 93775f7c99..28da03d6dc 100644
--- a/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs
+++ b/Src/FluentAssertions/Formatting/DefaultValueFormatter.cs
@@ -2,7 +2,7 @@
using System.Linq;
using System.Reflection;
using FluentAssertions.Common;
-using FluentAssertions.Equivalency;
+using Reflectify;
namespace FluentAssertions.Formatting;
@@ -56,7 +56,7 @@ public void Format(object value, FormattedObjectGraph formattedGraph, Formatting
/// The default is all non-private members.
protected virtual MemberInfo[] GetMembers(Type type)
{
- return type.GetMembers(MemberVisibility.Public);
+ return type.GetMembers(MemberKind.Public);
}
private static bool HasCompilerGeneratedToStringImplementation(object value)
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
index 89da67fff2..bc72e1fbec 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt
@@ -975,6 +975,7 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
+ DefaultInterfaceProperties = 8,
}
public class NestedExclusionOptionBuilder
{
@@ -2802,4 +2803,4 @@ namespace FluentAssertions.Xml
public bool CanHandle(object value) { }
public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { }
}
-}
+}
\ No newline at end of file
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt
index 0db0cc0251..5adccdf720 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt
@@ -988,6 +988,7 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
+ DefaultInterfaceProperties = 8,
}
public class NestedExclusionOptionBuilder
{
@@ -2932,4 +2933,4 @@ namespace FluentAssertions.Xml
public bool CanHandle(object value) { }
public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { }
}
-}
+}
\ No newline at end of file
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
index e76973c182..3df6aee5fb 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt
@@ -975,6 +975,7 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
+ DefaultInterfaceProperties = 8,
}
public class NestedExclusionOptionBuilder
{
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
index e76973c182..3df6aee5fb 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt
@@ -975,6 +975,7 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
+ DefaultInterfaceProperties = 8,
}
public class NestedExclusionOptionBuilder
{
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
index d14c1711b3..7995d53641 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt
@@ -968,6 +968,7 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
+ DefaultInterfaceProperties = 8,
}
public class NestedExclusionOptionBuilder
{
@@ -2753,4 +2754,4 @@ namespace FluentAssertions.Xml
public bool CanHandle(object value) { }
public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { }
}
-}
+}
\ No newline at end of file
diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
index 99eb81a1e3..3df6aee5fb 100644
--- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
+++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt
@@ -975,6 +975,7 @@ namespace FluentAssertions.Equivalency
Internal = 1,
Public = 2,
ExplicitlyImplemented = 4,
+ DefaultInterfaceProperties = 8,
}
public class NestedExclusionOptionBuilder
{
@@ -2804,4 +2805,4 @@ namespace FluentAssertions.Xml
public bool CanHandle(object value) { }
public void Format(object value, FluentAssertions.Formatting.FormattedObjectGraph formattedGraph, FluentAssertions.Formatting.FormattingContext context, FluentAssertions.Formatting.FormatChild formatChild) { }
}
-}
+}
\ No newline at end of file
diff --git a/Tests/FluentAssertions.Equivalency.Specs/FluentAssertions.Equivalency.Specs.csproj b/Tests/FluentAssertions.Equivalency.Specs/FluentAssertions.Equivalency.Specs.csproj
index 92f192b555..834982361b 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/FluentAssertions.Equivalency.Specs.csproj
+++ b/Tests/FluentAssertions.Equivalency.Specs/FluentAssertions.Equivalency.Specs.csproj
@@ -7,6 +7,7 @@
false
$(NoWarn),IDE0052,1573,1591,1712
full
+ 12
diff --git a/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs b/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs
index 0b02f9b911..24a48f9a24 100644
--- a/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs
+++ b/Tests/FluentAssertions.Equivalency.Specs/SelectionRulesSpecs.cs
@@ -301,6 +301,36 @@ internal class BaseClassPointingToClassWithoutProperties
internal class ClassWithoutProperty
{
}
+
+#if NETCOREAPP3_0_OR_GREATER
+ [Fact]
+ public void Will_include_default_interface_properties_in_the_comparison()
+ {
+ var lista = new List
+ {
+ new Test { Name = "Test" }
+ };
+
+ List listb = new()
+ {
+ new Test { Name = "Test" }
+ };
+
+ lista.Should().BeEquivalentTo(listb);
+ }
+
+ private class Test : ITest
+ {
+ public string Name { get; set; }
+ }
+
+ private interface ITest
+ {
+ public string Name { get; }
+
+ public int NameLength => Name.Length;
+ }
+#endif
}
public class Including
@@ -607,6 +637,63 @@ public void When_both_field_and_properties_are_configured_for_inclusion_both_sho
// Assert
act.Should().Throw().Which.Message.Should().Contain("Field1").And.Contain("Property1");
}
+
+#if NETCOREAPP3_0_OR_GREATER
+ [Fact]
+ public void Can_include_a_default_interface_property_using_an_expression()
+ {
+ // Arrange
+ IHaveDefaultProperty subject = new ClassReceivedDefaultInterfaceProperty
+ {
+ NormalProperty = "Value"
+ };
+
+ IHaveDefaultProperty expectation = new ClassReceivedDefaultInterfaceProperty
+ {
+ NormalProperty = "Another Value"
+ };
+
+ // Act
+ var act = () => subject.Should().BeEquivalentTo(expectation, x => x.Including(p => p.DefaultProperty));
+
+ // Assert
+ act.Should().Throw().WithMessage("Expected property subject.DefaultProperty to be 13, but found 5.*");
+ }
+
+ [Fact]
+ public void Can_include_a_default_interface_property_using_a_name()
+ {
+ // Arrange
+ IHaveDefaultProperty subject = new ClassReceivedDefaultInterfaceProperty
+ {
+ NormalProperty = "Value"
+ };
+
+ IHaveDefaultProperty expectation = new ClassReceivedDefaultInterfaceProperty
+ {
+ NormalProperty = "Another Value"
+ };
+
+ // Act
+ var act = () => subject.Should().BeEquivalentTo(expectation,
+ x => x.Including(p => p.Name.Contains("DefaultProperty")));
+
+ // Assert
+ act.Should().Throw().WithMessage("Expected property subject.DefaultProperty to be 13, but found 5.*");
+ }
+
+ private class ClassReceivedDefaultInterfaceProperty : IHaveDefaultProperty
+ {
+ public string NormalProperty { get; set; }
+ }
+
+ private interface IHaveDefaultProperty
+ {
+ string NormalProperty { get; set; }
+
+ int DefaultProperty => NormalProperty.Length;
+ }
+#endif
}
public class Excluding
@@ -1236,6 +1323,65 @@ public void When_excluding_virtual_or_abstract_property_exclusion_works_properly
.Excluding(o => o.VirtualProperty)
.Excluding(o => o.DerivedProperty2));
}
+
+#if NETCOREAPP3_0_OR_GREATER
+ [Fact]
+ public void Can_exclude_a_default_interface_property_using_an_expression()
+ {
+ // Arrange
+ IHaveDefaultProperty subject = new ClassReceivedDefaultInterfaceProperty
+ {
+ NormalProperty = "Value"
+ };
+
+ IHaveDefaultProperty expectation = new ClassReceivedDefaultInterfaceProperty
+ {
+ NormalProperty = "Another Value"
+ };
+
+ // Act
+ var act = () => subject.Should().BeEquivalentTo(expectation,
+ x => x.Excluding(p => p.DefaultProperty));
+
+ // Assert
+ act.Should().Throw().Which.Message.Should().NotContain("subject.DefaultProperty");
+ }
+
+ [Fact]
+ public void Can_exclude_a_default_interface_property_using_a_name()
+ {
+ // Arrange
+ IHaveDefaultProperty subject = new ClassReceivedDefaultInterfaceProperty
+ {
+ NormalProperty = "Value"
+ };
+
+ IHaveDefaultProperty expectation = new ClassReceivedDefaultInterfaceProperty
+ {
+ NormalProperty = "Another Value"
+ };
+
+ // Act
+ var act = () => subject.Should().BeEquivalentTo(expectation,
+ x => x.Excluding(info => info.Name.Contains("DefaultProperty")));
+
+ // Assert
+ act.Should().Throw().Which.Message.Should().NotContain("subject.DefaultProperty");
+ }
+
+ private class ClassReceivedDefaultInterfaceProperty : IHaveDefaultProperty
+ {
+ public string NormalProperty { get; set; }
+ }
+
+ private interface IHaveDefaultProperty
+ {
+ string NormalProperty { get; set; }
+
+ int DefaultProperty => NormalProperty.Length;
+ }
+#endif
+
}
public class Accessibility
@@ -1918,7 +2064,8 @@ public void Explicitly_implemented_subject_properties_are_ignored_if_a_normal_pr
}
[Fact]
- public void Explicitly_implemented_read_only_subject_properties_are_ignored_if_a_normal_property_exists_with_the_same_name()
+ public void
+ Explicitly_implemented_read_only_subject_properties_are_ignored_if_a_normal_property_exists_with_the_same_name()
{
// Arrange
IReadOnlyVehicle subject = new ExplicitReadOnlyVehicle(explicitValue: 1)
@@ -1982,6 +2129,34 @@ public void Explicitly_implemented_subject_properties_are_ignored_if_only_fields
.ExcludingMissingMembers());
}
+ [Fact]
+ public void Normal_properties_have_priority_over_explicitly_implemented_properties()
+ {
+ var instance = new MyClass
+ {
+ MyError = 42,
+ };
+
+ var other = new MyClass
+ {
+ MyError = 42,
+ };
+
+ instance.Should().BeEquivalentTo(other);
+ }
+
+ private class MyClass : Exception, IMyInterface
+ {
+ public int MyError { get; set; }
+
+ int IMyInterface.Message => MyError;
+ }
+
+ private interface IMyInterface
+ {
+ int Message { get; }
+ }
+
[Fact]
public void Excluding_missing_members_does_not_affect_how_explicitly_implemented_subject_properties_are_dealt_with()
{
@@ -2547,7 +2722,8 @@ public void Only_ignore_non_browsable_matching_members()
};
// Act
- Action action = () => subject.Should().BeEquivalentTo(expectation, config => config.IgnoringNonBrowsableMembersOnSubject());
+ Action action = () =>
+ subject.Should().BeEquivalentTo(expectation, config => config.IgnoringNonBrowsableMembersOnSubject());
// Assert
action.Should().Throw();
diff --git a/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs b/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.cs
similarity index 99%
rename from Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs
rename to Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.cs
index 73ef4247ed..a1ed9b664c 100644
--- a/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.cs
@@ -7,7 +7,7 @@
using FluentAssertions.Common;
using Xunit;
-namespace FluentAssertions.Specs.Types;
+namespace FluentAssertions.Specs.Common;
public class TypeExtensionsSpecs
{
diff --git a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj
index d5c5a52b90..b2b56399a7 100644
--- a/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj
+++ b/Tests/FluentAssertions.Specs/FluentAssertions.Specs.csproj
@@ -7,6 +7,7 @@
false
$(NoWarn),IDE0052,1573,1591,1712,CS8002
full
+ 12
diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md
index bab190b1d1..dacd0b0866 100644
--- a/docs/_pages/releases.md
+++ b/docs/_pages/releases.md
@@ -7,13 +7,18 @@ sidebar:
nav: "sidebar"
---
+## 6.12.2
+
+### Fixes
+* Better handling of normal vs explicitly implemented vs default interface properties - [2794](https://github.com/fluentassertions/fluentassertions/pull/2794)
+
## 6.12.1
### Improvements
* Improve `BeEmpty()` and `BeNullOrEmpty()` performance for `IEnumerable`, by materializing only the first item - [#2530](https://github.com/fluentassertions/fluentassertions/pull/2530)
### Fixes
-* Fixed formatting error when checking nullable `DateTimeOffset` with
+* Fixed formatting error when checking nullable `DateTimeOffset` with
`BeWithin(...).Before(...)` - [#2312](https://github.com/fluentassertions/fluentassertions/pull/2312)
* `BeEquivalentTo` will now find and can map subject properties that are implemented through an explicitly-implemented interface - [#2152](https://github.com/fluentassertions/fluentassertions/pull/2152)
* Fixed that the `because` and `becauseArgs` were not passed down the equivalency tree - [#2318](https://github.com/fluentassertions/fluentassertions/pull/2318)