diff --git a/src/main/java/org/springframework/data/repository/query/Parameter.java b/src/main/java/org/springframework/data/repository/query/Parameter.java
index 5fbf81ba8b..a439c2ab77 100644
--- a/src/main/java/org/springframework/data/repository/query/Parameter.java
+++ b/src/main/java/org/springframework/data/repository/query/Parameter.java
@@ -17,12 +17,12 @@
 
 import static java.lang.String.*;
 
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
-import java.util.stream.Stream;
 
 import org.springframework.core.MethodParameter;
 import org.springframework.core.ResolvableType;
@@ -214,15 +214,17 @@ private static boolean isDynamicProjectionParameter(MethodParameter parameter) {
 			return false;
 		}
 
-		ResolvableType returnType = ResolvableType.forMethodReturnType(parameter.getMethod());
+		Method method = parameter.getMethod();
 
-		if (TypeInformation.of(returnType).isCollectionLike()
-				|| org.springframework.util.ClassUtils.isAssignable(Stream.class, returnType.toClass())) {
-			returnType = returnType.getGeneric(0);
+		if (method == null) {
+			throw new IllegalArgumentException("Parameter is not associated with any method");
 		}
 
-		ResolvableType type = ResolvableType.forMethodParameter(parameter);
-		return returnType.getType().equals(type.getGeneric(0).getType());
+		TypeInformation<?> returnType = TypeInformation.fromReturnTypeOf(method);
+		TypeInformation<?> unwrapped = QueryExecutionConverters.unwrapWrapperTypes(returnType);
+		TypeInformation<?> reactiveUnwrapped = ReactiveWrapperConverters.unwrapWrapperTypes(unwrapped);
+
+		return reactiveUnwrapped.equals(TypeInformation.fromMethodParameter(parameter).getComponentType());
 	}
 
 	/**
diff --git a/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/src/main/java/org/springframework/data/util/TypeDiscoverer.java
index 03e6eecaa5..480d362e71 100644
--- a/src/main/java/org/springframework/data/util/TypeDiscoverer.java
+++ b/src/main/java/org/springframework/data/util/TypeDiscoverer.java
@@ -34,7 +34,6 @@
 import org.springframework.core.convert.TypeDescriptor;
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
-import org.springframework.util.ClassUtils;
 import org.springframework.util.ConcurrentLruCache;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.ReflectionUtils;
@@ -197,7 +196,7 @@ public TypeDescriptor toTypeDescriptor() {
 
 	@Override
 	public ClassTypeInformation<?> getRawTypeInformation() {
-		return new ClassTypeInformation<>(ResolvableType.forRawClass(resolvableType.getRawClass()));
+		return new ClassTypeInformation<>(ResolvableType.forRawClass(resolvableType.toClass()));
 	}
 
 	@Nullable
@@ -323,7 +322,7 @@ public boolean equals(@Nullable Object o) {
 			return true;
 		}
 
-		if ((o == null) || !ClassUtils.isAssignable(getClass(), o.getClass())) {
+		if ((o == null) || !ObjectUtils.nullSafeEquals(getClass(), o.getClass())) {
 			return false;
 		}
 
@@ -346,7 +345,11 @@ public boolean equals(@Nullable Object o) {
 
 	@Override
 	public int hashCode() {
-		return ObjectUtils.nullSafeHashCode(resolvableType.toClass());
+
+		int result = 31 * getClass().hashCode();
+		result += 31 * getType().hashCode();
+
+		return result;
 	}
 
 	@Override
diff --git a/src/main/java/org/springframework/data/util/TypeInformation.java b/src/main/java/org/springframework/data/util/TypeInformation.java
index 4423d267af..ec638f9b0b 100644
--- a/src/main/java/org/springframework/data/util/TypeInformation.java
+++ b/src/main/java/org/springframework/data/util/TypeInformation.java
@@ -17,11 +17,13 @@
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
+import java.lang.reflect.TypeVariable;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import org.springframework.core.MethodParameter;
 import org.springframework.core.ResolvableType;
 import org.springframework.core.convert.TypeDescriptor;
 import org.springframework.lang.Nullable;
@@ -61,9 +63,11 @@ public static TypeInformation<?> of(ResolvableType type) {
 
 		Assert.notNull(type, "Type must not be null");
 
-		return type.hasGenerics() || (type.isArray() && type.getComponentType().hasGenerics()) //
-				? TypeDiscoverer.td(type)
-				: ClassTypeInformation.cti(type);
+		return type.hasGenerics()
+				|| (type.isArray() && type.getComponentType().hasGenerics()) //
+				|| (type.getType() instanceof TypeVariable)
+						? TypeDiscoverer.td(type)
+						: ClassTypeInformation.cti(type);
 	}
 
 	/**
@@ -103,7 +107,23 @@ public static TypeInformation<?> fromReturnTypeOf(Method method) {
 	 * @since 3.0
 	 */
 	public static TypeInformation<?> fromReturnTypeOf(Method method, @Nullable Class<?> type) {
-		return ClassTypeInformation.fromReturnTypeOf(method, type);
+
+		ResolvableType intermediate = type == null
+				? ResolvableType.forMethodReturnType(method)
+				: ResolvableType.forMethodReturnType(method, type);
+
+		return TypeInformation.of(intermediate);
+	}
+
+	/**
+	 * Returns a new {@link TypeInformation} for the given {@link MethodParameter}.
+	 *
+	 * @param parameter must not be {@literal null}.
+	 * @return will never be {@literal null}.
+	 * @since 3.0
+	 */
+	public static TypeInformation<?> fromMethodParameter(MethodParameter parameter) {
+		return TypeInformation.of(ResolvableType.forMethodParameter(parameter));
 	}
 
 	/**
diff --git a/src/test/java/org/springframework/data/repository/query/ParameterUnitTests.java b/src/test/java/org/springframework/data/repository/query/ParameterUnitTests.java
index 8d0b1687f0..5d39c9ba86 100644
--- a/src/test/java/org/springframework/data/repository/query/ParameterUnitTests.java
+++ b/src/test/java/org/springframework/data/repository/query/ParameterUnitTests.java
@@ -19,6 +19,7 @@
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Stream;
 
 import org.jetbrains.annotations.NotNull;
@@ -48,14 +49,17 @@ void classParameterWithSameTypeParameterAsReturnedStreamIsDynamicProjectionParam
 		assertThat(parameter.isDynamicProjectionParameter()).isTrue();
 	}
 
+	@Test
+	void classParameterWithSameTypeParameterAsReturnedOptionalIsDynamicProjectionParameter() throws Exception {
+
+		var parameter = new Parameter(getMethodParameter("dynamicProjectionWithOptional"));
+
+		assertThat(parameter.isDynamicProjectionParameter()).isTrue();
+	}
+
 	@NotNull
 	private MethodParameter getMethodParameter(String methodName) throws NoSuchMethodException {
-		return new MethodParameter( //
-				this.getClass().getDeclaredMethod( //
-						methodName, //
-						Class.class //
-				), //
-				0);
+		return new MethodParameter(this.getClass().getDeclaredMethod(methodName, Class.class), 0);
 	}
 
 	<T> List<T> dynamicProjectionWithList(Class<T> type) {
@@ -65,4 +69,8 @@ <T> List<T> dynamicProjectionWithList(Class<T> type) {
 	<T> Stream<T> dynamicProjectionWithStream(Class<T> type) {
 		return Stream.empty();
 	}
+
+	<T> Optional<T> dynamicProjectionWithOptional(Class<T> type) {
+		return Optional.empty();
+	}
 }
diff --git a/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java b/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java
index 4916acce0d..56489f29c6 100755
--- a/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java
+++ b/src/test/java/org/springframework/data/util/TypeDiscovererUnitTests.java
@@ -274,8 +274,7 @@ void genericFieldOfType() {
 		var field = ReflectionUtils.findField(GenericPerson.class, "value");
 		var discoverer = TypeInformation.of(ResolvableType.forField(field, TypeExtendingGenericPersonWithAddress.class));
 
-		assertThat(discoverer).isEqualTo(TypeInformation.of(Address.class));
-		assertThat(discoverer.hashCode()).isEqualTo(TypeInformation.of(Address.class).hashCode());
+		assertThat(discoverer.getType()).isEqualTo(Address.class);
 	}
 
 	@Test // #2511
@@ -335,6 +334,20 @@ void detectsComponentTypeOfTypedClass() {
 		assertThat(property.getComponentType().getType()).isEqualTo(Leaf.class);
 	}
 
+	@Test
+	void differentEqualsAndHashCodeForTypeDiscovererAndClassTypeInformation() {
+
+		ResolvableType type = ResolvableType.forClass(Object.class);
+
+		var discoverer = new TypeDiscoverer<>(type);
+		var classTypeInformation = new ClassTypeInformation<>(type);
+
+		assertThat(discoverer).isNotEqualTo(classTypeInformation);
+		assertThat(classTypeInformation).isNotEqualTo(type);
+
+		assertThat(discoverer.hashCode()).isNotEqualTo(classTypeInformation.hashCode());
+	}
+
 	class Person {
 
 		Addresses addresses;