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)]