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/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.GetProperties.cs b/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.GetProperties.cs new file mode 100644 index 0000000000..21fe19a1c6 --- /dev/null +++ b/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.GetProperties.cs @@ -0,0 +1,198 @@ +#if NETCOREAPP3_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Reflection; +using FluentAssertions.Equivalency; +using Xunit; + +namespace FluentAssertions.Specs.Common; + +public partial class TypeExtensionsSpecs +{ + public class GetProperties + { + [Fact] + public void Can_get_all_public_explicit_and_default_interface_properties() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.Public | MemberVisibility.ExplicitlyImplemented | + MemberVisibility.DefaultInterfaceProperties); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "NormalProperty", PropertyType = typeof(string) }, + new { Name = "NewProperty", PropertyType = typeof(int) }, + new { Name = "InterfaceProperty", PropertyType = typeof(string) }, + new + { + Name = $"{typeof(IInterfaceWithSingleProperty).FullName!.Replace("+", ".")}.ExplicitlyImplementedProperty", + PropertyType = typeof(string) + }, + new { Name = "DefaultProperty", PropertyType = typeof(string) } + }); + } + + [Fact] + public void Can_get_normal_public_properties() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.Public); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "NormalProperty", PropertyType = typeof(string) }, + new { Name = "NewProperty", PropertyType = typeof(int) }, + new { Name = "InterfaceProperty", PropertyType = typeof(string) }, + }); + } + + [Fact] + public void Can_get_explicit_properties_only() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.ExplicitlyImplemented); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new + { + Name = $"{typeof(IInterfaceWithSingleProperty).FullName!.Replace("+", ".")}.ExplicitlyImplementedProperty", + PropertyType = typeof(string) + }, + }); + } + + [Fact] + public void Can_get_default_interface_properties_only() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.DefaultInterfaceProperties); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "DefaultProperty", PropertyType = typeof(string) }, + }); + } + + [Fact] + public void Can_get_internal_properties() + { + // Act + var properties = typeof(SuperClass) + .GetProperties2(MemberVisibility.Internal); + + // Assert + properties.Should().BeEquivalentTo(new[] + { + new { Name = "InternalProperty", PropertyType = typeof(bool) }, + }); + } + + private class SuperClass : BaseClass, IInterfaceWithDefaultProperty + { + public string NormalProperty { get; set; } + + public new int NewProperty { get; set; } + + internal bool InternalProperty { get; set; } + + string IInterfaceWithSingleProperty.ExplicitlyImplementedProperty { get; set; } + + public string InterfaceProperty { get; set; } + } + + private class BaseClass + { + public string NewProperty { get; set; } + } + + private interface IInterfaceWithDefaultProperty : IInterfaceWithSingleProperty + { + string InterfaceProperty { get; set; } + + string DefaultProperty => "Default"; + } + + private interface IInterfaceWithSingleProperty + { + string ExplicitlyImplementedProperty { get; set; } + } + } +} + +internal static class TypeReflector +{ + public static PropertyInfo[] GetProperties2(this Type type, MemberVisibility visibility) + { + // start with type + // iterate over all properties (including new) + // add explicitly implemented properties + // for each interface in the graph, recursively add default properties + // continue with base until base = object + var collectedProperties = new HashSet(); + var properties = new List(); + + // Start with the given type and iterate up the inheritance chain + while (type != null && type != typeof(object)) + { + // Add all properties declared in the current type (including new ones) + if (visibility.HasFlag(MemberVisibility.Public) || visibility.HasFlag(MemberVisibility.Internal) || + visibility.HasFlag(MemberVisibility.ExplicitlyImplemented)) + { + foreach (var prop in type + .GetProperties(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | + BindingFlags.NonPublic)) + { + if (!collectedProperties.Contains(prop.Name) && (HasVisibility(visibility, prop) || + (visibility.HasFlag(MemberVisibility.ExplicitlyImplemented) && IsExplicitlyImplemented(prop)))) + { + properties.Add(prop); + collectedProperties.Add(prop.Name); + } + } + } + + if (visibility.HasFlag(MemberVisibility.DefaultInterfaceProperties)) + { + // Add explicitly implemented interface properties (not included above) + var interfaces = type.GetInterfaces(); + + foreach (var iface in interfaces) + { + foreach (var prop in iface.GetProperties()) + { + if (!collectedProperties.Contains(prop.Name)) + { + properties.Add(prop); + collectedProperties.Add(prop.Name); + } + } + } + } + + // Move to the base type + type = type.BaseType; + } + + return properties.ToArray(); + } + + private static bool IsExplicitlyImplemented(PropertyInfo prop) + { + return prop.Name.Contains('.', StringComparison.InvariantCultureIgnoreCase); + } + + private static bool HasVisibility(MemberVisibility visibility, PropertyInfo prop) => + (visibility.HasFlag(MemberVisibility.Public) && prop.GetMethod?.IsPublic is true) || + (visibility.HasFlag(MemberVisibility.Internal) && prop.GetMethod?.IsAssembly is true); +} + +#endif 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..24fdf10663 100644 --- a/Tests/FluentAssertions.Specs/Types/TypeExtensionsSpecs.cs +++ b/Tests/FluentAssertions.Specs/Common/TypeExtensionsSpecs.cs @@ -7,9 +7,9 @@ using FluentAssertions.Common; using Xunit; -namespace FluentAssertions.Specs.Types; +namespace FluentAssertions.Specs.Common; -public class TypeExtensionsSpecs +public partial class TypeExtensionsSpecs { [Fact] public void When_comparing_types_and_types_are_same_it_should_return_true() 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