You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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仅作为门面代理,从架构上隔离客户可修改的部分与包内核心逻辑:

  1. 将核心服务OurService改为internal,禁止客户直接访问
  2. 创建公共门面类实现IOurService,内部依赖核心服务
  3. 注册时同时注册内部核心服务和门面类,包内逻辑直接依赖内部核心服务而非公共接口

代码示例:

// 保留公共接口,供客户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>();
}

优势:

  • 客户依然可以MockIOurService测试自身代码
  • 包内核心逻辑依赖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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:37:13