02020213 .NET Core重难点知识13-配置&日志&邮件服务案例、DI读取、DI与扩展方法、VS配置项目环境变量
1. 配置服务、日志服务、邮件发送器服务案例
1.1 需求
- 有配置服务、日志服务,然后再开发一个邮件发送服务器服务。
- 可以通过配置服务从文件、环境变量、数据库等地方读取配置。
- 可以通过日志服务来讲程序运行过程中的日志信息写入文件、控制台、数据库等。
- 本案例开发了自己的日志、配置等接口,这只是在揭示原理,.NET有现成的,后面讲。
1.2 实现

- (截图更正)创建三个.NET Core类库项目和一个控制台项目。
- ConfigServices → 配置服务的项目。
- LogServices → 日志服务的项目。
- MailServices → 邮件发送器的项目。
- MailServicesConsole → 是.NET Core控制台项目。
1.2 项目目录结构
ConsoleAppMailSender → 解决方案名称
├── ConsoleAppMailSender → .NET Core 5.0控制台项目。因为要发邮件,依赖项需要添加MailServices项目的引用。
| ├── Program.cs → 包含Main方法,程序的入口。
├── LogServices → .NET Standard 2.1类库项目
| ├── ILogProvider.cs → 接口
| └── ConsoleLogProvider.cs => 实现类
├── MailServices → .NET Standard 2.1类库项目。因为要写日志和读取配置,依赖项需要添加LogServices项目和ConfigServices项目的引用。
| ├── IMailService.cs → 接口
| └── MailServiceImpl → 实现类
├── ConfigServices → .NET Standard 2.1类库项目。
| ├── IConfigService.cs → 接口
└── └── EnvVarConfigService → 实现类说明:
1. 创建.NET Standard类库是为了便于.NET Core或者.NET Framework项目都可以使用。
2. 该项目为演示作用,并没有真实发送邮件。如果需要发送邮件,可以用MailKit来实现,这里面有提供发送邮件的方法。
3. NuGet包版本为:Install-Package Microsoft.Extensions.DependencyInjection 9.0.8。
1.3 IDE项目资源

