Repository: Azure/azure-sdk-for-java
Pull request #44859: Add request-reply pattern of ServiceBusTemplate 
Author: login: moarychan
Labels: name: azure-spring
description: All azure-spring related issues
Created: 2025-04-02T01:23:31Z | Merged: 2025-04-08T01:45:51Z

# Description

New feature to support Service Bus request-reply pattern.

# All SDK Contribution checklist:
- [x] **The pull request does not introduce [breaking changes]**
- [x] **CHANGELOG is updated for new features, bug fixes or other significant changes.**
- [x] **I have read the [contribution guidelines](https://github.com/Azure/azure-sdk-for-java/blob/main/CONTRIBUTING.md).**

## [General Guidelines and Best Practices](https://github.com/Azure/azure-sdk-for-java/blob/main/CONTRIBUTING.md#developer-guide)
- [x] Title of the pull request is clear and informative.
- [x] There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md).

### [Testing Guidelines](https://github.com/Azure/azure-sdk-for-java/blob/main/CONTRIBUTING.md#building-and-unit-testing)
- [x] Pull request includes test coverage for the included changes.



Diff:
diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md
index d1e2fe003244..8d31c966b766 100644
--- a/sdk/spring/CHANGELOG.md
+++ b/sdk/spring/CHANGELOG.md
@@ -1,5 +1,19 @@
 # Release History
 
