Skip to content

Commit

Permalink
Add support for open generics to TypeExtensions.Is()
Browse files Browse the repository at this point in the history
  • Loading branch information
skrysmanski committed Apr 30, 2024
1 parent 2cc4436 commit ff3c58c
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/AppMotor.Core/Extensions/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ public static bool IsNullableValueType(this Type type)
/// aims to be better understandable.
/// </summary>
/// <remarks>
/// The non-generic version of this method (<see cref="Is"/>) also supports open
/// generic types (e.g. <c>IList&lt;&gt;</c> ) for <typeparamref name="TBaseOrInterfaceType"/>.
/// </remarks>
/// <remarks>
/// Unlike <see cref="Type.IsAssignableFrom"/>, the parameter <paramref name="typeToCheck"/>
/// can't be <c>null</c> here.
/// </remarks>
Expand All @@ -249,6 +253,10 @@ public static bool Is<TBaseOrInterfaceType>(this Type typeToCheck)
/// aims to be better understandable.
/// </summary>
/// <remarks>
/// This method also supports open generic types (e.g. <c>IList&lt;&gt;</c> ) - both
/// for <paramref name="baseOrInterfaceType"/> as well as this type.
/// </remarks>
/// <remarks>
/// Unlike <see cref="Type.IsAssignableFrom"/>, the parameter <paramref name="typeToCheck"/>
/// can't be <c>null</c> here.
/// </remarks>
Expand All @@ -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<string,>").
//
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);
}
}

/// <summary>
Expand Down
41 changes: 41 additions & 0 deletions tests/AppMotor.Core.Tests/Tests/Extensions/TypeExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -178,12 +181,50 @@ public void Test_Is()
typeof(ClassB).Is(typeof(int)).ShouldBe(false);
}

[Fact]
public void Test_Is_OpenGeneric()
{
typeof(List<string>).Is(typeof(IReadOnlyCollection<>)).ShouldBe(true);
typeof(List<>).Is(typeof(IReadOnlyCollection<>)).ShouldBe(true);
typeof(List<>).Is(typeof(IReadOnlyCollection<string>)).ShouldBe(false);
typeof(List<>).Is(typeof(object)).ShouldBe(true);
typeof(List<>).Is(typeof(int)).ShouldBe(false);

typeof(GenericClassB<int, string>).Is(typeof(IGenericTestInterface<>)).ShouldBe(true);
typeof(GenericClassB<int, string>).Is(typeof(GenericClassA<>)).ShouldBe(true);

typeof(IGenericTestInterface<>).Is(typeof(object)).ShouldBe(true);

typeof(GenericStructA<string>).Is(typeof(IGenericTestInterface<>)).ShouldBe(true);
}

private interface ITestInterface;

private class ClassA : ITestInterface;

private class ClassB : ClassA;

private interface IGenericTestInterface<in T>
{
[UsedImplicitly] // only required to exist
void DoSomething(T value);
}

private class GenericClassA<TValue1> : IGenericTestInterface<TValue1>
{
public void DoSomething(TValue1 value) => throw new NotSupportedException();
}

private class GenericClassB<TValue1, TValue2> : GenericClassA<TValue1>, IGenericTestInterface<TValue2>
{
public void DoSomething(TValue2 value) => throw new NotSupportedException();
}

private struct GenericStructA<TValue> : IGenericTestInterface<TValue>
{
public void DoSomething(TValue value) => throw new NotSupportedException();
}

[Theory]
[InlineData("+", UnaryOperators.UnaryPlus)]
[InlineData("-", UnaryOperators.UnaryNegation)]
Expand Down

0 comments on commit ff3c58c

Please sign in to comment.