如何在C#中通过公共接口防止客户覆盖DI注册的NuGet包服务?
防止ASP.NET DI替换NuGet包服务实现的实践方案
我们开发了一款可供客户添加至其ASP.NET站点的NuGet包,核心代码如下:
public interface IOurService { void DoStuff(); } public class OurService: IOurService { public void DoStuff() { // 核心业务逻辑 } }
我们提供了DI注册方法:
public static void AddOurServices(this IServiceCollection services) { services.AddSingleton<IOurService, OurService>(); }
保留IOurService公共接口是为了让客户能在单元测试中Mock该服务,但目前存在问题:客户可通过后续DI注册替换IOurService的实现:
services.AddOurServices(); services.AddSingleton<IOurService, TheirImplementation>();
替换后若出现问题会产生额外支持需求,我们需要在保留Mock能力的前提下,从源头避免此类情况。
可行实践方案
方案一:内部核心服务+公共门面隔离(推荐)
通过将核心服务设为内部类型,对外暴露的IOurService仅作为门面代理,从架构上隔离客户可修改的部分与包内核心逻辑:
- 将核心服务
OurService改为internal,禁止客户直接访问 - 创建公共门面类实现
IOurService,内部依赖核心服务 - 注册时同时注册内部核心服务和门面类,包内逻辑直接依赖内部核心服务而非公共接口
代码示例:
// 保留公共接口,供客户Mock测试 public interface IOurService { void DoStuff(); } // 核心服务改为internal,客户无法直接引用 internal class OurService { public void DoStuff() { // 核心业务逻辑 } } // 公共门面类,代理核心服务的调用 public class OurServiceFacade : IOurService { private readonly OurService _innerService; public OurServiceFacade(OurService innerService) { _innerService = innerService; } public void DoStuff() { _innerService.DoStuff(); } } // DI注册方法 public static void AddOurServices(this IServiceCollection services) { // 注册内部核心服务,客户无法替换(因无法访问OurService类型) services.AddSingleton<OurService>(); // 注册门面类作为IOurService的对外实现 services.AddSingleton<IOurService, OurServiceFacade>(); }
优势:
- 客户依然可以Mock
IOurService测试自身代码 - 包内核心逻辑依赖
OurService,完全不受客户替换IOurService的影响 - 从设计层面彻底避免了核心服务被替换的可能
方案二:注册时锁定并拦截后续替换
通过在注册方法中清理已有注册,并添加拦截逻辑阻止后续替换IOurService:
public static void AddOurServices(this IServiceCollection services) { // 移除所有已存在的IOurService注册,防止提前替换 var existingDescriptors = services.Where(d => d.ServiceType == typeof(IOurService)).ToList(); foreach (var descriptor in existingDescriptors) { services.Remove(descriptor); } // 注册我们的服务实现 services.AddSingleton<IOurService, OurService>(); // 通过包装ServiceCollection拦截后续注册 services.Replace(ServiceDescriptor.Singleton<IServiceCollection>(new LockedServiceCollection(services))); } // 自定义包装类,拦截IOurService的注册 internal class LockedServiceCollection : IServiceCollection { private readonly IServiceCollection _innerCollection; public LockedServiceCollection(IServiceCollection innerCollection) { _innerCollection = innerCollection; } public int Count => _innerCollection.Count; public bool IsReadOnly => _innerCollection.IsReadOnly; public ServiceDescriptor this[int index] { get => _innerCollection[index]; set => _innerCollection[index] = value; } public void Add(ServiceDescriptor item) { if (item.ServiceType == typeof(IOurService)) { throw new InvalidOperationException("无法替换IOurService的实现,请使用官方提供的服务。"); } _innerCollection.Add(item); } // 其他接口成员委托给_innerCollection实现 public void Clear() => _innerCollection.Clear(); public bool Contains(ServiceDescriptor item) => _innerCollection.Contains(item); public void CopyTo(ServiceDescriptor[] array, int arrayIndex) => _innerCollection.CopyTo(array, arrayIndex); public IEnumerator<ServiceDescriptor> GetEnumerator() => _innerCollection.GetEnumerator(); public int IndexOf(ServiceDescriptor item) => _innerCollection.IndexOf(item); public void Insert(int index, ServiceDescriptor item) => _innerCollection.Insert(index, item); public bool Remove(ServiceDescriptor item) => _innerCollection.Remove(item); public void RemoveAt(int index) => _innerCollection.RemoveAt(index); IEnumerator IEnumerable.GetEnumerator() => _innerCollection.GetEnumerator(); }
优势:直接阻止客户替换IOurService的操作,明确传递不允许替换的规则
劣势:实现复杂,且可能与客户的DI扩展冲突,影响开发体验
方案三:工厂模式控制实例创建
通过注册工厂方法确保IOurService实例来自核心服务,同时允许客户替换IOurService但不影响包内逻辑:
public static void AddOurServices(this IServiceCollection services) { // 注册核心服务为单例 services.AddSingleton<OurService>(); // 通过工厂方法注册IOurService,返回核心服务实例 services.AddSingleton<IOurService>(sp => sp.GetRequiredService<OurService>()); }
优势:实现简单,客户依然可以MockIOurService
劣势:客户仍可通过后续注册替换IOurService,但包内逻辑若直接依赖OurService则不受影响
总结
优先选择方案一,它在满足客户Mock需求的同时,从架构层面彻底隔离了核心服务与可修改的对外接口,完全避免了客户替换实现带来的支持风险。
内容的提问来源于stack exchange,提问作者stormtrooper
相关产品推荐
相关产品推荐