+## 5.22.0-beta.1 (Unreleased)
+
+### Spring Cloud Azure Autoconfigure
+This section includes changes in `spring-cloud-azure-autoconfigure` module.
+
+#### Features Added
+- Register a new bean `ServiceBusConsumerFactory` to support request-reply pattern of `ServiceBusTemplate`.
+
+### Spring Messaging Azure Service Bus
+This section includes changes in the `spring-messaging-azure-servicebus` module.
+
+#### Features Added
+- `ServiceBusTemplate` supports request-reply pattern.
+
 ## 5.21.0 (2025-03-20)
 - This release is compatible with Spring Boot 3.4.0-3.4.2, 3.3.0-3.3.6, 3.2.0-3.2.12, 3.1.0-3.1.12, 3.0.0-3.0.13. (Note: 3.4.x (x>2), 3.3.y (y>6) and 3.2.z (z>12) should be supported, but they aren't tested with this release.)
 - This release is compatible with Spring Cloud 2024.0.0, 2023.0.0-2023.0.4, 2022.0.0-2022.0.5. (Note: 2024.0.x(x>0) and 2023.0.y (y>4) should be supported, but they aren't tested with this release.)
diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfiguration.java
index 73358400a4bb..64e782a0304b 100644
--- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfiguration.java
+++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfiguration.java
@@ -17,11 +17,14 @@
 import com.azure.spring.messaging.PropertiesSupplier;
 import com.azure.spring.messaging.converter.AzureMessageConverter;
 import com.azure.spring.messaging.implementation.converter.ObjectMapperHolder;
+import com.azure.spring.messaging.servicebus.core.DefaultServiceBusNamespaceConsumerFactory;
 import com.azure.spring.messaging.servicebus.core.DefaultServiceBusNamespaceProcessorFactory;
 import com.azure.spring.messaging.servicebus.core.DefaultServiceBusNamespaceProducerFactory;
+import com.azure.spring.messaging.servicebus.core.ServiceBusConsumerFactory;
 import com.azure.spring.messaging.servicebus.core.ServiceBusProcessorFactory;
 import com.azure.spring.messaging.servicebus.core.ServiceBusProducerFactory;
 import com.azure.spring.messaging.servicebus.core.ServiceBusTemplate;
+import com.azure.spring.messaging.servicebus.core.properties.ConsumerProperties;
 import com.azure.spring.messaging.servicebus.core.properties.NamespaceProperties;
 import com.azure.spring.messaging.servicebus.core.properties.ProcessorProperties;
 import com.azure.spring.messaging.servicebus.core.properties.ProducerProperties;
@@ -103,6 +106,27 @@ ServiceBusProcessorFactory defaultServiceBusNamespaceProcessorFactory(
         }
     }
 
+    @Configuration(proxyBeanMethods = false)
+    static class ConsumerContainerConfiguration {
+
+        @Bean
+        @ConditionalOnMissingBean
+        ServiceBusConsumerFactory defaultServiceBusNamespaceConsumerFactory(
+            NamespaceProperties properties,
+            ObjectProvider<PropertiesSupplier<ConsumerIdentifier, ConsumerProperties>> suppliers,
+            ObjectProvider<AzureTokenCredentialResolver> tokenCredentialResolvers,
+            ObjectProvider<TokenCredential> defaultTokenCredentials,
+            ObjectProvider<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder>> customizers,
+            ObjectProvider<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder>> sessionReceiverCustomizers) {
+            DefaultServiceBusNamespaceConsumerFactory factory = new DefaultServiceBusNamespaceConsumerFactory(properties, suppliers.getIfAvailable());
+            factory.setDefaultCredential(defaultTokenCredentials.getIfAvailable());
+            factory.setTokenCredentialResolver(tokenCredentialResolvers.getIfAvailable());
+            customizers.orderedStream().forEach(factory::addCustomizer);
+            sessionReceiverCustomizers.orderedStream().forEach(factory::addSessionReceiverCustomizer);
+            return factory;
+        }
+    }
+
     @Configuration(proxyBeanMethods = false)
     static class ServiceBusTemplateConfiguration {
 
@@ -141,9 +165,10 @@ AzureMessageConverter<ServiceBusReceivedMessage, ServiceBusMessage> serviceBusMe
         @ConditionalOnMissingBean
         @ConditionalOnBean(ServiceBusProducerFactory.class)
         ServiceBusTemplate serviceBusTemplate(AzureServiceBusProperties properties,
-                                              ServiceBusProducerFactory senderClientFactory,
+                                              ServiceBusProducerFactory producerFactory,
+                                              ServiceBusConsumerFactory consumerFactory,
                                               AzureMessageConverter<ServiceBusReceivedMessage, ServiceBusMessage> messageConverter) {
-            ServiceBusTemplate serviceBusTemplate = new ServiceBusTemplate(senderClientFactory);
+            ServiceBusTemplate serviceBusTemplate = new ServiceBusTemplate(producerFactory, consumerFactory);
             serviceBusTemplate.setMessageConverter(messageConverter);
             if (properties.getProducer().getEntityType() != null) {
                 serviceBusTemplate.setDefaultEntityType(properties.getProducer().getEntityType());
diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfigurationTests.java
index 76acfffda1b2..e1622836b4ad 100644
--- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfigurationTests.java
+++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfigurationTests.java
@@ -29,7 +29,10 @@ void disableServiceBusShouldNotConfigure() {
                 "spring.cloud.azure.servicebus.enabled=false",
                 "spring.cloud.azure.servicebus.namespace=test-namespace"
             )
-            .run(context -> assertThat(context).doesNotHaveBean(AzureServiceBusMessagingAutoConfiguration.ProcessorContainerConfiguration.class));
+            .run(context -> {
+                assertThat(context).doesNotHaveBean(AzureServiceBusMessagingAutoConfiguration.ProcessorContainerConfiguration.class);
+                assertThat(context).doesNotHaveBean(AzureServiceBusMessagingAutoConfiguration.ConsumerContainerConfiguration.class);
+            });
     }
 
     @Test
@@ -39,18 +42,24 @@ void withoutServiceBusTemplateShouldNotConfigure() {
             .withPropertyValues(
                 "spring.cloud.azure.servicebus.namespace=test-namespace"
             )
-            .run(context -> assertThat(context).doesNotHaveBean(AzureServiceBusMessagingAutoConfiguration.ProcessorContainerConfiguration.class));
+            .run(context -> {
+                assertThat(context).doesNotHaveBean(AzureServiceBusMessagingAutoConfiguration.ProcessorContainerConfiguration.class);
+                assertThat(context).doesNotHaveBean(AzureServiceBusMessagingAutoConfiguration.ConsumerContainerConfiguration.class);
+            });
     }
 
     @Test
     void withoutServiceBusConnectionShouldNotConfigure() {
         this.contextRunner
             .withUserConfiguration(AzureServiceBusPropertiesTestConfiguration.class)
-            .run(context -> assertThat(context).doesNotHaveBean(AzureServiceBusMessagingAutoConfiguration.ProcessorContainerConfiguration.class));
+            .run(context -> {
+                assertThat(context).doesNotHaveBean(AzureServiceBusMessagingAutoConfiguration.ProcessorContainerConfiguration.class);
+                assertThat(context).doesNotHaveBean(AzureServiceBusMessagingAutoConfiguration.ConsumerContainerConfiguration.class);
+            });
     }
 
     @Test
-    void connectionInfoAndCheckpointStoreProvidedShouldConfigure() {
+    void connectionInfoProvidedShouldConfigure() {
         this.contextRunner
             .withPropertyValues(
                 "spring.cloud.azure.servicebus.connection-string=" + String.format(CONNECTION_STRING_FORMAT, "test-namespace")
@@ -59,6 +68,7 @@ void connectionInfoAndCheckpointStoreProvidedShouldConfigure() {
             .run(context -> {
                 assertThat(context).hasSingleBean(ServiceBusProcessorFactory.class);
                 assertThat(context).hasSingleBean(AzureServiceBusMessagingAutoConfiguration.ProcessorContainerConfiguration.class);
+                assertThat(context).hasSingleBean(AzureServiceBusMessagingAutoConfiguration.ConsumerContainerConfiguration.class);
             });
     }
 
diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceConsumerFactory.java b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceConsumerFactory.java
new file mode 100644
index 000000000000..f22d4fc4b27f
--- /dev/null
+++ b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceConsumerFactory.java
@@ -0,0 +1,214 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.messaging.servicebus.core;
+
+import com.azure.core.credential.TokenCredential;
+import com.azure.messaging.servicebus.ServiceBusClientBuilder;
+import com.azure.messaging.servicebus.ServiceBusSessionReceiverClient;
+import com.azure.messaging.servicebus.models.ServiceBusReceiveMode;
+import com.azure.spring.cloud.core.credential.AzureCredentialResolver;
+import com.azure.spring.cloud.core.customizer.AzureServiceClientBuilderCustomizer;
+import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier;
+import com.azure.spring.cloud.service.implementation.servicebus.factory.ServiceBusSessionReceiverClientBuilderFactory;
+import com.azure.spring.cloud.service.servicebus.properties.ServiceBusEntityType;
+import com.azure.spring.messaging.ConsumerIdentifier;
+import com.azure.spring.messaging.PropertiesSupplier;
+import com.azure.spring.messaging.servicebus.core.properties.ConsumerProperties;
+import com.azure.spring.messaging.servicebus.core.properties.NamespaceProperties;
+import com.azure.spring.messaging.servicebus.implementation.properties.merger.ConsumerPropertiesParentMerger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.lang.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * The {@link ServiceBusConsumerFactory} implementation to produce new {@link ServiceBusSessionReceiverClient} instances
+ * for provided {@link NamespaceProperties} and optional producer {@link PropertiesSupplier} on each
+ * {@link #createReceiver} invocation.
+ * <p>
+ * {@link ServiceBusSessionReceiverClient} produced by this factory will share the same namespace level configuration, but
+ * if a configuration entry is provided at both producer and namespace level, the producer level configuration will
+ * take advantage.
+ * </p>
+ * @since 5.22.0
+ */
+public final class DefaultServiceBusNamespaceConsumerFactory implements ServiceBusConsumerFactory, DisposableBean {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultServiceBusNamespaceConsumerFactory.class);
+    private final List<Listener> listeners = new ArrayList<>();
+    private final NamespaceProperties namespaceProperties;
+    private final PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> propertiesSupplier;
+    private final Map<String, ServiceBusSessionReceiverClient> clients = new ConcurrentHashMap<>();
+    private final ConsumerPropertiesParentMerger parentMerger = new ConsumerPropertiesParentMerger();
+    private final List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder>> customizers = new ArrayList<>();
+    private final List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder>> sessionReceiverCustomizers = new ArrayList<>();
+    private final Map<String, List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder>>> dedicatedSessionReceiverCustomizers = new HashMap<>();
+    private AzureCredentialResolver<TokenCredential> tokenCredentialResolver = null;
+    private TokenCredential defaultCredential = null;
+
+    /**
+     * Construct a factory with the provided namespace level configuration.
+     * @param namespaceProperties the namespace properties
+     */
+    public DefaultServiceBusNamespaceConsumerFactory(NamespaceProperties namespaceProperties) {
+        this(namespaceProperties, key -> null);
+    }
+
+    /**
+     * Construct a factory with the provided namespace level configuration and producer {@link PropertiesSupplier}.
+     * @param namespaceProperties the namespace properties.
+     * @param supplier the {@link PropertiesSupplier} to supply {@link ConsumerProperties} for each queue/topic entity.
+     */
+    public DefaultServiceBusNamespaceConsumerFactory(NamespaceProperties namespaceProperties,
+                                                     PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> supplier) {
+        this.namespaceProperties = namespaceProperties;
+        this.propertiesSupplier = supplier == null ? key -> null : supplier;
+    }
+
+    @Override
+    public ServiceBusSessionReceiverClient createReceiver(String name) {
+        return doCreateReceiver(name, null);
+    }
+
+    @Override
+    public ServiceBusSessionReceiverClient createReceiver(String name, ServiceBusEntityType entityType) {
+        ConsumerProperties consumerProperties = this.propertiesSupplier.getProperties(new ConsumerIdentifier(name)) != null
+            ? this.propertiesSupplier.getProperties(new ConsumerIdentifier(name)) : new ConsumerProperties();
+        if (entityType != null) {
+            consumerProperties.setEntityType(entityType);
+        }
+        return doCreateReceiver(name, consumerProperties);
+    }
+
+    @Override
+    public void addListener(Listener listener) {
+        this.listeners.add(listener);
+    }
+
+    @Override
+    public boolean removeListener(Listener listener) {
+        return this.listeners.remove(listener);
+    }
+
+    @Override
+    public void destroy() {
+        clients.forEach((name, receiver) -> {
+            listeners.forEach(l -> l.receiverRemoved(name, receiver));
+            receiver.close();
+        });
+        this.clients.clear();
+        this.listeners.clear();
+    }
+
+    private ServiceBusSessionReceiverClient doCreateReceiver(String name, @Nullable ConsumerProperties properties) {
+        return clients.computeIfAbsent(name, entityName -> {
+            ConsumerProperties consumerProperties = parentMerger.merge(properties, this.namespaceProperties);
+            consumerProperties.setEntityName(entityName);
+
+            ServiceBusSessionReceiverClient receiverClient;
+            if (Boolean.TRUE.equals(consumerProperties.getSessionEnabled())) {
+                ServiceBusSessionReceiverClientBuilderFactory factory =
+                    new ServiceBusSessionReceiverClientBuilderFactory(consumerProperties, this.customizers);
+
+                factory.setDefaultTokenCredential(this.defaultCredential);
+                factory.setTokenCredentialResolver(this.tokenCredentialResolver);
+                factory.setSpringIdentifier(AzureSpringIdentifier.AZURE_SPRING_INTEGRATION_SERVICE_BUS);
+
+                ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder builder = factory.build();
+
+                customizeBuilder(name, builder);
+
+                builder.receiveMode(ServiceBusReceiveMode.RECEIVE_AND_DELETE);
+                builder.disableAutoComplete();
+                LOGGER.debug("Set RECEIVE_AND_DELETE mode for request-reply-pattern receiver client, "
+                    + "'enableAutoComplete' is not needed in for RECEIVE_AND_DELETE mode.");
+
+                receiverClient = builder.buildClient();
+
+                this.listeners.forEach(l -> l.receiverAdded(entityName, receiverClient));
+            } else {
+                receiverClient = null;
+                LOGGER.warn("Receiver client is null. Define a bean PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> to enable consumer 'session-enabled'.");
+            }
+            return receiverClient;
+        });
+    }
+
+    /**
+     * Set the token credential resolver.
+     *
+     * @param tokenCredentialResolver The token credential resolver.
+     */
+    public void setTokenCredentialResolver(AzureCredentialResolver<TokenCredential> tokenCredentialResolver) {
+        this.tokenCredentialResolver = tokenCredentialResolver;
+    }
+
+    /**
+     * Set the default credential for all clients generated from this factory.
+     *
+     * @param defaultCredential The default credential.
+     */
+    public void setDefaultCredential(TokenCredential defaultCredential) {
+        this.defaultCredential = defaultCredential;
+    }
+
+    /**
+     * Add a {@link ServiceBusClientBuilder}
+     * customizer to customize the shared client builder created in this factory, it's used to build other sender clients.
+     *
+     * @param customizer the provided builder customizer.
+     */
+    public void addCustomizer(AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder> customizer) {
+        if (customizer == null) {
+            LOGGER.debug("The provided '{}' customizer is null, will ignore it.", ServiceBusClientBuilder.class.getName());
+        } else {
+            this.customizers.add(customizer);
+        }
+    }
+
+    /**
+     * Add a {@link ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder}
+     * customizer to customize all the session clients created from this factory.
+     * @param customizer the provided builder customizer.
+     */
+    public void addSessionReceiverCustomizer(AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder> customizer) {
+        if (customizer == null) {
+            LOGGER.debug("The provided '{}' customizer is null, will ignore it.",
+                ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder.class.getName());
+            return;
+        }
+        this.sessionReceiverCustomizers.add(customizer);
+    }
+
+    /**
+     * Add a session receiver client builder customizer to customize the clients created from this factory with Service Bus
+     * entity name of value {@code entityName}.
+     *
+     * @param entityName the entity name of the client.
+     * @param customizer the provided customizer.
+     */
+    public void addDedicatedSessionReceiverCustomizer(String entityName,
+                                               AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder> customizer) {
+        if (customizer == null) {
+            LOGGER.debug("The provided '{}' dedicated customizer is null, will ignore it.",
+                ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder.class.getName());
+        } else {
+            this.dedicatedSessionReceiverCustomizers
+                .computeIfAbsent(entityName, key -> new ArrayList<>())
+                .add(customizer);
+        }
+    }
+
+    private void customizeBuilder(String entityName, ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder builder) {
+        this.sessionReceiverCustomizers.forEach(customizer -> customizer.customize(builder));
+        this.dedicatedSessionReceiverCustomizers.getOrDefault(entityName, new ArrayList<>())
+                                         .forEach(customizer -> customizer.customize(builder));
+    }
+
+}
diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceProcessorFactory.java b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceProcessorFactory.java
index f911953ebdaf..d0edbb3b874d 100644
--- a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceProcessorFactory.java
+++ b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceProcessorFactory.java
@@ -51,7 +51,6 @@
 public final class DefaultServiceBusNamespaceProcessorFactory implements ServiceBusProcessorFactory, DisposableBean {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultServiceBusNamespaceProcessorFactory.class);
-    private static final String LOG_IGNORE_NULL_CUSTOMIZER = "The provided '{}' customizer is null, will ignore it.";
     private final Map<ConsumerIdentifier, ServiceBusProcessorClient> processorMap = new ConcurrentHashMap<>();
     private final List<Listener> listeners = new ArrayList<>();
     private final NamespaceProperties namespaceProperties;
@@ -246,7 +245,7 @@ private void close(Map<ConsumerIdentifier, ServiceBusProcessorClient> map, Consu
      */
     public void addServiceBusClientBuilderCustomizer(AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder> customizer) {
         if (customizer == null) {
-            LOGGER.debug(LOG_IGNORE_NULL_CUSTOMIZER, ServiceBusClientBuilder.class.getName());
+            LOGGER.debug("The provided '{}' customizer is null, will ignore it.", ServiceBusClientBuilder.class.getName());
         } else {
             this.serviceBusClientBuilderCustomizers.add(customizer);
         }
@@ -260,7 +259,8 @@ public void addServiceBusClientBuilderCustomizer(AzureServiceClientBuilderCustom
      */
     public void addBuilderCustomizer(AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusProcessorClientBuilder> customizer) {
         if (customizer == null) {
-            LOGGER.debug(LOG_IGNORE_NULL_CUSTOMIZER, ServiceBusClientBuilder.ServiceBusProcessorClientBuilder.class.getName());
+            LOGGER.debug("The provided '{}' customizer is null, will ignore it.", 
+                ServiceBusClientBuilder.ServiceBusProcessorClientBuilder.class.getName());
         } else {
             this.customizers.add(customizer);
         }
@@ -274,7 +274,8 @@ public void addBuilderCustomizer(AzureServiceClientBuilderCustomizer<ServiceBusC
      */
     public void addSessionBuilderCustomizer(AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionProcessorClientBuilder> customizer) {
         if (customizer == null) {
-            LOGGER.debug(LOG_IGNORE_NULL_CUSTOMIZER, ServiceBusClientBuilder.ServiceBusSessionProcessorClientBuilder.class.getName());
+            LOGGER.debug("The provided '{}' customizer is null, will ignore it.", 
+                ServiceBusClientBuilder.ServiceBusSessionProcessorClientBuilder.class.getName());
         } else {
             this.sessionCustomizers.add(customizer);
         }
@@ -292,7 +293,8 @@ public void addBuilderCustomizer(String entityName,
                                      String subscription,
                                      AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusProcessorClientBuilder> customizer) {
         if (customizer == null) {
-            LOGGER.debug(LOG_IGNORE_NULL_CUSTOMIZER, ServiceBusClientBuilder.ServiceBusProcessorClientBuilder.class.getName());
+            LOGGER.debug("The provided '{}' dedicated customizer is null, will ignore it.",
+                ServiceBusClientBuilder.ServiceBusProcessorClientBuilder.class.getName());
         } else {
             this.dedicatedCustomizers
                 .computeIfAbsent(new ConsumerIdentifier(entityName, subscription), key -> new ArrayList<>())
@@ -312,7 +314,8 @@ public void addSessionBuilderCustomizer(String entityName,
                                             String subscription,
                                             AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionProcessorClientBuilder> customizer) {
         if (customizer == null) {
-            LOGGER.debug(LOG_IGNORE_NULL_CUSTOMIZER, ServiceBusClientBuilder.ServiceBusSessionProcessorClientBuilder.class.getName());
+            LOGGER.debug("The provided '{}' dedicated customizer is null, will ignore it.",
+                ServiceBusClientBuilder.ServiceBusSessionProcessorClientBuilder.class.getName());
         } else {
             this.dedicatedSessionCustomizers
                 .computeIfAbsent(new ConsumerIdentifier(entityName, subscription), key -> new ArrayList<>())
diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceProducerFactory.java b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceProducerFactory.java
index bcb459d9ee08..3aacb7496047 100644
--- a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceProducerFactory.java
+++ b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceProducerFactory.java
@@ -38,7 +38,6 @@
  */
 public final class DefaultServiceBusNamespaceProducerFactory implements ServiceBusProducerFactory, DisposableBean {
 
-    private static final String LOG_IGNORE_NULL_CUSTOMIZER = "The provided '{}' customizer is null, will ignore it.";
     private final List<Listener> listeners = new ArrayList<>();
     private final NamespaceProperties namespaceProperties;
     private final PropertiesSupplier<String, ProducerProperties> propertiesSupplier;
@@ -151,7 +150,7 @@ public void setDefaultCredential(TokenCredential defaultCredential) {
      */
     public void addServiceBusClientBuilderCustomizer(AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder> customizer) {
         if (customizer == null) {
-            LOGGER.debug(LOG_IGNORE_NULL_CUSTOMIZER, ServiceBusClientBuilder.class.getName());
+            LOGGER.debug("The provided '{}' customizer is null, will ignore it.", ServiceBusClientBuilder.class.getName());
         } else {
             this.serviceBusClientBuilderCustomizers.add(customizer);
         }
@@ -164,7 +163,8 @@ public void addServiceBusClientBuilderCustomizer(AzureServiceClientBuilderCustom
      */
     public void addBuilderCustomizer(AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSenderClientBuilder> customizer) {
         if (customizer == null) {
-            LOGGER.debug(LOG_IGNORE_NULL_CUSTOMIZER, ServiceBusClientBuilder.ServiceBusSenderClientBuilder.class.getName());
+            LOGGER.debug("The provided '{}' customizer is null, will ignore it.",
+                ServiceBusClientBuilder.ServiceBusSenderClientBuilder.class.getName());
             return;
         }
         this.customizers.add(customizer);
@@ -180,7 +180,8 @@ public void addBuilderCustomizer(AzureServiceClientBuilderCustomizer<ServiceBusC
     public void addBuilderCustomizer(String entityName,
                                      AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSenderClientBuilder> customizer) {
         if (customizer == null) {
-            LOGGER.debug("The provided customizer is null, will ignore it.");
+            LOGGER.debug("The provided '{}' dedicated customizer is null, will ignore it.",
+                ServiceBusClientBuilder.ServiceBusSenderClientBuilder.class.getName());
             return;
         }
         this.dedicatedCustomizers
diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/ServiceBusConsumerFactory.java b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/ServiceBusConsumerFactory.java
new file mode 100644
index 000000000000..c6256aa7fdf2
--- /dev/null
+++ b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/ServiceBusConsumerFactory.java
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.messaging.servicebus.core;
+
+
+import com.azure.messaging.servicebus.ServiceBusSessionReceiverClient;
+import com.azure.spring.cloud.service.servicebus.properties.ServiceBusEntityType;
+
+/**
+ * The strategy to produce {@link ServiceBusSessionReceiverClient} instance.
+ * @since 5.22.0
+ */
+public interface ServiceBusConsumerFactory {
+
+    /**
+     * Create a {@link ServiceBusSessionReceiverClient} to consume events from the specified queue.
+     *
+     * @param name the destination entity name.
+     *
+     * @return ServiceBusReceiverClient queue receiver client.
+     */
+    ServiceBusSessionReceiverClient createReceiver(String name);
+
+    /**
+     * Create a {@link ServiceBusSessionReceiverClient} to consume events from the specified queue.
+     *
+     * @param name the destination entity name.
+     * @param entityType the Service Bus entity type.
+     *
+     * @return ServiceBusReceiverClient queue receiver client.
+     */
+    ServiceBusSessionReceiverClient createReceiver(String name, ServiceBusEntityType entityType);
+
+    /**
+     * Add a listener for this factory.
+     *
+     * @param listener the listener.
+     */
+    default void addListener(Listener listener) {
+
+    }
+
+    /**
+     * Remove a listener from the factory.
+     *
+     * @param listener the listener.
+     *
+     * @return true if removed.
+     */
+    default boolean removeListener(Listener listener) {
+        return false;
+    }
+
+    /**
+     * Called whenever a receiver is added or removed.
+     */
+    @FunctionalInterface
+    interface Listener {
+
+        /**
+         * The callback method that the session receiver has been added.
+         *
+         * @param name the name for the receiver.
+         * @param client the client for the session receiver.
+         */
+        void receiverAdded(String name, ServiceBusSessionReceiverClient client);
+
+        /**
+         * The default callback method that the session receiver has been removed.
+         *
+         * @param name the name for the receiver.
+         * @param client the client for the session receiver.
+         */
+        default void receiverRemoved(String name, ServiceBusSessionReceiverClient client) {
+        }
+    }
+
+}
diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/ServiceBusTemplate.java b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/ServiceBusTemplate.java
index 518146082de2..c8fc7b72a503 100644
--- a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/ServiceBusTemplate.java
+++ b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/ServiceBusTemplate.java
@@ -6,17 +6,26 @@
 import com.azure.messaging.servicebus.ServiceBusMessage;
 import com.azure.messaging.servicebus.ServiceBusReceivedMessage;
 import com.azure.messaging.servicebus.ServiceBusSenderAsyncClient;
+import com.azure.messaging.servicebus.ServiceBusSessionReceiverClient;
+import com.azure.spring.cloud.service.servicebus.properties.ServiceBusEntityType;
 import com.azure.spring.messaging.PropertiesSupplier;
 import com.azure.spring.messaging.converter.AzureMessageConverter;
 import com.azure.spring.messaging.core.SendOperation;
-import com.azure.spring.cloud.service.servicebus.properties.ServiceBusEntityType;
 import com.azure.spring.messaging.servicebus.core.properties.NamespaceProperties;
 import com.azure.spring.messaging.servicebus.implementation.support.converter.ServiceBusMessageConverter;
+import com.azure.spring.messaging.servicebus.support.ServiceBusMessageHeaders;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.lang.NonNull;
 import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageHeaders;
 import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
 import reactor.core.publisher.Mono;
 
+import java.time.Duration;
+import java.util.UUID;
+
 /**
  * Azure Service Bus template to support send {@link Message} asynchronously.
  *
@@ -26,18 +35,33 @@
  * </p>
  */
 public class ServiceBusTemplate implements SendOperation {
-
+    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceBusTemplate.class);
     private static final ServiceBusMessageConverter DEFAULT_CONVERTER = new ServiceBusMessageConverter();
+    private static final Duration DEFAULT_PRC_SEND_TIMEOUT = Duration.ofSeconds(30);
     private final ServiceBusProducerFactory producerFactory;
+    private final ServiceBusConsumerFactory consumerFactory;
     private AzureMessageConverter<ServiceBusReceivedMessage, ServiceBusMessage> messageConverter = DEFAULT_CONVERTER;
     private ServiceBusEntityType defaultEntityType;
+    private Duration rpcSendTimeout = DEFAULT_PRC_SEND_TIMEOUT;
 
     /**
      * Create an instance using the supplied producer factory.
      * @param producerFactory the producer factory.
      */
     public ServiceBusTemplate(@NonNull ServiceBusProducerFactory producerFactory) {
+        this(producerFactory, null);
+    }
+
+    /**
+     * Create an instance using the supplied producer factory, processor factory, it supports request-reply pattern.
+     * @param producerFactory the producer factory.
+     * @param consumerFactory the consumer factory.
+     * @since 5.22.0
+     */
+    public ServiceBusTemplate(@NonNull ServiceBusProducerFactory producerFactory,
+                              ServiceBusConsumerFactory consumerFactory) {
         this.producerFactory = producerFactory;
+        this.consumerFactory = consumerFactory;
     }
 
     @Override
@@ -49,6 +73,102 @@ public <U> Mono<Void> sendAsync(String destination, Message<U> message) {
         return senderAsyncClient.sendMessage(serviceBusMessage);
     }
 
+    /**
+     * Basic RPC pattern usage. Send a message to the destination and wait for a reply message from the replay channel,
+     * which must be specified by the message header {@link MessageHeaders#REPLY_CHANNEL} and the session must be enabled;
+     * the message header {@link ServiceBusMessageHeaders#REPLY_TO_SESSION_ID} is used to specify a unique reply session ID,
+     * if not set, a random {@link UUID} will be used. Use the default entity type and PRC send timeout.
+     *
+     * @param <U> The type of the message payload.
+     * @param destination topic or queue name.
+     * @param message Message to be sent to the Service Bus entity.
+     *
+     * @return the reply message of the response. If the reply message fails to be obtained, null is returned.
+     *
+     * @throws IllegalStateException if sender or receiver is already disposed.
+     * @since 5.22.0
+     */
+    public <U> ServiceBusReceivedMessage sendAndReceive(String destination,
+                                                        Message<U> message) {
+        return sendAndReceive(destination, defaultEntityType, message, rpcSendTimeout);
+    }
+
+    /**
+     * Basic RPC pattern usage. Send a message to the destination and wait for a reply message from the replay channel,
+     * which must be specified by the message header {@link MessageHeaders#REPLY_CHANNEL} and the session must be enabled;
+     * the message header {@link ServiceBusMessageHeaders#REPLY_TO_SESSION_ID} is used to specify a unique reply session ID,
+     * if not set, a random {@link UUID} will be used. Use the default PRC send timeout.
+     *
+     * @param <U> The type of the message payload.
+     * @param destination topic or queue name.
+     * @param entityType type of Service Bus entity.
+     * @param message Message to be sent to the Service Bus entity.
+     *
+     * @return the reply message of the response. If the reply message fails to be obtained, null is returned.
+     *
+     * @throws IllegalStateException if sender or receiver is already disposed.
+     * @since 5.22.0
+     */
+    public <U> ServiceBusReceivedMessage sendAndReceive(String destination,
+                                    ServiceBusEntityType entityType,
+                                    Message<U> message) {
+        return sendAndReceive(destination, entityType, message, rpcSendTimeout);
+    }
+
+    /**
+     * Basic RPC pattern usage. Send a message to the destination and wait for a reply message from the replay channel,
+     * which must be specified by the message header {@link MessageHeaders#REPLY_CHANNEL} and the session must be enabled;
+     * the message header {@link ServiceBusMessageHeaders#REPLY_TO_SESSION_ID} is used to specify a unique reply session ID,
+     * if not set, a random {@link UUID} will be used.
+     *
+     * @param <U> The type of the message payload.
+     * @param destination topic or queue name.
+     * @param entityType type of Service Bus entity.
+     * @param message the message to be sent to the Service Bus entity.
+     * @param sendTimeout timeout for sending the message.
+     *
+     * @return the reply message of the response. If the reply message fails to be obtained, null is returned.
+     *
+     * @throws IllegalStateException if sender or receiver is already disposed.
+     * @since 5.22.0
+     */
+    public <U> ServiceBusReceivedMessage sendAndReceive(String destination,
+                                    ServiceBusEntityType entityType,
+                                    Message<U> message,
+                                    Duration sendTimeout) {
+        Assert.hasText(destination, "'destination' can't be null or empty");
+        Assert.notNull(consumerFactory, "'consumerFactory' can't be null, please enable 'session-enabled' and 'rpc-enabled' for consumer.");
+
+        ServiceBusEntityType currentEntityType = entityType;
+        if (entityType == null && defaultEntityType != null) {
+            currentEntityType = defaultEntityType;
+        }
+
+        MessageHeaders headers = message.getHeaders();
+        String replyDestination = headers.get(MessageHeaders.REPLY_CHANNEL, String.class);
+        Assert.hasText(replyDestination, "Message header '" + MessageHeaders.REPLY_CHANNEL + "' can't be null or empty.");
+        ServiceBusMessage serviceBusMessage = messageConverter.fromMessage(message, ServiceBusMessage.class);
+        String replyToSessionId = serviceBusMessage.getReplyToSessionId();
+        if (!StringUtils.hasText(replyToSessionId)) {
+            replyToSessionId = UUID.randomUUID().toString();
+            serviceBusMessage.setReplyToSessionId(replyToSessionId);
+            LOGGER.debug("Generated unique reply-to session-id '{}' for entity '{}'.", replyToSessionId, destination);
+        } else {
+            LOGGER.debug("Provided reply-to session id ‘{}’ for entity '{}', it should be unique.", replyToSessionId, destination);
+        }
+
+        ServiceBusSenderAsyncClient senderAsyncClient = this.producerFactory.createProducer(destination, defaultEntityType);
+        senderAsyncClient.sendMessage(serviceBusMessage).block(sendTimeout);
+
+        ServiceBusSessionReceiverClient sessionReceiver = consumerFactory.createReceiver(replyDestination, currentEntityType);
+        Assert.notNull(sessionReceiver, "'sessionReceiver' can't be null, please enable 'session-enabled' for consumer.");
+        return sessionReceiver.acceptSession(replyToSessionId)
+                              .receiveMessages(1)
+                              .stream()
+                              .findFirst()
+                              .orElse(null);
+    }
+
     /**
      * Set the message converter to use.
      * @param messageConverter the message converter.
@@ -73,4 +193,15 @@ public AzureMessageConverter<ServiceBusReceivedMessage, ServiceBusMessage> getMe
     public void setDefaultEntityType(ServiceBusEntityType entityType) {
         defaultEntityType = entityType;
     }
+
+    /**
+     * Set the timeout for sending message for request-reply pattern. If not set, the default {@link #DEFAULT_PRC_SEND_TIMEOUT} will be used.
+     * @param rpcSendTimeout the timeout of sending messages.
+     * @since 5.22.0
+     */
+    public void setRpcSendTimeout(Duration rpcSendTimeout) {
+        if (rpcSendTimeout != null) {
+            this.rpcSendTimeout = rpcSendTimeout;
+        }
+    }
 }
diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/properties/merger/ConsumerPropertiesParentMerger.java b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/properties/merger/ConsumerPropertiesParentMerger.java
new file mode 100644
index 000000000000..30581e597fb9
--- /dev/null
+++ b/sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/implementation/properties/merger/ConsumerPropertiesParentMerger.java
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.messaging.servicebus.implementation.properties.merger;
+
+import com.azure.spring.cloud.core.implementation.properties.PropertyMapper;
+import com.azure.spring.cloud.core.implementation.util.AzurePropertiesUtils;
+import com.azure.spring.cloud.service.implementation.core.PropertiesMerger;
+import com.azure.spring.messaging.servicebus.core.properties.ConsumerProperties;
+import com.azure.spring.messaging.servicebus.core.properties.NamespaceProperties;
+
+/**
+ * A merger used to merge a {@link ConsumerProperties} with its parent {@link NamespaceProperties}. When a property is
+ * set in the child, it will be kept. For those properties not set in the child, it will use the value in the parent.
+ * @since 5.22.0
+ */
+public class ConsumerPropertiesParentMerger implements PropertiesMerger<ConsumerProperties, NamespaceProperties> {
+
+    @Override
+    public ConsumerProperties merge(ConsumerProperties child, NamespaceProperties parent) {
+        ConsumerProperties properties = new ConsumerProperties();
+        if (child == null && parent == null) {
+            return properties;
+        }
+
+        if (child == null) {
+            child = new ConsumerProperties();
+        }
+        if (parent == null) {
+            parent = new NamespaceProperties();
+        }
+
+        PropertyMapper propertyMapper = new PropertyMapper();
+
+        AzurePropertiesUtils.mergeAzureCommonProperties(parent, child, properties);
+
+        propertyMapper.from(parent.getDomainName()).to(properties::setDomainName);
+        propertyMapper.from(parent.getNamespace()).to(properties::setNamespace);
+        propertyMapper.from(parent.getConnectionString()).to(properties::setConnectionString);
+        propertyMapper.from(parent.getEntityName()).to(properties::setEntityName);
+        propertyMapper.from(parent.getEntityType()).to(properties::setEntityType);
+        propertyMapper.from(parent.getCustomEndpointAddress()).to(properties::setCustomEndpointAddress);
+
+        // If a same property appears in both two objects, the value from the child will take precedence.
+        copyConsumerPropertiesIfNotNull(child, properties);
+
+        return properties;
+    }
+
+    public static void copyConsumerPropertiesIfNotNull(ConsumerProperties source, ConsumerProperties target) {
+        PropertyMapper propertyMapper = new PropertyMapper();
+
+        propertyMapper.from(source.getDomainName()).to(target::setDomainName);
+        propertyMapper.from(source.getNamespace()).to(target::setNamespace);
+        propertyMapper.from(source.getConnectionString()).to(target::setConnectionString);
+        propertyMapper.from(source.getEntityName()).to(target::setEntityName);
+        propertyMapper.from(source.getEntityType()).to(target::setEntityType);
+        propertyMapper.from(source.getCustomEndpointAddress()).to(target::setCustomEndpointAddress);
+
+        propertyMapper.from(source.getSessionEnabled()).to(target::setSessionEnabled);
+        propertyMapper.from(source.getAutoComplete()).to(target::setAutoComplete);
+        propertyMapper.from(source.getPrefetchCount()).to(target::setPrefetchCount);
+        propertyMapper.from(source.getSubQueue()).to(target::setSubQueue);
+        propertyMapper.from(source.getReceiveMode()).to(target::setReceiveMode);
+        propertyMapper.from(source.getSubscriptionName()).to(target::setSubscriptionName);
+        propertyMapper.from(source.getMaxAutoLockRenewDuration()).to(target::setMaxAutoLockRenewDuration);
+    }
+}
diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceConsumerFactoryTests.java b/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceConsumerFactoryTests.java
new file mode 100644
index 000000000000..ffcb1be7deaf
--- /dev/null
+++ b/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceConsumerFactoryTests.java
@@ -0,0 +1,122 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.messaging.servicebus.core;
+
+import com.azure.messaging.servicebus.ServiceBusSessionReceiverClient;
+import com.azure.spring.cloud.service.servicebus.properties.ServiceBusEntityType;
+import com.azure.spring.messaging.ConsumerIdentifier;
+import com.azure.spring.messaging.PropertiesSupplier;
+import com.azure.spring.messaging.servicebus.core.properties.ConsumerProperties;
+import com.azure.spring.messaging.servicebus.core.properties.NamespaceProperties;
+import com.azure.spring.messaging.servicebus.core.properties.ProcessorProperties;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class DefaultServiceBusNamespaceConsumerFactoryTests {
+    private ServiceBusConsumerFactory consumerFactory;
+    private int queueReceiverAddedTimes = 0;
+    private int topicReceiverAddedTimes = 0;
+
+    @BeforeEach
+    void setUp() {
+        NamespaceProperties namespaceProperties = new NamespaceProperties();
+        namespaceProperties.setNamespace("test-namespace");
+        PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> supplier = key -> {
+            if (key.getDestination().startsWith("queue-") || key.getDestination().startsWith("topic-")) {
+                ConsumerProperties consumerProperties = new ProcessorProperties();
+                consumerProperties.setEntityName(key.getDestination());
+                consumerProperties.setSessionEnabled(true);
+                if (key.getDestination().startsWith("topic-")) {
+                    consumerProperties.setSubscriptionName("test-sub");
+                }
+                return consumerProperties;
+            }
+            return null;
+        };
+        this.consumerFactory = new DefaultServiceBusNamespaceConsumerFactory(namespaceProperties, supplier);
+        queueReceiverAddedTimes = 0;
+        topicReceiverAddedTimes = 0;
+        this.consumerFactory.addListener((name, client) -> {
+            if (name.startsWith("queue-")) {
+                queueReceiverAddedTimes++;
+            } else {
+                topicReceiverAddedTimes++;
+            }
+        });
+    }
+
+    @Test
+    void getSessionReceiverClientForQueue() {
+        ServiceBusSessionReceiverClient sessionReceiverClient = consumerFactory.createReceiver("queue-1", ServiceBusEntityType.QUEUE);
+        assertNotNull(sessionReceiverClient);
+        assertEquals(0, topicReceiverAddedTimes);
+        assertEquals(1, queueReceiverAddedTimes);
+    }
+
+    @Test
+    void getSessionReceiverClientForTopic() {
+        ServiceBusSessionReceiverClient sessionReceiverClient = consumerFactory.createReceiver("topic-1", ServiceBusEntityType.TOPIC);
+        assertNotNull(sessionReceiverClient);
+        assertEquals(1, topicReceiverAddedTimes);
+        assertEquals(0, queueReceiverAddedTimes);
+    }
+
+    @Test
+    void createSessionReceiverClientQueueTwice() {
+        ServiceBusSessionReceiverClient sessionReceiverClient = consumerFactory.createReceiver("queue-1", ServiceBusEntityType.QUEUE);
+        assertNotNull(sessionReceiverClient);
+
+        consumerFactory.createReceiver("queue-1", ServiceBusEntityType.QUEUE);
+        assertEquals(1, queueReceiverAddedTimes);
+    }
+
+    @Test
+    void createSessionReceiverClientTopicTwice() {
+        ServiceBusSessionReceiverClient sessionReceiverClient = consumerFactory.createReceiver("topic-1", ServiceBusEntityType.TOPIC);
+        assertNotNull(sessionReceiverClient);
+
+        consumerFactory.createReceiver("topic-1", ServiceBusEntityType.TOPIC);
+        assertEquals(1, topicReceiverAddedTimes);
+    }
+
+    @Test
+    void createMultipleSessionReceiverClients() {
+        ServiceBusSessionReceiverClient topicSessionReceiverClient = consumerFactory.createReceiver("topic-1", ServiceBusEntityType.QUEUE);
+        assertNotNull(topicSessionReceiverClient);
+
+        ServiceBusSessionReceiverClient anotherTopicSessionReceiverClient = consumerFactory.createReceiver("topic-2", ServiceBusEntityType.TOPIC);
+        assertNotNull(anotherTopicSessionReceiverClient);
+        assertEquals(2, topicReceiverAddedTimes);
+    }
+
+    @Test
+    void customizerShouldBeCalledOnEachCreatedClient() {
+        AtomicInteger shareClientCalledTimes = new AtomicInteger();
+        AtomicInteger sessionClientCalledTimes = new AtomicInteger();
+        AtomicInteger dedicatedSessionClientCalledTimes = new AtomicInteger();
+
+        DefaultServiceBusNamespaceConsumerFactory factory = (DefaultServiceBusNamespaceConsumerFactory) this.consumerFactory;
+
+        factory.addCustomizer(builder -> shareClientCalledTimes.getAndIncrement());
+        factory.addSessionReceiverCustomizer(builder -> sessionClientCalledTimes.getAndIncrement());
+
+        factory.addDedicatedSessionReceiverCustomizer("queue-1", builder -> dedicatedSessionClientCalledTimes.getAndIncrement());
+        factory.addDedicatedSessionReceiverCustomizer("topic-1", builder -> dedicatedSessionClientCalledTimes.getAndIncrement());
+
+        factory.createReceiver("queue-1", ServiceBusEntityType.QUEUE);
+        factory.createReceiver("queue-2", ServiceBusEntityType.QUEUE);
+
+        factory.createReceiver("topic-1", ServiceBusEntityType.TOPIC);
+        factory.createReceiver("topic-2", ServiceBusEntityType.TOPIC);
+
+        assertEquals(4, shareClientCalledTimes.get());
+        assertEquals(4, sessionClientCalledTimes.get());
+        assertEquals(2, dedicatedSessionClientCalledTimes.get());
+    }
+}
diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/core/ServiceBusTemplateSendTests.java b/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/core/ServiceBusTemplateSendTests.java
index 70fa975a20f0..afa6abc83e25 100644
--- a/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/core/ServiceBusTemplateSendTests.java
+++ b/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/core/ServiceBusTemplateSendTests.java
@@ -3,12 +3,24 @@
 
 package com.azure.spring.messaging.servicebus.core;
 
+import com.azure.core.util.IterableStream;
 import com.azure.messaging.servicebus.ServiceBusMessage;
+import com.azure.messaging.servicebus.ServiceBusReceivedMessage;
+import com.azure.messaging.servicebus.ServiceBusReceiverClient;
 import com.azure.messaging.servicebus.ServiceBusSenderAsyncClient;
+import com.azure.messaging.servicebus.ServiceBusSessionReceiverClient;
 import com.azure.spring.messaging.core.SendOperationTests;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.GenericMessage;
 import reactor.core.publisher.Mono;
 
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
@@ -25,17 +37,48 @@
 public class ServiceBusTemplateSendTests extends SendOperationTests<ServiceBusTemplate> {
 
     private ServiceBusSenderAsyncClient mockSenderClient;
+    private ServiceBusSessionReceiverClient mockSessionReceiverClient;
+    private ServiceBusReceiverClient mockReceiverClient;
     private ServiceBusProducerFactory producerFactory;
+    protected String replyEntity = "reply-event-hub";
 
     @BeforeEach
     public void setUp() {
         this.producerFactory = mock(ServiceBusProducerFactory.class);
+        ServiceBusConsumerFactory consumerFactory = mock(ServiceBusConsumerFactory.class);
         this.mockSenderClient = mock(ServiceBusSenderAsyncClient.class);
 
+        this.mockSessionReceiverClient = mock(ServiceBusSessionReceiverClient.class);
+        this.mockReceiverClient = mock(ServiceBusReceiverClient.class);
+        when(consumerFactory.createReceiver(eq(this.replyEntity), any())).thenReturn(this.mockSessionReceiverClient);
+        when(mockSessionReceiverClient.acceptSession(isA(String.class))).thenReturn(mockReceiverClient);
+
         when(this.producerFactory.createProducer(eq(this.destination), any())).thenReturn(this.mockSenderClient);
         when(this.mockSenderClient.sendMessage(isA(ServiceBusMessage.class))).thenReturn(this.mono);
 
-        this.sendOperation = new ServiceBusTemplate(producerFactory);
+        this.sendOperation = new ServiceBusTemplate(producerFactory, consumerFactory);
+
+        Map<String, Object> valueMap = new HashMap<>(message.getHeaders());
+        valueMap.put(MessageHeaders.REPLY_CHANNEL, replyEntity);
+        message = new GenericMessage<>("testPayload", valueMap);
+
+    }
+
+    @Test
+    public void testSendAndReceive() {
+        when(mockReceiverClient.receiveMessages(1)).thenReturn(mock());
+        final ServiceBusReceivedMessage replyMessage = this.sendOperation.sendAndReceive(destination, null, message);
+
+        assertNull(replyMessage);
+        verify(mockSenderClient, times(1)).sendMessage(isA(ServiceBusMessage.class));
+        verify(mockSessionReceiverClient, times(1)).acceptSession(isA(String.class));
+        verify(mockReceiverClient, times(1)).receiveMessages(1);
+    }
+
+    @Test
+    public void testSendAndReceiveReturnNull() {
+        when(mockReceiverClient.receiveMessages(1)).thenReturn(new IterableStream<>(List.of()));
+        assertNull(this.sendOperation.sendAndReceive(destination, null, message));
     }
 
     @Override
diff --git a/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/implementation/properties/merger/ConsumerPropertiesParentMergerTests.java b/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/implementation/properties/merger/ConsumerPropertiesParentMergerTests.java
new file mode 100644
index 000000000000..b94b240f2d4a
--- /dev/null
+++ b/sdk/spring/spring-messaging-azure-servicebus/src/test/java/com/azure/spring/messaging/servicebus/implementation/properties/merger/ConsumerPropertiesParentMergerTests.java
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.messaging.servicebus.implementation.properties.merger;
+
+import com.azure.core.amqp.AmqpTransportType;
+import com.azure.core.management.AzureEnvironment;
+import com.azure.spring.messaging.servicebus.core.properties.ConsumerProperties;
+import com.azure.spring.messaging.servicebus.core.properties.NamespaceProperties;
+import org.junit.jupiter.api.Test;
+
+import static com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider.CloudType.AZURE_CHINA;
+import static com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ConsumerPropertiesParentMergerTests {
+    private final ConsumerPropertiesParentMerger merger = new ConsumerPropertiesParentMerger();
+    
+    @Test
+    void childNotProvidedShouldUseParent() {
+        ConsumerProperties child = new ConsumerProperties();
+        child.setSessionEnabled(true);
+        String customEndpoint = "https://test.address.com:443";
+        NamespaceProperties parent = new NamespaceProperties();
+        parent.setConnectionString("parent-connection-str");
+        parent.getProxy().setHostname("parent-hostname");
+        parent.getProfile().setCloudType(AZURE_US_GOVERNMENT);
+        parent.setDomainName("parent-domain");
+        parent.setCustomEndpointAddress(customEndpoint);
+        parent.getClient().setTransportType(AmqpTransportType.AMQP_WEB_SOCKETS);
+
+        ConsumerProperties result = merger.merge(child, parent);
+
+        assertEquals("parent-connection-str", result.getConnectionString());
+        assertEquals("parent-hostname", result.getProxy().getHostname());
+        assertEquals(AZURE_US_GOVERNMENT, result.getProfile().getCloudType());
+        assertEquals(AzureEnvironment.AZURE_US_GOVERNMENT.getActiveDirectoryEndpoint(),
+            result.getProfile().getEnvironment().getActiveDirectoryEndpoint());
+        assertEquals("parent-domain", result.getDomainName());
+        assertEquals(customEndpoint, result.getCustomEndpointAddress());
+        assertEquals(AmqpTransportType.AMQP_WEB_SOCKETS, result.getClient().getTransportType());
+        assertTrue(result.getSessionEnabled());
+    }
+
+    @Test
+    void childProvidedShouldUseChild() {
+        ConsumerProperties child = new ConsumerProperties();
+        child.setConnectionString("child-connection-str");
+        child.getProxy().setHostname("child-hostname");
+        child.setPrefetchCount(3);
+        child.getProfile().setCloudType(AZURE_CHINA);
+        child.setDomainName("child-domain");
+        child.setCustomEndpointAddress("https://child.address.com:443");
+        child.getClient().setTransportType(AmqpTransportType.AMQP);
+
+        NamespaceProperties parent = new NamespaceProperties();
+        parent.setConnectionString("parent-connection-str");
+        parent.getProxy().setHostname("parent-hostname");
+        parent.getProfile().setCloudType(AZURE_US_GOVERNMENT);
+        parent.setDomainName("parent-domain");
+        parent.setCustomEndpointAddress("https://parent.address.com:443");
+        parent.getClient().setTransportType(AmqpTransportType.AMQP_WEB_SOCKETS);
+
+        ConsumerProperties result = merger.merge(child, parent);
+
+        assertEquals("child-connection-str", result.getConnectionString());
+        assertEquals("child-hostname", result.getProxy().getHostname());
+        assertEquals(3, result.getPrefetchCount());
+        assertEquals("child-domain", result.getDomainName());
+        assertEquals("https://child.address.com:443", result.getCustomEndpointAddress());
+        assertEquals(AZURE_CHINA, result.getProfile().getCloudType());
+        assertEquals(AzureEnvironment.AZURE_CHINA.getActiveDirectoryEndpoint(),
+            result.getProfile().getEnvironment().getActiveDirectoryEndpoint());
+        assertEquals(AmqpTransportType.AMQP, result.getClient().getTransportType());
+    }
+}


Reviews:
user: login: saragluna
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T05:42:07Z
user: login: moarychan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T06:06:49Z
user: login: saragluna
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-07T06:08:50Z

Review comments:
diff_hunk: @@ -0,0 +1,214 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.messaging.servicebus.core;
+
+import com.azure.core.credential.TokenCredential;
+import com.azure.messaging.servicebus.ServiceBusClientBuilder;
+import com.azure.messaging.servicebus.ServiceBusSessionReceiverClient;
+import com.azure.messaging.servicebus.models.ServiceBusReceiveMode;
+import com.azure.spring.cloud.core.credential.AzureCredentialResolver;
+import com.azure.spring.cloud.core.customizer.AzureServiceClientBuilderCustomizer;
+import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier;
+import com.azure.spring.cloud.service.implementation.servicebus.factory.ServiceBusSessionReceiverClientBuilderFactory;
+import com.azure.spring.cloud.service.servicebus.properties.ServiceBusEntityType;
+import com.azure.spring.messaging.ConsumerIdentifier;
+import com.azure.spring.messaging.PropertiesSupplier;
+import com.azure.spring.messaging.servicebus.core.properties.ConsumerProperties;
+import com.azure.spring.messaging.servicebus.core.properties.NamespaceProperties;
+import com.azure.spring.messaging.servicebus.implementation.properties.merger.ConsumerPropertiesParentMerger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.lang.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * The {@link ServiceBusConsumerFactory} implementation to produce new {@link ServiceBusSessionReceiverClient} instances
+ * for provided {@link NamespaceProperties} and optional producer {@link PropertiesSupplier} on each
+ * {@link #createReceiver} invocation.
+ * <p>
+ * {@link ServiceBusSessionReceiverClient} produced by this factory will share the same namespace level configuration, but
+ * if a configuration entry is provided at both producer and namespace level, the producer level configuration will
+ * take advantage.
+ * </p>
+ * @since 5.22.0
+ */
+public final class DefaultServiceBusNamespaceConsumerFactory implements ServiceBusConsumerFactory, DisposableBean {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultServiceBusNamespaceConsumerFactory.class);
+    private static final String LOG_IGNORE_NULL_CUSTOMIZER = "The provided '{}' customizer is null, will ignore it.";
+    private static final String LOG_IGNORE_NULL_DEDICATED_CUSTOMIZER = "The provided '{}' dedicated customizer is null, will ignore it.";
+    private final List<Listener> listeners = new ArrayList<>();
+    private final NamespaceProperties namespaceProperties;
+    private final PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> propertiesSupplier;
+    private final Map<String, ServiceBusSessionReceiverClient> clients = new ConcurrentHashMap<>();
+    private final ConsumerPropertiesParentMerger parentMerger = new ConsumerPropertiesParentMerger();
+    private final List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder>> customizers = new ArrayList<>();
+    private final List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder>> sessionReceiverCustomizers = new ArrayList<>();
+    private final Map<String, List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder>>> dedicatedSessionReceiverCustomizers = new HashMap<>();
+    private AzureCredentialResolver<TokenCredential> tokenCredentialResolver = null;
+    private TokenCredential defaultCredential = null;
+
+    /**
+     * Construct a factory with the provided namespace level configuration.
+     * @param namespaceProperties the namespace properties
+     */
+    public DefaultServiceBusNamespaceConsumerFactory(NamespaceProperties namespaceProperties) {
+        this(namespaceProperties, key -> null);
+    }
+
+    /**
+     * Construct a factory with the provided namespace level configuration and producer {@link PropertiesSupplier}.
+     * @param namespaceProperties the namespace properties.
+     * @param supplier the {@link PropertiesSupplier} to supply {@link ConsumerProperties} for each queue/topic entity.
+     */
+    public DefaultServiceBusNamespaceConsumerFactory(NamespaceProperties namespaceProperties,
+                                                     PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> supplier) {
+        this.namespaceProperties = namespaceProperties;
+        this.propertiesSupplier = supplier == null ? key -> null : supplier;
+    }
+
+    @Override
+    public ServiceBusSessionReceiverClient createReceiver(String name) {
+        return doCreateReceiver(name, null);
+    }
+
+    @Override
+    public ServiceBusSessionReceiverClient createReceiver(String name, ServiceBusEntityType entityType) {
+        ConsumerProperties consumerProperties = this.propertiesSupplier.getProperties(new ConsumerIdentifier(name)) != null
+            ? this.propertiesSupplier.getProperties(new ConsumerIdentifier(name)) : new ConsumerProperties();
+        if (entityType != null) {
+            consumerProperties.setEntityType(entityType);
+        }
+        return doCreateReceiver(name, consumerProperties);
+    }
+
+    @Override
+    public void addListener(Listener listener) {
+        this.listeners.add(listener);
+    }
+
+    @Override
+    public boolean removeListener(Listener listener) {
+        return this.listeners.remove(listener);
+    }
+
+    @Override
+    public void destroy() {
+        clients.forEach((name, receiver) -> {
+            listeners.forEach(l -> l.receiverRemoved(name, receiver));
+            receiver.close();
+        });
+        this.clients.clear();
+        this.listeners.clear();
+    }
+
+    private ServiceBusSessionReceiverClient doCreateReceiver(String name, @Nullable ConsumerProperties properties) {
+        return clients.computeIfAbsent(name, entityName -> {
+            ConsumerProperties consumerProperties = parentMerger.merge(properties, this.namespaceProperties);
+            consumerProperties.setEntityName(entityName);
+
+            ServiceBusSessionReceiverClient receiverClient;
+            if (Boolean.TRUE.equals(consumerProperties.getSessionEnabled())) {
+                ServiceBusSessionReceiverClientBuilderFactory factory =
+                    new ServiceBusSessionReceiverClientBuilderFactory(consumerProperties, this.customizers);
+
+                factory.setDefaultTokenCredential(this.defaultCredential);
+                factory.setTokenCredentialResolver(this.tokenCredentialResolver);
+                factory.setSpringIdentifier(AzureSpringIdentifier.AZURE_SPRING_INTEGRATION_SERVICE_BUS);
path: sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceConsumerFactory.java
user: login: saragluna
body: is there a messaging identifier now?
created_at: 2025-04-03T05:33:14Z
author_association: MEMBER
diff_hunk: @@ -292,7 +293,7 @@ public void addBuilderCustomizer(String entityName,
                                      String subscription,
                                      AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusProcessorClientBuilder> customizer) {
         if (customizer == null) {
-            LOGGER.debug(LOG_IGNORE_NULL_CUSTOMIZER, ServiceBusClientBuilder.ServiceBusProcessorClientBuilder.class.getName());
+            LOGGER.debug(LOG_IGNORE_NULL_DEDICATED_CUSTOMIZER, ServiceBusClientBuilder.ServiceBusProcessorClientBuilder.class.getName());
path: sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceProcessorFactory.java
user: login: saragluna
body: I'd prefer not using any static variable of log format for such case, plain text is much easier to read.
created_at: 2025-04-03T05:35:12Z
author_association: MEMBER
diff_hunk: @@ -142,8 +166,9 @@ AzureMessageConverter<ServiceBusReceivedMessage, ServiceBusMessage> serviceBusMe
         @ConditionalOnBean(ServiceBusProducerFactory.class)
         ServiceBusTemplate serviceBusTemplate(AzureServiceBusProperties properties,
                                               ServiceBusProducerFactory senderClientFactory,
path: sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/servicebus/AzureServiceBusMessagingAutoConfiguration.java
user: login: saragluna
body: Please help update this `senderClientFactory` vairable name to `producerFactory`
created_at: 2025-04-03T05:37:58Z
author_association: MEMBER
diff_hunk: @@ -49,6 +73,82 @@ public <U> Mono<Void> sendAsync(String destination, Message<U> message) {
         return senderAsyncClient.sendMessage(serviceBusMessage);
     }
 
+    /**
+     * Basic RPC pattern usage. Send a message to the destination and wait for a reply message from the replay channel,
+     * which must be specified by the message header {@link MessageHeaders#REPLY_CHANNEL} and the session must be enabled;
+     * the message header {@link ServiceBusMessageHeaders#REPLY_TO_SESSION_ID} is used to specify a unique reply session ID,
+     * if not set, a random {@link UUID} will be used.
+     *
+     * @param <U> The type of the message payload.
+     * @param destination topic or queue name.
+     * @param entityType type of Service Bus entity.
+     * @param message Message to be sent to the Service Bus entity.
+     *
+     * @return the reply message of the response. If the reply message fails to be obtained, null is returned.
+     *
+     * @throws IllegalStateException if sender or receiver is already disposed.
+     * @since 5.22.0
+     */
+    public <U> ServiceBusReceivedMessage sendAndReceive(String destination,
+                                    ServiceBusEntityType entityType,
path: sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/ServiceBusTemplate.java
user: login: saragluna
body: Should we provide an api without needing the `entityType`?
created_at: 2025-04-03T05:39:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,214 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.messaging.servicebus.core;
+
+import com.azure.core.credential.TokenCredential;
+import com.azure.messaging.servicebus.ServiceBusClientBuilder;
+import com.azure.messaging.servicebus.ServiceBusSessionReceiverClient;
+import com.azure.messaging.servicebus.models.ServiceBusReceiveMode;
+import com.azure.spring.cloud.core.credential.AzureCredentialResolver;
+import com.azure.spring.cloud.core.customizer.AzureServiceClientBuilderCustomizer;
+import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier;
+import com.azure.spring.cloud.service.implementation.servicebus.factory.ServiceBusSessionReceiverClientBuilderFactory;
+import com.azure.spring.cloud.service.servicebus.properties.ServiceBusEntityType;
+import com.azure.spring.messaging.ConsumerIdentifier;
+import com.azure.spring.messaging.PropertiesSupplier;
+import com.azure.spring.messaging.servicebus.core.properties.ConsumerProperties;
+import com.azure.spring.messaging.servicebus.core.properties.NamespaceProperties;
+import com.azure.spring.messaging.servicebus.implementation.properties.merger.ConsumerPropertiesParentMerger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.lang.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * The {@link ServiceBusConsumerFactory} implementation to produce new {@link ServiceBusSessionReceiverClient} instances
+ * for provided {@link NamespaceProperties} and optional producer {@link PropertiesSupplier} on each
+ * {@link #createReceiver} invocation.
+ * <p>
+ * {@link ServiceBusSessionReceiverClient} produced by this factory will share the same namespace level configuration, but
+ * if a configuration entry is provided at both producer and namespace level, the producer level configuration will
+ * take advantage.
+ * </p>
+ * @since 5.22.0
+ */
+public final class DefaultServiceBusNamespaceConsumerFactory implements ServiceBusConsumerFactory, DisposableBean {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultServiceBusNamespaceConsumerFactory.class);
+    private static final String LOG_IGNORE_NULL_CUSTOMIZER = "The provided '{}' customizer is null, will ignore it.";
+    private static final String LOG_IGNORE_NULL_DEDICATED_CUSTOMIZER = "The provided '{}' dedicated customizer is null, will ignore it.";
+    private final List<Listener> listeners = new ArrayList<>();
+    private final NamespaceProperties namespaceProperties;
+    private final PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> propertiesSupplier;
+    private final Map<String, ServiceBusSessionReceiverClient> clients = new ConcurrentHashMap<>();
+    private final ConsumerPropertiesParentMerger parentMerger = new ConsumerPropertiesParentMerger();
+    private final List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder>> customizers = new ArrayList<>();
+    private final List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder>> sessionReceiverCustomizers = new ArrayList<>();
+    private final Map<String, List<AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder.ServiceBusSessionReceiverClientBuilder>>> dedicatedSessionReceiverCustomizers = new HashMap<>();
+    private AzureCredentialResolver<TokenCredential> tokenCredentialResolver = null;
+    private TokenCredential defaultCredential = null;
+
+    /**
+     * Construct a factory with the provided namespace level configuration.
+     * @param namespaceProperties the namespace properties
+     */
+    public DefaultServiceBusNamespaceConsumerFactory(NamespaceProperties namespaceProperties) {
+        this(namespaceProperties, key -> null);
+    }
+
+    /**
+     * Construct a factory with the provided namespace level configuration and producer {@link PropertiesSupplier}.
+     * @param namespaceProperties the namespace properties.
+     * @param supplier the {@link PropertiesSupplier} to supply {@link ConsumerProperties} for each queue/topic entity.
+     */
+    public DefaultServiceBusNamespaceConsumerFactory(NamespaceProperties namespaceProperties,
+                                                     PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> supplier) {
+        this.namespaceProperties = namespaceProperties;
+        this.propertiesSupplier = supplier == null ? key -> null : supplier;
+    }
+
+    @Override
+    public ServiceBusSessionReceiverClient createReceiver(String name) {
+        return doCreateReceiver(name, null);
+    }
+
+    @Override
+    public ServiceBusSessionReceiverClient createReceiver(String name, ServiceBusEntityType entityType) {
+        ConsumerProperties consumerProperties = this.propertiesSupplier.getProperties(new ConsumerIdentifier(name)) != null
+            ? this.propertiesSupplier.getProperties(new ConsumerIdentifier(name)) : new ConsumerProperties();
+        if (entityType != null) {
+            consumerProperties.setEntityType(entityType);
+        }
+        return doCreateReceiver(name, consumerProperties);
+    }
+
+    @Override
+    public void addListener(Listener listener) {
+        this.listeners.add(listener);
+    }
+
+    @Override
+    public boolean removeListener(Listener listener) {
+        return this.listeners.remove(listener);
+    }
+
+    @Override
+    public void destroy() {
+        clients.forEach((name, receiver) -> {
+            listeners.forEach(l -> l.receiverRemoved(name, receiver));
+            receiver.close();
+        });
+        this.clients.clear();
+        this.listeners.clear();
+    }
+
+    private ServiceBusSessionReceiverClient doCreateReceiver(String name, @Nullable ConsumerProperties properties) {
+        return clients.computeIfAbsent(name, entityName -> {
+            ConsumerProperties consumerProperties = parentMerger.merge(properties, this.namespaceProperties);
+            consumerProperties.setEntityName(entityName);
+
+            ServiceBusSessionReceiverClient receiverClient;
+            if (Boolean.TRUE.equals(consumerProperties.getSessionEnabled())) {
+                ServiceBusSessionReceiverClientBuilderFactory factory =
+                    new ServiceBusSessionReceiverClientBuilderFactory(consumerProperties, this.customizers);
+
+                factory.setDefaultTokenCredential(this.defaultCredential);
+                factory.setTokenCredentialResolver(this.tokenCredentialResolver);
+                factory.setSpringIdentifier(AzureSpringIdentifier.AZURE_SPRING_INTEGRATION_SERVICE_BUS);
path: sdk/spring/spring-messaging-azure-servicebus/src/main/java/com/azure/spring/messaging/servicebus/core/DefaultServiceBusNamespaceConsumerFactory.java
user: login: moarychan
body: No, the message and integration layers are not yet distinguished.
created_at: 2025-04-03T06:06:49Z
author_association: MEMBER

PR comments:
user: login: azure-sdk
created_at: 2025-04-02T03:38:02Z
body: **API change check**

APIView has identified API level changes in this PR and created following API reviews.

[com.azure.spring:spring-messaging-azure-servicebus](https://apiview.dev/Assemblies/Review/6fbb1bdeffae4b59934bbb6c6e7ecd8a?revisionId=8c2ae9497312471e867106b3d1f94b70)


author_association: COLLABORATOR
user: login: moarychan
created_at: 2025-04-07T08:29:02Z
body: Queue scenario:

```java
@Configuration
public class QueueConfiguration {

    @Bean
    PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> consumerPropertiesSupplier() {
        return key -> {
            ConsumerProperties consumerProperties = new ConsumerProperties();
            consumerProperties.setSessionEnabled(true);
            return consumerProperties;
        };
    }
}

@RestController
@RequestMapping("/rpc")
public class ServiceBusTemplateQueueRPCController {
    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceBusTemplateQueueRPCController.class);
    private final ServiceBusTemplate serviceBusTemplate;
    private static final String RPC_QUEUE_NAME = "rpc";
    private static final String RPC_QUEUE_REPLY_NAME = "rpc-reply-session";

    public ServiceBusTemplateQueueRPCController(ServiceBusTemplate serviceBusTemplate) {
        this.serviceBusTemplate = serviceBusTemplate;
    }

    @PostMapping("/queue")
    public String send(@RequestParam("message") String message) {
        return queueRpc(message);
    }

    private String queueRpc(String flag) {
        ProductOfferEvent event = new ProductOfferEvent();
        event.setProductId(new Random().nextInt(1000));
        event.setDiscountOffer(0.25);
        event.setTimestamp(Instant.now());
        event.setContent("Queue - RPC - " + flag);
        LOGGER.info("Sent queue message: {}", event);

        Map<String, Object> headers = new HashMap<>();
        headers.put(MessageHeaders.REPLY_CHANNEL, RPC_QUEUE_REPLY_NAME);
        Message<ProductOfferEvent> message = MessageBuilder.createMessage(event, new MessageHeaders(headers));

        ServiceBusReceivedMessage replyMessage = serviceBusTemplate.sendAndReceive(RPC_QUEUE_NAME, ServiceBusEntityType.QUEUE, message);
        ProductOfferEvent productOfferEvent = replyMessage.getBody().toObject(ProductOfferEvent.class);
        LOGGER.info("Received reply message: {}", productOfferEvent);
        return productOfferEvent.getContent();
    }

    @ServiceBusListener(destination = RPC_QUEUE_NAME)
    public void handleMessageFromServiceBusTopic(ServiceBusReceivedMessage message) {
        ProductOfferEvent productOfferEvent = message.getBody().toObject(ProductOfferEvent.class);
        LOGGER.info("Consuming RPC queue message: {}", productOfferEvent);

        if (StringUtils.hasText(message.getReplyTo())) {
            productOfferEvent.setContent(productOfferEvent.getContent() + " - processed.");

            Map<String, Object> headers = new HashMap<>();
            headers.put(ServiceBusMessageHeaders.SESSION_ID, message.getReplyToSessionId());

            serviceBusTemplate.send(message.getReplyTo(), MessageBuilder.createMessage(productOfferEvent, new MessageHeaders(headers)));
            LOGGER.info("Sent reply message: {}", productOfferEvent);
        }
    }
}
```
author_association: MEMBER
user: login: moarychan
created_at: 2025-04-07T08:29:54Z
body: Topic scenario:

```java
@Configuration
public class TopicConfiguration {


    @Bean
    PropertiesSupplier<ConsumerIdentifier, ConsumerProperties> consumerPropertiesSupplier() {
        return key -> {
            ConsumerProperties consumerProperties = new ConsumerProperties();
            consumerProperties.setSessionEnabled(true);
            consumerProperties.setSubscriptionName("sub-rpc-reply-session");
            return consumerProperties;
        };
    }
}

@RestController
@RequestMapping("/rpc")
public class ServiceBusTemplateTopicRPCController {
    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceBusTemplateTopicRPCController.class);
    private final ServiceBusTemplate serviceBusTemplate;
    private static final String RPC_TOPIC_NAME = "topic-rpc";
    private static final String RPC_REPLY_TOPIC_NAME = "topic-rpc-reply";
    private static final String RPC_TOPIC_SUB_NAME = "sub-rpc";

    public ServiceBusTemplateTopicRPCController(ServiceBusTemplate serviceBusTemplate) {
        this.serviceBusTemplate = serviceBusTemplate;
    }

    @PostMapping("/topic")
    public String send(@RequestParam("message") String message) {
        return topicRpc(message);
    }

    private String topicRpc(String flag) {
        ProductOfferEvent event = new ProductOfferEvent();
        event.setProductId(new Random().nextInt(1000));
        event.setDiscountOffer(0.25);
        event.setTimestamp(Instant.now());
        event.setContent("Topic - RPC - " + flag);
        LOGGER.info("Sent topic message: {}", event);

        Map<String, Object> headers = new HashMap<>();
        headers.put(MessageHeaders.REPLY_CHANNEL, RPC_REPLY_TOPIC_NAME);
        Message<ProductOfferEvent> message = MessageBuilder.createMessage(event, new MessageHeaders(headers));

        ServiceBusReceivedMessage replyMessage = serviceBusTemplate.sendAndReceive(RPC_TOPIC_NAME, ServiceBusEntityType.TOPIC, message);
        ProductOfferEvent productOfferEvent = replyMessage.getBody().toObject(ProductOfferEvent.class);
        LOGGER.info("Received topic reply message: {}", productOfferEvent);
        return productOfferEvent.getContent();
    }

    @ServiceBusListener(destination = RPC_TOPIC_NAME, group = RPC_TOPIC_SUB_NAME)
    public void handleMessageFromServiceBusQueue(ServiceBusReceivedMessage message) {
        ProductOfferEvent productOfferEvent = message.getBody().toObject(ProductOfferEvent.class);
        LOGGER.info("Consuming RPC TOPIC message: {}", productOfferEvent);

        if (StringUtils.hasText(message.getReplyTo())) {
            productOfferEvent.setContent(productOfferEvent.getContent() + " - topic processed.");

            Map<String, Object> headers = new HashMap<>();
            headers.put(ServiceBusMessageHeaders.SESSION_ID, message.getReplyToSessionId());
            serviceBusTemplate.send(message.getReplyTo(), MessageBuilder.createMessage(productOfferEvent, new MessageHeaders(headers)));
            LOGGER.info("Sent topic reply message: {}", productOfferEvent);
        }
    }
}
```
author_association: MEMBER