1.4 源码
// EnvVarConfigService.cs
using System;namespace ConfigServices
{public class EnvVarConfigService : IConfigService{public string GetValue(string name){return Environment.GetEnvironmentVariable(name);}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// IConfigService.cs
namespace ConfigServices
{/// <summary>/// 如果配置找不到,就返回null/// </summary>/// <param name="name"></param>/// <returns></returns>public interface IConfigService{public string GetValue(string name); // 读取名字为name的配置文件}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Program.cs
using System;
using ConfigServices;
using LogServices;
using MailServices;
using Microsoft.Extensions.DependencyInjection;namespace ConsoleAppMailSender
{class Program{static void Main(string[] args){ServiceCollection services = new ServiceCollection();services.AddScoped<IConfigService, EnvVarConfigService>();services.AddScoped<IMailService, MailServiceImpl>();services.AddScoped<ILogProvider, ConsoleLogProvider>();using( var sp = services.BuildServiceProvider()){// 第一个根上的对象只能用ServiceLocatorvar mailService = sp.GetRequiredService<IMailService>();mailService.Send("Hello", "Trump", "懂王你好");}Console.ReadLine();}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// ConsoleLogProvider.cs
using System;namespace LogServices
{public class ConsoleLogProvider : ILogProvider{public void LogError(string msg){Console.WriteLine($"Error: {msg}");}public void LogInfo(string msg){Console.WriteLine($"Info: {msg}");}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// ILogProvider.cs
namespace LogServices
{public interface ILogProvider{public void LogError(string msg); // 记录错误信息public void LogInfo(string msg); // 记录普通信息}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// IMailService.cs
using System;
using System.Collections.Generic;
using System.Text;namespace MailServices
{public interface IMailService{public void Send(string title, string to, string name); // 发送邮件}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// MailServiceImpl.cs
using ConfigServices;
using LogServices;
using System;namespace MailServices
{public class MailServiceImpl : IMailService{private readonly ILogProvider log;private readonly IConfigService config;public MailServiceImpl(ILogProvider log, IConfigService config){this.log = log;this.config = config;}public void Send(string title, string to, string name){this.log.LogInfo("准备发送邮件");string smtpServer = this.config.GetValue("SmtpServer");string userName = this.config.GetValue("UserName");string password = this.config.GetValue("Password");Console.WriteLine($"邮件服务器地址{smtpServer},{userName},{password}");Console.WriteLine($"发送成功:{title},{to}");this.log.LogInfo("邮件发送成功");}}
}控制台输出:
Info: 准备发送邮件
邮件服务器地址,Administrator,
发送成功:Hello,Trump
Info: 邮件发送成功
1.5 项目总结
- 本例其实和02020212中2.2中是一样的,只是多了项目这个分层。
- 注意各个项目之间的引用关系。
2. 从配置文件读取数据
2.1 项目目录结构
ConsoleAppMailSender → 解决方案名称
├── ConsoleAppMailSender → .NET Core 5.0控制台项目。因为要发邮件,依赖项需要添加MailServices项目的引用。
| ├── Program.cs → (※修改※)包含Main方法,程序的入口。
| └── mail.ini → (※新增※)配置文件。通过文本文件创建,名称后缀带上.ini。文件名 → 右键 → 属性 → 高级 → 复制到输出目录 → 如果较新则复制
├── LogServices → .NET Standard 2.1类库项目
| ├── ILogProvider.cs → 接口
| └── ConsoleLogProvider.cs => 实现类
├── MailServices → .NET Standard 2.1类库项目。因为要写日志和读取配置,依赖项需要添加LogServices项目和ConfigServices项目的引用。
| ├── IMailService.cs → 接口
| └── MailServiceImpl → 实现类
├── ConfigServices → .NET Standard 2.1类库项目。
| ├── IConfigService.cs → 接口
| ├── IniFileConfigService.cs → (※新增※)实现类
└── └── EnvVarConfigService → 实现类说明:
1. 创建.NET Standard类库是为了便于.NET Core或者.NET Framework项目都可以使用。
2. 该项目为演示作用,并没有真实发送邮件。如果需要发送邮件,可以用MailKit来实现,这里面有提供发送邮件的方法。
3. NuGet包版本为:Install-Package Microsoft.Extensions.DependencyInjection 9.0.8。
2.2 源码
- 在1.4基础上,源代码作如下修改,用于从配置文件获取数据。
// 新增配置文件:mail.ini
SmtpServer=abc.mail.com
UserName=admin
Password=123456
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 新增实现类:IniFileConfigService.cs
using System.IO;
using System.Linq;namespace ConfigServices
{public class IniFileConfigService : IConfigService{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;}else{return null;}}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 修改:Program.cs
using System;
using ConfigServices;
using LogServices;
using MailServices;
using Microsoft.Extensions.DependencyInjection;namespace ConsoleAppMailSender
{class Program{static void Main(string[] args){ServiceCollection services = new ServiceCollection();// AddScoped方法的重载形式,用到了回调的知识,用new的目的是为了给属性赋值。services.AddScoped(typeof(IConfigService), s => new IniFileConfigService { FilePath = "mail.ini" });services.AddScoped<IMailService, MailServiceImpl>(); // @1services.AddScoped<ILogProvider, ConsoleLogProvider>(); // @2using( var sp = services.BuildServiceProvider()){// 第一个根上的对象只能用ServiceLocatorvar mailService = sp.GetRequiredService<IMailService>();mailService.Send("Hello", "Trump", "懂王你好");}Console.ReadLine();}}
}控制台输出:
Info: 准备发送邮件
邮件服务器地址abc.mail.com,admin,123456
发送成功:Hello,Trump
Info: 邮件发送成功说明:
1. 在@1处和@2处,引入了如何让调用者不关注实现类名称(不显式指定实现类)的问题。
2. 在@1处如果有services.AddConsoleLog方法,即提供一个AddXX方法,能够自动.出来(自动提示出来),此时会更加简单。此时,可以用扩展方法来解决。
3. 使用扩展方法来简化案例
3.1 思路

- 给Microsoft.Extensions.DependencyInjection 9.0.8包里面IServiceCollection接口增加一个扩展方法。
- 并将这个扩展方法直接生成在Microsoft.Extensions.DependencyInjection这个命名空间底下。
3.2 项目目录结构
ConsoleAppMailSender → 解决方案名称
├── ConsoleAppMailSender → .NET Core 5.0控制台项目。因为要发邮件,依赖项需要添加MailServices项目的引用。
| ├── Program.cs →(※修改※) 包含Main方法,程序的入口。
| └── mail.ini → 配置文件。通过文本文件创建,名称后缀带上.ini。文件名 → 右键 → 属性 → 高级 → 复制到输出目录 → 如果较新则复制
├── LogServices → .NET Standard 2.1类库项目。(※新增包的引用※)
| ├── ILogProvider.cs → 接口
| ├── ConsoleLogExtensions.cs → (※新增※)实现类
| └── ConsoleLogProvider.cs → (※修改※)实现类。修改类的访问权限为private,此时调用者已经不关心类名了,在类的内部访问即可。
├── MailServices → .NET Standard 2.1类库项目。因为要写日志和读取配置,依赖项需要添加LogServices项目和ConfigServices项目的引用。
| ├── IMailService.cs → 接口
| └── MailServiceImpl → 实现类
├── ConfigServices → .NET Standard 2.1类库项目。
| ├── IConfigService.cs → 接口
| ├── IniFileConfigService.cs → 实现类
└── └── EnvVarConfigService → 实现类
3.3 源码
- 在2.2基础上,用用扩展方法来实现。
// 新增实现类:ConsoleLogExtensions
using LogServices;// 此时命名空间要将默认的LogServices改为Microsoft.Extensions.DependencyInjection
namespace Microsoft.Extensions.DependencyInjection
{public static class ConsoleLogExtensions // 扩展方法的类必须是静态的{// 此时LogServices项目也要添加Microsoft.Extensions.DependencyInjection 9.0.8包的引用。public static void AddConsoleLog(this IServiceCollection services) {services.AddScoped<ILogProvider, ConsoleLogProvider>();}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 修改:ConsoleLogProvider类的权限
using System;namespace LogServices
{class ConsoleLogProvider : ILogProvider // 此时可以将public取消,调用者已经不需要知道类的名称了。{public void LogError(string msg){Console.WriteLine($"Error: {msg}");}public void LogInfo(string msg){Console.WriteLine($"Info: {msg}");}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 修改:Program.cs
using System;
using ConfigServices;
using MailServices;
using Microsoft.Extensions.DependencyInjection; // 此命名空间里面包含ConsoleLogExtensions这个类namespace ConsoleAppMailSender
{class Program{static void Main(string[] args){ServiceCollection services = new ServiceCollection();// AddScoped方法的重载形式,用到了回调的知识,用new的目的是为了给属性赋值。services.AddScoped(typeof(IConfigService), s => new IniFileConfigService { FilePath = "mail.ini" });services.AddScoped<IMailService, MailServiceImpl>();services.AddConsoleLog(); // @1 扩展方法using( var sp = services.BuildServiceProvider()){// 第一个根上的对象只能用ServiceLocatorvar mailService = sp.GetRequiredService<IMailService>();mailService.Send("Hello", "Trump", "懂王你好");}Console.ReadLine();}}
}控制台输出:
Info: 准备发送邮件
邮件服务器地址abc.mail.com,admin,123456
发送成功:Hello,Trump
Info: 邮件发送成功说明:为了让@1处AddConsoleLog方法能直接.出来,用户不关心实现类和接口的名字。我们给IServiceCollection接口增加了一个AddConsoleLog方法,用来完成注册过程。
4. 用扩展方法继续简化
4.1 项目目录结构
ConsoleAppMailSender → 解决方案名称
├── ConsoleAppMailSender → .NET Core 5.0控制台项目。因为要发邮件,依赖项需要添加MailServices项目的引用。
| ├── Program.cs → 包含Main方法,程序的入口。(※修改※)
| └── mail.ini → 配置文件。通过文本文件创建,名称后缀带上.ini。文件名 → 右键 → 属性 → 高级 → 复制到输出目录 → 如果较新则复制
├── LogServices → .NET Standard 2.1类库项目。
| ├── ILogProvider.cs → 接口
| ├── ConsoleLogExtensions.cs → 实现类
| └── ConsoleLogProvider.cs → (※修改※)实现类。修改类的访问权限为private,此时调用者已经不关心类名了,在类的内部访问即可。
├── MailServices → .NET Standard 2.1类库项目。因为要写日志和读取配置,依赖项需要添加LogServices项目和ConfigServices项目的引用。
| ├── IMailService.cs → 接口
| └── MailServiceImpl → 实现类
├── ConfigServices → .NET Standard 2.1类库项目。(※新增包的引用※)
| ├── IConfigService.cs → 接口
| ├── IniFileConfigService.cs → 实现类
| ├── IniFileConfigExtensions → (※新增※)
└── └── EnvVarConfigService → 实现类。(※修改访问权限为private※),本例不修改,任然保持public权限用作对比。
4.2 源码
- 在3.3基础上,继续使用扩展方法。
// 新增实现类:IniFileConfigExtensions
using ConfigServices;namespace Microsoft.Extensions.DependencyInjection
{public static class IniFileConfigExtensions{public static void AddIniFileConfig(this IServiceCollection services, string filePath){services.AddScoped(typeof(IConfigService), s => new IniFileConfigService { FilePath = filePath });}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 修改:Program.cs
using System;
using MailServices;
using Microsoft.Extensions.DependencyInjection;namespace ConsoleAppMailSender
{class Program{static void Main(string[] args){ServiceCollection services = new ServiceCollection();services.AddIniFileConfig("mail.ini"); // 扩展方法services.AddScoped<IMailService, MailServiceImpl>();services.AddConsoleLog();using( var sp = services.BuildServiceProvider()){// 第一个根上的对象只能用ServiceLocatorvar mailService = sp.GetRequiredService<IMailService>();mailService.Send("Hello", "Trump", "懂王你好");}Console.ReadLine();}}
}控制台输出:
Info: 准备发送邮件
邮件服务器地址abc.mail.com,admin,123456
发送成功:Hello,Trump
Info: 邮件发送成功
5. 可覆盖的配置读取器

5.1 项目目录结构
ConsoleAppMailSender → 解决方案名称
├── ConsoleAppMailSender → .NET Core 5.0控制台项目。因为要发邮件,依赖项需要添加MailServices项目的引用。
| ├── Program.cs → 包含Main方法,程序的入口。(※修改※)
| └── mail.ini → 配置文件。通过文本文件创建,名称后缀带上.ini。文件名 → 右键 → 属性 → 高级 → 复制到输出目录 → 如果较新则复制
├── LogServices → .NET Standard 2.1类库项目。
| ├── ILogProvider.cs → 接口
| ├── ConsoleLogExtensions.cs → 实现类
| └── ConsoleLogProvider.cs → (※修改※)实现类。修改类的访问权限为private,此时调用者已经不关心类名了,在类的内部访问即可。
├── MailServices → .NET Standard 2.1类库项目。因为要写日志和读取配置,依赖项需要添加LogServices项目和ConfigServices项目的引用。
| ├── IMailService.cs → 接口
| └── MailServiceImpl.cs → 实现类。(※修改※)
├── ConfigServices → .NET Standard 2.1类库项目。
| ├── IConfigService.cs → 接口
| ├── IniFileConfigService.cs → 实现类
| ├── IniFileConfigExtensions.cs → 实现类
| ├── IConfigReader.cs → (※新增※)接口。通常放在一个新的类库里面,这里为了演示方便继续放在这个类库当中。
| ├── LayeredConfigReader.cs → (※新增※)实现类。通常放在一个新的类库里面,这里为了演示方便继续放在这个类库当中。
| ├── LayeredConfigExtension.cs → (※新增※)扩展方法类,便于使用。
└── └── EnvVarConfigService → 实现类。
5.2 源码
- 在4.2基础上,实现可覆盖读取器。
// 新增:IConfigReader.cs
using System;
using System.Collections.Generic;
using System.Text;namespace ConfigServices
{public interface IConfigReader{public string GetValue(string name);}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 新增:LayeredConfigReader.cs
using System;
using System.Collections.Generic;
using System.Text;namespace ConfigServices
{class LayeredConfigReader : IConfigReader{private readonly IEnumerable<IConfigService> services;public LayeredConfigReader(IEnumerable<IConfigService> services){this.services = services;}public string GetValue(string name){string value = null; // 如果配置找不到为nullforeach (var service in services) // 按照顺序读{string newValue = service.GetValue(name);if(newValue != null){value = newValue; // 最后一个不为null的值,就是最终值。}}return value;}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 新增:LayeredConfigExtension.cs
using ConfigServices;
using System;
using System.Collections.Generic;
using System.Text;namespace Microsoft.Extensions.DependencyInjection
{public static class LayeredConfigExtension{public static void AddLayeredConfig(this IServiceCollection services){services.AddScoped<IConfigReader, LayeredConfigReader>();}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 修改:MailServiceImpl.cs
using ConfigServices;
using LogServices;
using System;namespace MailServices
{public class MailServiceImpl : IMailService{private readonly ILogProvider log;private readonly IConfigReader config; // 现在逻辑不是简单的从某一个里面读public MailServiceImpl(ILogProvider log, IConfigReader config){this.log = log;this.config = config;}public void Send(string title, string to, string name){this.log.LogInfo("准备发送邮件");string smtpServer = this.config.GetValue("SmtpServer");string userName = this.config.GetValue("UserName");string password = this.config.GetValue("Password");Console.WriteLine($"邮件服务器地址{smtpServer},{userName},{password}");Console.WriteLine($"发送成功:{title},{to}");this.log.LogInfo("邮件发送成功");}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 修改:Program.cs
using System;
using ConfigServices;
using MailServices;
using Microsoft.Extensions.DependencyInjection;namespace ConsoleAppMailSender
{class Program{static void Main(string[] args){ServiceCollection services = new ServiceCollection();services.AddScoped<IConfigService, EnvVarConfigService>(); // 这里读取环境变量,可以自行修改。这里保留了课堂用法。services.AddIniFileConfig("mail.ini");services.AddLayeredConfig();services.AddScoped<IMailService, MailServiceImpl>();services.AddConsoleLog();using( var sp = services.BuildServiceProvider()){// 第一个根上的对象只能用ServiceLocatorvar mailService = sp.GetRequiredService<IMailService>();mailService.Send("Hello", "Trump", "懂王你好");}Console.ReadLine();}}
}控制台输出:
Info: 准备发送邮件
邮件服务器地址abc.mail.com,admin,123456
发送成功:Hello,Trump
Info: 邮件发送成功
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 修改配置文件内容
SmtpServer=abc.mail.com → 修改为 → SmtpServer2=abc.mail.com
UserName=admin
Password=123456控制台输出:
Info: 准备发送邮件
邮件服务器地址,admin,123456
发送成功:Hello,Trump
Info: 邮件发送成功
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
总结:配置文件有用配置文件的,否则用环境变量的。
6. 配置环境变量

- VS配置环境变量:项目名称 → 属性 → 调试 → 名称&值
// 做了如上配置之后,通过修改ini文件的SmtpServer名称,可以显式不同的输出结果。
控制台输出1:
Info: 准备发送邮件
邮件服务器地址qinway.QW.com,admin,123456
发送成功:Hello,Trump
Info: 邮件发送成功控制台输出2:
Info: 准备发送邮件
邮件服务器地址abc.mail.com,admin,123456
发送成功:Hello,Trump
Info: 邮件发送成功
7. 案例总结
- 关注于接口,而不是关注于实现,各个服务可以更弱耦合的协同工作。
- 编写代码的时候,我们甚至都不知道具体的服务是什么。
- 第三方DI容器:Autofac等。
- Autofac优点 → 支持属性注入、基于名字注入、基于约定注入等。
- 老师评价基于构造函数注入比基于属性注入更好,目前学习过程中体会不出来。
- 重点:如无必要,勿增实体。
- 不必要的情况下,不要把东西变得很复杂。
- 与其狂拽吊炸天,实际上满足项目前提下尽量不要扩展一些新东西,保持架构简洁。提高可维护性和稳定性。
8. 个人评价
- 目前一边看一边写,算是看完了也写完了,目前还是一脸懵逼,“如看”。老师讲的可以,个人理解不够,后续会再看一遍。课程继续往后推进,不纠结。
结尾
书籍:ASP.NET Core技术内幕与项目实战
视频:https://www.bilibili.com/video/BV1pK41137He
著:杨中科
ISBN:978-7-115-58657-5
版次:第1版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※