依赖注入 DI综合案例
需求说明
先建一个ConsoleMailSend总项目,然后建一个类库项目
声明一个接口:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace LogServices
{public interface IlogProvider{public void LogError(string msg);public void LogInfo(string msg);}
}
实现类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace LogServices
{public class ConsoleLogProvider:IlogProvider{public void LogInfo(string msg){Console.WriteLine($"ERROR:{msg}"); }public void LogError(string msg){Console.WriteLine($"Info:{msg}");}}
}
然后在新建邮件的一个类库:
声明一个接口:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace MailService
{public interface IMailService{public void Send(string title, string to, string body);}
}
实现类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConfigService;
using LogServices;namespace MailService
{public class MailServices : IMailService{private readonly IlogProvider log;private readonly IConfigServices config;public MailServices(IlogProvider log, IConfigServices config){this.log = log;this.config = config;}public void Send(string title, string to, string body){log.LogInfo("准备发邮件");string smtpServer = this.config.GetValue("SmtpServer");string userName = this.config.GetValue("UserName");string password = this.config.GetValue("password");Console.WriteLine($"真发邮件啦!{title},{to}");log.LogInfo("邮件准备发送完毕");}}
}
新建配置服务类库+接口+实现类:
namespace ConfigService
{public interface IConfigServices{public string GetValue(string name);}
}
从环境变量读取:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace ConfigService
{public class EnvVarConfigService : IConfigServices{public string GetValue(string name){return Environment.GetEnvironmentVariable(name);}}
}
最后MailSercices添加配置类库的引用:
控制台项目,添加邮件服务的引用:
接着:
最后写控制台:
安装包:
using ConfigService;
using LogServices;
using MailService;
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleMailSend
{internal class Program{static void Main(string[] args){ServiceCollection services = new ServiceCollection();services.AddScoped<IConfigServices, EnvVarConfigService>();services.AddScoped<IlogProvider, ConsoleLogProvider>();services.AddScoped<IMailService, MailServices>();using (var sp = services.BuildServiceProvider()){var mailService = sp.GetService<IMailService>();mailService.Send("Hello ", "trump@usa.gov", "懂王你好");}Console.Read();}}
}
新建文件配置读取实现类:
改从文件mail.ini读取配置
SmtpServer=abc.mail.com
UserName=admin
Password = hahaha
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace ConfigService
{public class IniFileConfigService : IConfigServices{public string FilePath { get; set; }public string GetValue(string name){var kv = File.ReadAllLines(FilePath).Select(s => s.Split("=")).Select(strs => new { Name = strs[0], Value = strs[1] }).SingleOrDefault(kv => kv.Name == name);if (kv != null)return kv.Value;elsereturn null;}}
}
deepseek:
1. 读取文件所有行
csharp
复制
File.ReadAllLines(FilePath)
行为:读取指定路径的文件,返回一个字符串数组,每个元素对应文件中的一行。示例输入:若文件内容为:复制
Server=localhost
Port=8080
2. 按等号分割每一行
csharp
复制
.Select(s => s.Split("="))
行为:将每一行按等号 = 拆分为子字符串数组。示例转换:行 "Server=localhost" → 数组 ["Server", "localhost"]行 "Port=8080" → 数组 ["Port", "8080"]3. 转换为键值对象
csharp
复制
.Select(strs => new { Name = strs[0], Value = strs[1] })
行为:将分割后的数组转换为匿名对象,包含 Name(键)和 Value(值)。示例转换:数组 ["Server", "localhost"] → 对象 { Name = "Server", Value = "localhost" }数组 ["Port", "8080"] → 对象 { Name = "Port", Value = "8080" }4. 按名称查找匹配项
csharp
复制
.SingleOrDefault(kv => kv.Name == name)
行为:查找 Name 属性等于参数 name 的第一个对象。找到 → 返回该对象。未找到 → 返回 null。风险:若存在多个同名键,会抛出 InvalidOperationException(因 SingleOrDefault 要求最多一个匹配项)。
using ConfigService;
using LogServices;
using MailService;
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleMailSend
{internal class Program{static void Main(string[] args){ServiceCollection services = new ServiceCollection();//services.AddScoped<IConfigServices, EnvVarConfigService>();services.AddScoped(typeof(IConfigServices), s => new IniFileConfigService { FilePath = "mail.ini" });services.AddScoped<IlogProvider, ConsoleLogProvider>();services.AddScoped<IMailService, MailServices>();using (var sp = services.BuildServiceProvider()){var mailService = sp.GetService<IMailService>();mailService.Send("Hello ", "trump@usa.gov", "懂王你好");}Console.Read();}}
}
deepseek:
在 ASP.NET Core 的依赖注入容器中,AddScoped 方法的参数设计决定了这种写法不可行。以下是详细解释:1. AddScoped 方法的重载分析
ASP.NET Core 的 IServiceCollection 中 AddScoped 有以下关键重载:csharp
复制
// 重载 1:注册实现类型
services.AddScoped(typeof(IConfigServices), typeof(IniFileConfigService));// 重载 2:注册工厂方法
services.AddScoped(typeof(IConfigServices), s => new IniFileConfigService { FilePath = "mail.ini"
});
没有直接接受实例的重载(如 AddScoped(Type, object))。若强行传递实例,编译器会报错。2. 为何不能直接传递实例?
依赖注入容器的核心职责是 管理对象的生命周期。直接传递实例(如 new IniFileConfigService)会破坏这一机制:生命周期失控:如果直接传递实例,容器无法控制对象的创建和释放,相当于强制将对象设为单例(Singleton),无论注册为 Scoped 还是 Transient。依赖隔离失效:Scoped 生命周期要求每个作用域内有独立实例,但直接传递实例会共享同一对象,导致状态污染。3. 正确场景对比
场景 1:注册工厂方法(正确)
csharp
复制
services.AddScoped(typeof(IConfigServices), s => new IniFileConfigService { FilePath = "mail.ini"
});
行为:每次解析 IConfigServices 时,容器会调用工厂方法创建一个新实例。生命周期:符合 Scoped 语义(同一作用域内复用实例,不同作用域隔离)。场景 2:直接传递实例(错误)
csharp
复制
// 编译错误:不存在匹配的重载方法
services.AddScoped(typeof(IConfigServices), new IniFileConfigService { FilePath = "mail.ini"
});
问题:容器无法管理预创建实例的生命周期,违背 Scoped 设计原则。4. 特殊例外:单例注册
如果希望强制将实例设为单例,可以使用 AddSingleton 的一个重载:csharp
复制
// 允许直接传递实例(单例)
services.AddSingleton<IConfigServices>(new IniFileConfigService { FilePath = "mail.ini"
});
原理:单例的生命周期与应用程序一致,容器只需保存引用,无需控制创建过程。5. 为什么 AddSingleton 允许直接传递实例?
单例的特殊性:单例对象在应用程序启动时创建,且全局唯一,无需依赖容器管理其生命周期。Scoped/Transient 的局限性:这两种生命周期需要容器动态控制实例的创建和释放,因此必须通过类型或工厂方法注册。6. 终极解决方案
若希望保持 IniFileConfigService 的配置灵活性,可通过以下方式优化:方案 1:工厂方法注册(推荐)
csharp
复制
services.AddScoped<IConfigServices>(s => new IniFileConfigService { FilePath = "mail.ini"
});
方案 2:强类型配置绑定
csharp
复制
// 在 appsettings.json 中配置
{"IniConfig": {"FilePath": "mail.ini"}
}// 注册配置类
services.Configure<IniConfig>(Configuration.GetSection("IniConfig"));// 修改服务实现
public class IniFileConfigService : IConfigServices
{private readonly IniConfig _config;public IniFileConfigService(IOptions<IniConfig> config){_config = config.Value;}// ...
}
总结
核心问题:AddScoped 没有直接接受实例的重载,因其生命周期管理需要动态控制对象创建。正确实践:通过工厂方法或配置绑定实现灵活注册。单例例外:仅 AddSingleton 允许直接传递实例,因其生命周期与容器一致
给日志服务添加扩展方法:
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LogServices;
using Microsoft.Extensions.DependencyInjection;namespace Microsoft.Extensions.DependencyInjection;public static class ConsolelogExtensions
{public static void AddConsoleLog(this IServiceCollection service){service.AddScoped<IlogProvider, ConsoleLogProvider>(); }}
这时候也可以去掉public了:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace LogServices
{class ConsoleLogProvider:IlogProvider{public void LogInfo(string msg){Console.WriteLine($"ERROR:{msg}"); }public void LogError(string msg){Console.WriteLine($"Info:{msg}");}}
}
using ConfigService;
using LogServices;
using MailService;
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleMailSend
{internal class Program{static void Main(string[] args){ServiceCollection services = new ServiceCollection();//services.AddScoped<IConfigServices, EnvVarConfigService>();services.AddScoped(typeof(IConfigServices), s => new IniFileConfigService { FilePath = "mail.ini" });//services.AddScoped<IlogProvider, ConsoleLogProvider>();services.AddConsoleLog();services.AddScoped<IMailService, MailServices>();using (var sp = services.BuildServiceProvider()){var mailService = sp.GetService<IMailService>();mailService.Send("Hello ", "trump@usa.gov", "懂王你好");}Console.Read();}}
}
给文件配置添加扩展方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConfigService;
using Microsoft.Extensions.DependencyInjection;namespace Microsoft.Extensions.DependencyInjection;public static class IniFileConfigExtensions
{ public static void AddIniFileConfig(this IServiceCollection service,string filepath){service.AddScoped(typeof(IConfigServices), s => new IniFileConfigService { FilePath = filepath });}}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace ConfigService
{public class IniFileConfigService : IConfigServices{public string FilePath { get; set; }public string GetValue(string name){var kv = File.ReadAllLines(FilePath).Select(s => s.Split("=")).Select(strs => new { Name = strs[0], Value = strs[1] }).SingleOrDefault(kv => kv.Name == name);if (kv != null)return kv.Value;elsereturn null;}}
}
其中有一台服务器临时需要改,其他保持不变,引用可覆盖的配置。
新建接口:
实现类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace ConfigService
{public class LayeredConfigReader : IConfigReader{private readonly IEnumerable<IConfigServices> services;public LayeredConfigReader(IEnumerable<IConfigServices> services){this.services = services;}public string GetValue(string name){string value = null;foreach (var service in services) { string newVlaue = service.GetValue(name);if (newVlaue != null) { value = newVlaue;}}return value;}}
}
为了方便别人使用,添加扩展方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConfigService;
using Microsoft.Extensions.DependencyInjection;namespace Microsoft.Extensions.DependencyInjection;
public static class LayeredConfigEXtensions
{public static void AddLayeredConfig(this IServiceCollection service){service.AddScoped<IConfigReader, LayeredConfigReader>();}}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ConfigService;
using LogServices;namespace MailService
{public class MailServices : IMailService{private readonly IlogProvider log;//private readonly IConfigServices config;private readonly IConfigReader config;public MailServices(IlogProvider log, IConfigReader config){this.log = log;this.config = config;}public void Send(string title, string to, string body){log.LogInfo("准备发邮件");string smtpServer = this.config.GetValue("SmtpServer");string userName = this.config.GetValue("UserName");string Password = this.config.GetValue("Password");Console.WriteLine(smtpServer);Console.WriteLine(userName);Console.WriteLine(Password);Console.WriteLine($"真发邮件啦!{title},{to}");log.LogInfo("邮件准备发送完毕");}}
}
报错:
using ConfigService;
using LogServices;
using MailService;
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleMailSend
{internal class Program{static void Main(string[] args){ServiceCollection services = new ServiceCollection();services.AddScoped<IConfigServices, EnvVarConfigService>();//services.AddScoped(typeof(IConfigServices), s => new IniFileConfigService { FilePath = "mail.ini" });services.AddIniFileConfig("mail.ini");//services.AddScoped<IlogProvider, ConsoleLogProvider>();services.AddConsoleLog();services.AddLayeredConfig();services.AddScoped<IMailService, MailServices>();using (var sp = services.BuildServiceProvider()){var mailService = sp.GetService<IMailService>();mailService.Send("Hello ", "trump@usa.gov", "懂王你好");}Console.Read();}}
}