From ff3c58c974d4563a4992278ada4642fac9f95828 Mon Sep 17 00:00:00 2001 From: Sebastian Krysmanski Date: Tue, 30 Apr 2024 17:27:12 +0200 Subject: [PATCH] Add support for open generics to TypeExtensions.Is() --- .../Extensions/TypeExtensions.cs | 82 +++++++++++++++++++ .../Tests/Extensions/TypeExtensionsTests.cs | 41 ++++++++++ 2 files changed, 123 insertions(+) diff --git a/src/AppMotor.Core/Extensions/TypeExtensions.cs b/src/AppMotor.Core/Extensions/TypeExtensions.cs index ae576b8d..0ad6fc02 100644 --- a/src/AppMotor.Core/Extensions/TypeExtensions.cs +++ b/src/AppMotor.Core/Extensions/TypeExtensions.cs @@ -233,6 +233,10 @@ public static bool IsNullableValueType(this Type type) /// aims to be better understandable. /// /// + /// The non-generic version of this method () also supports open + /// generic types (e.g. IList<> ) for . + /// + /// /// Unlike , the parameter /// can't be null here. /// @@ -249,6 +253,10 @@ public static bool Is(this Type typeToCheck) /// aims to be better understandable. /// /// + /// This method also supports open generic types (e.g. IList<> ) - both + /// for as well as this type. + /// + /// /// Unlike , the parameter /// can't be null here. /// @@ -258,7 +266,81 @@ public static bool Is(this Type typeToCheck, Type baseOrInterfaceType) Validate.ArgumentWithName(nameof(typeToCheck)).IsNotNull(typeToCheck); Validate.ArgumentWithName(nameof(baseOrInterfaceType)).IsNotNull(baseOrInterfaceType); + // This includes enums, arrays, and value types. + if (baseOrInterfaceType.IsSealed) + { + return false; + } + + if (baseOrInterfaceType.IsGenericTypeDefinition) + { + // + // Open generic type (e.g. "IList<>"). + // + // NOTE: At the time of writing (.NET 8/C# 12) it's NOT possible to have a partially open generics + // (e.g. "IDictionary"). + // + if (baseOrInterfaceType.IsInterface) + { + var baseTypeFullName = baseOrInterfaceType.FullName!; + + // + // Try "Type.GetInterface()" first, if possible. + // + // "Type.GetInterface()" doesn't work for nested classes (detectable by the "+" in the type name). + if (!baseTypeFullName.Contains('+')) + { + try + { + var implementedInterface = typeToCheck.GetInterface(baseTypeFullName); + return implementedInterface?.Assembly == baseOrInterfaceType.Assembly; + } + catch (AmbiguousMatchException) + { + // This type implements multiple interfaces (with either different type arguments or + // from different assemblies) with the same name. + // + // NOTE: Because the search in "Type.GetInterface()" only looks at the type's full name + // but not at the assembly, we can NOT assume "return true;" in case of this exception + // (as the exception may be caused by an interface with the same name but from a different + // assembly). + } + } + + // + // Use "Type.FindInterfaces()" if "Type.GetInterface()" couldn't be used. + // + return typeToCheck.FindInterfaces(FindOpenGenericInterfaceFilter, baseOrInterfaceType).Length != 0; + } + else + { + var baseType = typeToCheck.BaseType; + + while (baseType is not null && baseType != typeof(object)) + { + if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == baseOrInterfaceType) + { + return true; + } + + baseType = baseType.BaseType; + } + } + } + return baseOrInterfaceType.IsAssignableFrom(typeToCheck); + + static bool FindOpenGenericInterfaceFilter(Type interfaceTypeToCheck, object? criteria) + { + if (!interfaceTypeToCheck.IsGenericType) + { + // We only get here for open generic types. So if "interfaceTypeToCheck" is not a generic type, + // it can't be the type we're looking for. + return false; + } + + return interfaceTypeToCheck.GetGenericTypeDefinition().Equals(criteria); + } } /// diff --git a/tests/AppMotor.Core.Tests/Tests/Extensions/TypeExtensionsTests.cs b/tests/AppMotor.Core.Tests/Tests/Extensions/TypeExtensionsTests.cs index 14b4459a..01e74703 100644 --- a/tests/AppMotor.Core.Tests/Tests/Extensions/TypeExtensionsTests.cs +++ b/tests/AppMotor.Core.Tests/Tests/Extensions/TypeExtensionsTests.cs @@ -1,11 +1,14 @@ // SPDX-License-Identifier: MIT // Copyright AppMotor Framework (https://github.com/skrysmanski/AppMotor) +using System.Collections; using System.Numerics; using AppMotor.Core.DataModel; using AppMotor.Core.Extensions; +using JetBrains.Annotations; + using Shouldly; using Xunit; @@ -178,12 +181,50 @@ public void Test_Is() typeof(ClassB).Is(typeof(int)).ShouldBe(false); } + [Fact] + public void Test_Is_OpenGeneric() + { + typeof(List).Is(typeof(IReadOnlyCollection<>)).ShouldBe(true); + typeof(List<>).Is(typeof(IReadOnlyCollection<>)).ShouldBe(true); + typeof(List<>).Is(typeof(IReadOnlyCollection)).ShouldBe(false); + typeof(List<>).Is(typeof(object)).ShouldBe(true); + typeof(List<>).Is(typeof(int)).ShouldBe(false); + + typeof(GenericClassB).Is(typeof(IGenericTestInterface<>)).ShouldBe(true); + typeof(GenericClassB).Is(typeof(GenericClassA<>)).ShouldBe(true); + + typeof(IGenericTestInterface<>).Is(typeof(object)).ShouldBe(true); + + typeof(GenericStructA).Is(typeof(IGenericTestInterface<>)).ShouldBe(true); + } + private interface ITestInterface; private class ClassA : ITestInterface; private class ClassB : ClassA; + private interface IGenericTestInterface + { + [UsedImplicitly] // only required to exist + void DoSomething(T value); + } + + private class GenericClassA : IGenericTestInterface + { + public void DoSomething(TValue1 value) => throw new NotSupportedException(); + } + + private class GenericClassB : GenericClassA, IGenericTestInterface + { + public void DoSomething(TValue2 value) => throw new NotSupportedException(); + } + + private struct GenericStructA : IGenericTestInterface + { + public void DoSomething(TValue value) => throw new NotSupportedException(); + } + [Theory] [InlineData("+", UnaryOperators.UnaryPlus)] [InlineData("-", UnaryOperators.UnaryNegation)]