ASP.NET Core 7模块化单体中按模块注册IBlobStorage的依赖注入问题
模块化单体ASP.NET Core 7 API的依赖注册问题
问题背景
我在基于ASP.NET Core 7的模块化单体API中遇到了依赖注册的核心难题:
- 所有模块都依赖Blob存储服务,因此在公共工具项目中定义了统一接口:
public interface IBlobStorage { void Save(string name, byte[] content); Task<byte[]> Read(string name); void Remove(string name); }
- 该接口有多类实现(如Azure存储、谷歌云存储),每个实现的构造函数需要对应配置(如带连接字符串的
IOptions)。 - 每个模块独立负责自身依赖注册,目标是未来能以最小改动拆分模块为独立部署单元,且模块无需感知当前运行在单体环境中。
- 关键需求:每个模块需使用独立配置的Blob存储实例,但目前用内置DI会出现后注册模块覆盖前一个的问题(比如示例中ModuleB的服务覆盖了ModuleA,导致两个接口都返回ModuleB的数据)。
我考虑过用Autofac的键控注册,但觉得工作量较大,想寻找更轻量化的方案。
解决方案
方案1:工厂模式+模块标识(无需替换内置DI)
这是适配ASP.NET Core 7的轻量化方案,无需引入第三方容器:
- 定义模块标识特性:给每个模块的控制器添加唯一标识特性,用于区分模块:
[AttributeUsage(AttributeTargets.Class)] public class ModuleAttribute : Attribute { public string ModuleName { get; } public ModuleAttribute(string moduleName) => ModuleName = moduleName; } // ModuleA控制器示例 [Module("ModuleA")] [ApiController] [Route("A")] public class ModuleAController : ControllerBase { ... }
- 实现Blob存储工厂:在公共项目中定义工厂,负责根据模块标识返回对应配置的实例:
public interface IBlobStorageFactory { IBlobStorage GetForModule(string moduleName); } public class BlobStorageFactory : IBlobStorageFactory { private readonly IServiceProvider _serviceProvider; private readonly IConfiguration _configuration; public BlobStorageFactory(IServiceProvider serviceProvider, IConfiguration configuration) { _serviceProvider = serviceProvider; _configuration = configuration; } public IBlobStorage GetForModule(string moduleName) { // 读取模块专属Blob配置 var config = _configuration.GetSection($"BlobStorage:{moduleName}").Get<BlobStorageConfig>(); // 根据配置类型创建对应实现 return config.Type switch { "Azure" => ActivatorUtilities.CreateInstance<AzureBlobStorage>(_serviceProvider, Options.Create(config)), "Google" => ActivatorUtilities.CreateInstance<GoogleCloudBlobStorage>(_serviceProvider, Options.Create(config)), _ => throw new NotSupportedException($"不支持的存储类型: {config.Type}") }; } }
- 模块注册简化:每个模块只需注册自身业务服务,无需注册
IBlobStorage,依赖通过工厂获取:
// ModuleA注册类 public static class ModuleARegistration { public static IServiceCollection AddModuleA(this IServiceCollection services, IConfiguration config) { services.AddScoped<ModuleAService>(); return services; } }
- 控制器中使用工厂:通过特性获取模块标识,再从工厂拿到对应实例:
[Module("ModuleA")] [ApiController] [Route("A")] public class ModuleAController : ControllerBase { private readonly IBlobStorage _blobStorage; public ModuleAController(IBlobStorageFactory factory) { var moduleName = this.GetType().GetCustomAttribute<ModuleAttribute>()?.ModuleName ?? throw new InvalidOperationException("未找到模块标识特性"); _blobStorage = factory.GetForModule(moduleName); } // 控制器业务方法... }
方案2:模块专属接口(最简单但略冗余)
如果模块数量不多,可以为每个模块定义专属的Blob存储接口,继承自IBlobStorage:
// ModuleA项目中 public interface IModuleABlobStorage : IBlobStorage { } // 注册时绑定模块专属配置 services.AddScoped<IModuleABlobStorage>(sp => { var config = sp.GetRequiredService<IOptions<ModuleABlobStorageConfig>>().Value; return new AzureBlobStorage(config); });
模块控制器直接注入IModuleABlobStorage,完全避免服务冲突,缺点是需要每个模块定义专属接口,适合模块数量较少的场景。
方案3:自定义命名服务包装(模拟键控注册)
内置DI无原生命名服务,但可以通过包装类实现类似效果:
// 定义命名服务包装类 public class NamedBlobStorage { public string ModuleName { get; } public IBlobStorage Instance { get; } public NamedBlobStorage(string moduleName, IBlobStorage instance) { ModuleName = moduleName; Instance = instance; } } // ModuleA注册命名实例 services.AddScoped<NamedBlobStorage>(sp => { var config = sp.GetRequiredService<IOptions<ModuleABlobConfig>>().Value; return new NamedBlobStorage("ModuleA", new AzureBlobStorage(config)); }); // ModuleB注册命名实例 services.AddScoped<NamedBlobStorage>(sp => { var config = sp.GetRequiredService<IOptions<ModuleBBlobConfig>>().Value; return new NamedBlobStorage("ModuleB", new GoogleCloudBlobStorage(config)); }); // 实现解析器 public interface IBlobStorageResolver { IBlobStorage Resolve(string moduleName); } public class BlobStorageResolver : IBlobStorageResolver { private readonly IEnumerable<NamedBlobStorage> _namedStorages; public BlobStorageResolver(IEnumerable<NamedBlobStorage> namedStorages) { _namedStorages = namedStorages; } public IBlobStorage Resolve(string moduleName) { return _namedStorages.First(s => s.ModuleName == moduleName).Instance; } } // 注册解析器 services.AddScoped<IBlobStorageResolver, BlobStorageResolver>();
控制器通过解析器+模块标识获取对应实例,逻辑和方案1类似,但提前注册了所有模块的Blob实例。
示例问题快速修复
你的示例中IModuleSpecificService被覆盖,用方案2最直接:给每个模块定义专属接口(如IModuleAService、IModuleBService),各自继承IModuleSpecificService,模块分别注册自己的专属接口,控制器注入对应接口即可避免覆盖。
内容的提问来源于stack exchange,提问作者timnot90
相关产品推荐
相关产品推荐

