lazy = Lazy.of(() -> delegate.doWith(object));
+ return converter.mapDocument(object, lazy::get);
+ }
+ }
+
/**
* {@link DocumentCallback} transforming {@link Document} into the given {@code targetType} or decorating the
* {@code sourceType} with a {@literal projection} in case the {@code targetType} is an {@literal interface}.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java
new file mode 100644
index 0000000000..e271ee23cc
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.mongodb.core;
+
+import org.bson.Document;
+
+/**
+ * Converter for MongoDB query results.
+ *
+ * This is a functional interface that allows for mapping a {@link Document} to a result type.
+ * {@link #mapDocument(Document, ConversionResultSupplier) row mapping} can obtain upstream a
+ * {@link ConversionResultSupplier upstream converter} to enrich the final result object. This is useful when e.g.
+ * wrapping result objects where the wrapper needs to obtain information from the actual {@link Document}.
+ *
+ * @param object type accepted by this converter.
+ * @param the returned result type.
+ * @author Mark Paluch
+ * @since x.x
+ */
+@FunctionalInterface
+public interface QueryResultConverter {
+
+ /**
+ * Returns a function that returns the materialized entity.
+ *
+ * @param the type of the input and output entity to the function.
+ * @return a function that returns the materialized entity.
+ */
+ @SuppressWarnings("unchecked")
+ static QueryResultConverter entity() {
+ return (QueryResultConverter) EntityResultConverter.INSTANCE;
+ }
+
+ /**
+ * Map a {@link Document} that is read from the MongoDB query/aggregation operation to a query result.
+ *
+ * @param document the raw document from the MongoDB query/aggregation result.
+ * @param reader reader object that supplies an upstream result from an earlier converter.
+ * @return the mapped result.
+ */
+ R mapDocument(Document document, ConversionResultSupplier reader);
+
+ /**
+ * Returns a composed function that first applies this function to its input, and then applies the {@code after}
+ * function to the result. If evaluation of either function throws an exception, it is relayed to the caller of the
+ * composed function.
+ *
+ * @param the type of output of the {@code after} function, and of the composed function.
+ * @param after the function to apply after this function is applied.
+ * @return a composed function that first applies this function and then applies the {@code after} function.
+ */
+ default QueryResultConverter andThen(QueryResultConverter super R, ? extends V> after) {
+ return (row, reader) -> after.mapDocument(row, () -> mapDocument(row, reader));
+ }
+
+ /**
+ * A supplier that converts a {@link Document} into {@code T}. Allows for lazy reading of query results.
+ *
+ * @param type of the returned result.
+ */
+ interface ConversionResultSupplier {
+
+ /**
+ * Obtain the upstream conversion result.
+ *
+ * @return the upstream conversion result.
+ */
+ T get();
+
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java
index 54129e6b5d..883bc65579 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java
@@ -18,6 +18,7 @@
import reactor.core.publisher.Flux;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.lang.Contract;
/**
* {@link ReactiveAggregationOperation} allows creation and execution of reactive MongoDB aggregation operations in a
@@ -44,7 +45,7 @@ public interface ReactiveAggregationOperation {
/**
* Start creating an aggregation operation that returns results mapped to the given domain type.
* Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to specify a potentially different
- * input type for he aggregation.
+ * input type for the aggregation.
*
* @param domainType must not be {@literal null}.
* @return new instance of {@link ReactiveAggregation}. Never {@literal null}.
@@ -73,6 +74,18 @@ interface AggregationOperationWithCollection {
*/
interface TerminatingAggregationOperation {
+ /**
+ * Map the query result to a different type using {@link QueryResultConverter}.
+ *
+ * @param {@link Class type} of the result.
+ * @param converter the converter, must not be {@literal null}.
+ * @return new instance of {@link ExecutableFindOperation.TerminatingFindNear}.
+ * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}.
+ * @since x.y
+ */
+ @Contract("_ -> new")
+ TerminatingAggregationOperation map(QueryResultConverter super T, ? extends R> converter);
+
/**
* Apply pipeline operations as specified and stream all matching elements.
*
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java
index 954fd61716..a25d0eed6c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java
@@ -51,22 +51,25 @@ public ReactiveAggregation aggregateAndReturn(Class domainType) {
Assert.notNull(domainType, "DomainType must not be null");
- return new ReactiveAggregationSupport<>(template, domainType, null, null);
+ return new ReactiveAggregationSupport<>(template, domainType, QueryResultConverter.entity(), null, null);
}
- static class ReactiveAggregationSupport
+ static class ReactiveAggregationSupport
implements AggregationOperationWithAggregation, ReactiveAggregation, TerminatingAggregationOperation {
private final ReactiveMongoTemplate template;
- private final Class domainType;
+ private final Class domainType;
+ private final QueryResultConverter super S, ? extends T> resultConverter;
private final Aggregation aggregation;
private final String collection;
- ReactiveAggregationSupport(ReactiveMongoTemplate template, Class domainType, Aggregation aggregation,
+ ReactiveAggregationSupport(ReactiveMongoTemplate template, Class domainType,
+ QueryResultConverter super S, ? extends T> resultConverter, Aggregation aggregation,
String collection) {
this.template = template;
this.domainType = domainType;
+ this.resultConverter = resultConverter;
this.aggregation = aggregation;
this.collection = collection;
}
@@ -76,7 +79,7 @@ public AggregationOperationWithAggregation inCollection(String collection) {
Assert.hasText(collection, "Collection must not be null nor empty");
- return new ReactiveAggregationSupport<>(template, domainType, aggregation, collection);
+ return new ReactiveAggregationSupport<>(template, domainType, resultConverter, aggregation, collection);
}
@Override
@@ -84,12 +87,21 @@ public TerminatingAggregationOperation by(Aggregation aggregation) {
Assert.notNull(aggregation, "Aggregation must not be null");
- return new ReactiveAggregationSupport<>(template, domainType, aggregation, collection);
+ return new ReactiveAggregationSupport<>(template, domainType, resultConverter, aggregation, collection);
+ }
+
+ @Override
+ public TerminatingAggregationOperation map(QueryResultConverter super T, ? extends R> converter) {
+
+ Assert.notNull(converter, "QueryResultConverter must not be null");
+
+ return new ReactiveAggregationSupport<>(template, domainType, resultConverter.andThen(converter), aggregation,
+ collection);
}
@Override
public Flux all() {
- return template.aggregate(aggregation, getCollectionName(aggregation), domainType);
+ return template.doAggregate(aggregation, getCollectionName(aggregation), domainType, domainType, resultConverter);
}
private String getCollectionName(Aggregation aggregation) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java
index cba827ffed..24d8c975bb 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java
@@ -25,6 +25,7 @@
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.lang.Contract;
/**
* {@link ReactiveFindOperation} allows creation and execution of reactive MongoDB find operations in a fluent API
@@ -66,7 +67,28 @@ public interface ReactiveFindOperation {
/**
* Compose find execution by calling one of the terminating methods.
*/
- interface TerminatingFind {
+ interface TerminatingFind extends TerminatingResults, TerminatingProjection {
+
+ }
+
+ /**
+ * Compose find execution by calling one of the terminating methods.
+ *
+ * @since x.y
+ */
+ interface TerminatingResults {
+
+ /**
+ * Map the query result to a different type using {@link QueryResultConverter}.
+ *
+ * @param