C# 不使用DI框架如何创建Azure ServiceBusSender单例实例
无DI/IoC场景下ServiceBusSender单例实现方案
Azure Service Bus官方SDK中的ServiceBusClient、ServiceBusSender均为线程安全类型,设计层面就推荐作为单例长期复用,避免频繁创建销毁带来的连接开销。以下是纯原生实现的线程安全单例方案,无需依赖任何DI/IoC框架:
using Azure.Messaging.ServiceBus; using System.Collections.Concurrent; public sealed class ServiceBusQueueSender : IDisposable { // 同个Service Bus命名空间全局复用一个Client,减少AMQP连接开销 private static readonly ServiceBusClient _serviceBusClient; private readonly ServiceBusSender _sender; private bool _disposed = false; // 静态构造函数仅执行一次,完成全局Client初始化 static ServiceBusQueueSender() { // 连接字符串从环境变量读取,禁止硬编码到业务代码 var connectionString = Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString"); if (string.IsNullOrWhiteSpace(connectionString)) { throw new InvalidOperationException("未配置Azure Service Bus连接字符串"); } // 如需自定义重试、超时、代理等配置,可在此处传入ServiceBusClientOptions实例 _serviceBusClient = new ServiceBusClient(connectionString); } // 私有构造函数,禁止外部直接实例化 private ServiceBusQueueSender(string queueOrTopicName) { _sender = _serviceBusClient.CreateSender(queueOrTopicName); } // 按队列/主题名称缓存Sender实例,线程安全 private static readonly ConcurrentDictionary<string, ServiceBusQueueSender> _senderCache = new(); /// <summary> /// 获取指定队列/主题的Sender单例 /// </summary> public static ServiceBusQueueSender GetSender(string queueOrTopicName) { if (string.IsNullOrWhiteSpace(queueOrTopicName)) { throw new ArgumentException("队列/主题名不能为空", nameof(queueOrTopicName)); } return _senderCache.GetOrAdd(queueOrTopicName, name => new ServiceBusQueueSender(name)); } /// <summary> /// 发送单条消息 /// </summary> public async Task SendMessageAsync(ServiceBusMessage message, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, this); await _sender.SendMessageAsync(message, cancellationToken).ConfigureAwait(false); } /// <summary> /// 批量发送消息 /// </summary> public async Task SendBatchAsync(IReadOnlyList<ServiceBusMessage> messages, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, this); await _sender.SendMessagesAsync(messages, cancellationToken).ConfigureAwait(false); } public void Dispose() { if (_disposed) return; _sender.Dispose(); _disposed = true; GC.SuppressFinalize(this); } /// <summary> /// 应用退出时调用,释放全局Client连接资源 /// </summary> public static async Task ShutdownAsync() { foreach (var sender in _senderCache.Values) { sender.Dispose(); } _senderCache.Clear(); await _serviceBusClient.DisposeAsync().ConfigureAwait(false); } }
使用示例
// 获取指定队列的Sender实例,重复传入同个队列名会返回同一个实例 var orderSender = ServiceBusQueueSender.GetSender("order-processing-queue"); // 构造消息发送 var msg = new ServiceBusMessage(BinaryData.FromString("{\"orderId\":123, \"status\":\"paid\"}")) { MessageId = Guid.NewGuid().ToString(), ContentType = "application/json" }; await orderSender.SendMessageAsync(msg); // 应用退出前执行资源释放,控制台程序可在主逻辑结束前调用,桌面程序可绑定应用退出事件 // await ServiceBusQueueSender.ShutdownAsync();
关键说明
- 静态构造函数保证
ServiceBusClient全局只初始化一次,ConcurrentDictionary保证多线程场景下每个队列/主题的Sender只会创建一个实例,完全满足单例要求 - 所有实例均为线程安全,可在多线程、异步场景下直接调用,无需额外加锁
- 禁止每次发送消息都重新创建Client或Sender,否则会导致TCP连接数暴涨、发送延迟升高,甚至触发Service Bus的连接限流
- 生产环境不要硬编码连接字符串,建议从环境变量、配置中心或者密钥管理服务读取
- 自定义重试策略、超时时间、传输模式等全局配置,在初始化
ServiceBusClient时传入ServiceBusClientOptions即可,所有Sender会自动继承该配置 - 该实现同时兼容向Service Bus主题(Topic)发送消息的场景,只需要传入主题名调用
GetSender方法即可
内容的提问来源于stack exchange,提问作者Aryan
相关产品推荐
相关产品推荐

