SignalR 1.2.2+Autofac+NUnit测试报错:已添加相同键项
解决SignalR 1.2.2 + Autofac + NUnit测试中"同键重复添加"异常
我完全懂你这种被测试环境卡壳的烦躁——Web上跑好好的,一到单元测试就炸,堆栈还指向SignalR的内部字典,确实头大。先给你拆解问题根源,再给两个可行的解决方案:
问题到底出在哪?
这个异常是SignalR 1.2.2的ReflectedHubDescriptorProvider在构建Hub缓存时,把同一个ProductHub重复往字典里加导致的。
- Web环境里
Application_Start只执行一次,Hub扫描注册也只做一次,所以没问题; - 但NUnit测试中,每次测试都会重新触发初始化逻辑,而
GlobalHost是静态单例,它的Hub缓存是懒加载的,多次初始化就会导致同一个Hub被重复注册,直接抛出重复键异常。
方案1:测试中重置SignalR静态状态
既然GlobalHost是静态的,那我们就在每个测试前手动清理它的状态,避免跨测试污染。你可以在测试类里加个SetUp方法:
[SetUp] public void SetupTestEnvironment() { // 1. 先重置GlobalHost的默认依赖解析器 GlobalHost.DependencyResolver = new DefaultDependencyResolver(); // 2. 反射清除HubDescriptorProvider的缓存(SignalR 1.2.2没公开清理方法,只能硬来) var hubManager = GlobalHost.DependencyResolver.Resolve<IHubManager>(); if (hubManager is DefaultHubManager defaultHubManager) { var providersField = typeof(DefaultHubManager).GetField("_providers", BindingFlags.Instance | BindingFlags.NonPublic); if (providersField != null) { var providers = (List<IHubDescriptorProvider>)providersField.GetValue(defaultHubManager); foreach (var provider in providers.OfType<ReflectedHubDescriptorProvider>()) { var cacheField = typeof(ReflectedHubDescriptorProvider).GetField("_hubs", BindingFlags.Instance | BindingFlags.NonPublic); if (cacheField != null) { var lazyCache = (Lazy<IDictionary<string, HubDescriptor>>)cacheField.GetValue(provider); var resetMethod = typeof(Lazy<>).GetMethod("ResetValue", BindingFlags.Instance | BindingFlags.NonPublic); resetMethod?.Invoke(lazyCache, null); } } } } // 3. 重新初始化Autofac容器和SignalR配置(和Application_Start保持一致) var builder = new ContainerBuilder(); builder.RegisterType<Product_WCF_Service>().AsSelf(); builder.RegisterType<DbFactory>().As<IDbFactory>(); builder.RegisterType<UnitOfWork>().As<IUnitOfWork>(); builder.RegisterAssemblyTypes(typeof(ProductService).Assembly) .Where(t => t.Name.EndsWith("Service")) .AsImplementedInterfaces(); builder.RegisterAssemblyTypes(typeof(ProductRepository).Assembly) .Where(t => t.Name.EndsWith("Repository")) .AsImplementedInterfaces(); builder.RegisterType<ProductHub>().ExternallyOwned(); var container = builder.Build(); GlobalHost.DependencyResolver = new AutofacDependencyResolver(container); AutofacHostFactory.Container = container; }
方案2:抽象HubContext获取逻辑(更优雅)
如果你的WCF服务只是在某个地方需要调用Hub推送,那不如把GlobalHost.GetHubContext()的逻辑抽象成接口,测试时直接模拟这个接口,彻底绕过SignalR的静态依赖:
第一步:定义抽象接口
public interface IHubContextProvider { IHubContext GetProductHubContext(); }
第二步:生产环境实现
public class DefaultHubContextProvider : IHubContextProvider { public IHubContext GetProductHubContext() { return GlobalHost.ConnectionManager.GetHubContext<ProductHub>(); } }
第三步:修改WCF服务构造函数
把IHubContextProvider注入进去,代替直接调用GlobalHost:
public class Product_WCF_Service : IProduct_WCF_Service { readonly IUnitOfWork _unitOfWork; readonly IProductService _productService; readonly IHubContextProvider _hubContextProvider; public Product_WCF_Service(IUnitOfWork unitOfWork, IProductService productService, IHubContextProvider hubContextProvider) { _unitOfWork = unitOfWork; _productService = productService; _hubContextProvider = hubContextProvider; } // 原来用到GlobalHost.GetHubContext的地方,改成_hubContextProvider.GetProductHubContext() }
第四步:测试中模拟依赖
[TestFixture] public class WCF_Product_Service_Tests { private Mock<IUnitOfWork> _unitOfWorkMock; private Mock<IProductService> _productServiceMock; private Mock<IHubContextProvider> _hubContextProviderMock; private Mock<IHubContext> _productHubContextMock; public WCF_Product_Service_Tests() { _unitOfWorkMock = new Mock<IUnitOfWork>(); _productServiceMock = new Mock<IProductService>(); _productHubContextMock = new Mock<IHubContext>(); _hubContextProviderMock = new Mock<IHubContextProvider>(); // 配置模拟:调用GetProductHubContext时返回我们的Mock对象 _hubContextProviderMock.Setup(p => p.GetProductHubContext()) .Returns(_productHubContextMock.Object); } [Test] public void RetreiveDataFromWCF_Service() { byte commitStatus = 1; string response = string.Empty; var service = new Product_WCF_Service( _unitOfWorkMock.Object, _productServiceMock.Object, _hubContextProviderMock.Object); service.CreateProduct("", out response, out commitStatus); // 如果需要验证Hub的调用逻辑,直接验证Mock即可 // _productHubContextMock.Verify(h => h.Clients.All.someMethod(It.IsAny<string>()), Times.Once); } }
这种方式完全隔离了测试和SignalR的静态组件,不仅解决了当前的异常,还让测试更纯粹,只关注业务逻辑。
额外提醒
SignalR 1.2.2确实太老了,虽然你没法升级,但尽量在测试中避免直接依赖它的静态类——依赖注入抽象是解决这类问题的通用思路。
内容的提问来源于stack exchange,提问作者DKar
相关产品推荐
相关产品推荐

