
第一步:先把自定义服务的“骨架”搭对——接口+实现类
很多新手一开始会跳过“接口”,直接写一个XXService实现功能,比如直接写AliyunSmsService
发送短信。但去年帮朋友做电商项目时,他就踩了这个坑:他把支付逻辑直接写在WeChatPaymentService
里,后来要切换到支付宝支付,改代码改了整整三天——因为所有调用支付的地方都直接引用了WeChatPaymentService
,每处都要替换成AlipayPaymentService
。那三天他跟我说:“早知道写个接口就好了。”
接口的作用其实就一个:解耦——把“要做什么”和“具体怎么做”分开。比如发送短信的服务,接口ISmsService
只定义“发送短信”这个行为(SendSmsAsync
方法),而AliyunSmsService
、TencentSmsService
是具体的实现。以后要换短信服务商,只需要新增一个实现类,再改下注册的地方,其他调用ISmsService
的代码完全不用动。
给你举个具体的“搭骨架”例子,比如做一个发送短信的服务:
public interface ISmsService
{
// 发送短信的方法,参数是手机号和内容,返回是否成功
Task SendSmsAsync(string phoneNumber, string content);
}
public class AliyunSmsService ISmsService
{
// 从配置文件读取阿里云的AccessKey(后面讲怎么注入配置)
private readonly string _accessKey;
private readonly string _accessSecret;
// 通过构造函数注入IConfiguration(.NET Core自带的配置服务)
public AliyunSmsService(IConfiguration configuration)
{
_accessKey = configuration["Aliyun:Sms:AccessKey"];
_accessSecret = configuration["Aliyun:Sms:AccessSecret"];
}
public async Task SendSmsAsync(string phoneNumber, string content)
{
// 初始化阿里云短信客户端
var client = new DefaultAcsClient(
new Credential(_accessKey, _accessSecret),
"cn-hangzhou"
);
// 构造发送短信的请求(阿里云SDK的标准写法)
var request = new SendSmsRequest
{
PhoneNumbers = phoneNumber,
SignName = "你的短信签名(要在阿里云备案)",
TemplateCode = "你的短信模板ID",
TemplateParam = JsonConvert.SerializeObject(new { content }) // 模板参数
};
// 调用API发送短信
var response = await client.GetAcsResponseAsync(request);
// 返回是否成功(阿里云返回Code为"OK"表示成功)
return response.Code.Equals("OK", StringComparison.OrdinalIgnoreCase);
}
}
这里要避的坑:别把接口写得太“胖”——比如不要在ISmsService
里加SendEmail
(发邮件)、Log
(日志)的方法。去年有个网友给我看他的代码,IService
接口里塞了5个不同功能的方法,结果要拆分服务时,改得头都大了。接口一定要“单一职责”:一个接口只负责一件事。
第二步:把服务“装”进.NET Core的“容器”里——注册的3种方式
写好接口和实现类后,得把服务“放”进.NET Core的依赖注入容器(可以理解成一个“服务仓库”)里,不然程序找不到你的服务。注册服务就一句话,但关键是选对生命周期——这是新手最常踩的坑。
先搞懂3种生命周期的区别
.NET Core提供了3种服务生命周期,每种对应不同的场景,选错了会出大问题:
生命周期类型 | 实例创建规则 | 适用场景 | 踩坑案例 |
---|---|---|---|
Singleton(单例) | 整个应用一生只创建1个实例,所有请求共享 | 无状态、全局通用的服务(比如日志、配置) | 把日志服务注册成Transient,导致内存暴涨(每请求造1个实例) |
Scoped(作用域) | 每个HTTP请求创建1个实例,请求结束后销毁 | 与请求相关的有状态服务(比如用户会话、购物车) | 把购物车服务注册成Singleton,导致不同用户的购物车数据串了 |
Transient(瞬时) | 每次请求服务时都创建新实例 | 无状态、轻量级的服务(比如短信、邮件发送) | 把短信服务注册成Singleton,导致多次发送短信的参数串了 |
注册服务的具体操作
在.NET 6+项目里,注册服务是在Program.cs
里的builder.Services
对象上操作(.NET 5及以下是在Startup.cs
的ConfigureServices
方法里)。比如注册ISmsService
和AliyunSmsService
:
builder.Services.AddTransient();
用Scoped生命周期(比如购物车服务):
csharp
builder.Services.AddScoped();
用Singleton生命周期(比如日志服务):
csharp
builder.Services.AddSingleton();
最容易踩的坑:注册时漏写“接口”——比如写成
AddTransient(),结果在控制器里注入
ISmsService时,会报“无法解析服务类型”的错。去年帮朋友调过这个问题,他说“我明明注册了啊”,结果一看,注册的是具体类,不是接口——容器里没有
ISmsService的映射关系,自然找不到。
new第三步:用对方法“拿”服务——依赖注入的正确姿势
服务注册好后,接下来要“拿”出来用。.NET Core推荐的方式是依赖注入(DI)——不用你自己
实例,容器会自动把服务“递”到你手里。
最推荐的方式:构造函数注入 构造函数注入是.NET Core的“官方首选”,因为简单、安全,而且容器会自动处理所有依赖。比如控制器里要发送短信:
csharp
[ApiController]
[Route(“api/[controller]”)]
public class SmsController ControllerBase
{
// 声明一个ISmsService的字段,用来存注入的服务
private readonly ISmsService _smsService;
// 构造函数注入:容器会自动把ISmsService的实例传进来
public SmsController(ISmsService smsService)
{
_smsService = smsService;
}
[HttpPost(“send”)]
public async Task SendSms(string phone, string content)
{
// 直接调用服务的方法,不用自己new
var result = await _smsService.SendSmsAsync(phone, content);
if (result)
{
return Ok(“短信发送成功”);
}
return BadRequest(“短信发送失败,请检查参数”);
}
}
###
OrderService服务类之间的依赖:同样用构造函数注入 如果你的
需要用
ISmsService发送订单通知,直接在
OrderService的构造函数里加
ISmsService参数就行:
csharp
public class OrderService
{
private readonly ISmsService _smsService;
public OrderService(ISmsService smsService)
{
_smsService = smsService;
}
public async Task CreateOrderAsync(Order order)
{
//
//
await _smsService.SendSmsAsync(
order.UserPhone,
$”您的订单{order.Id}已创建,预计3天内送达”
);
}
}
###
IServiceProvider特殊场景:从IServiceProvider手动获取服务 有些场景下没法用构造函数注入——比如后台任务、中间件里,这时候可以用
(服务容器的入口)手动拿服务。比如一个后台定时发送短信的任务:
csharp
public class SmsBackgroundService BackgroundService
{
private readonly IServiceProvider _serviceProvider;
public SmsBackgroundService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// 创建一个作用域:避免Singleton服务持有Scoped服务(会导致内存泄漏)
using (var scope = _serviceProvider.CreateScope())
{
// 从作用域里拿ISmsService
var smsService = scope.ServiceProvider.GetRequiredService();
// 发送测试短信(实际场景可以从数据库读待发送的短信)
await smsService.SendSmsAsync(“138XXXX1234”, “这是一条后台任务发送的短信”);
}
// 每隔10分钟执行一次
await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);
}
}
}
这里要避的坑:别在Singleton服务里注入Scoped服务。比如把
UserService注册成Singleton,里面注入了
Scoped的
DbContext,结果不同用户的请求用的是同一个
DbContext,导致用户数据串了——去年帮朋友调过这个错,改成Scoped后直接解决。
这些步骤你学会了吗?其实自定义服务实现真的不难,关键是把“接口+实现类”的骨架搭对、选对生命周期、用对注入方式。我去年帮朋友做项目时,按照这三步做,从来没再出过错。你要是照着做遇到问题,欢迎在评论区告诉我,我帮你调。
自定义服务为什么一定要写接口?直接写实现类不行吗?
当然不是不行,但直接写实现类会踩“解耦”的大坑。就像我去年帮朋友做电商项目时,他把支付逻辑直接写在WeChatPaymentService里,后来要切换到支付宝支付,所有调用支付的地方都得手动替换成AlipayPaymentService,改了整整三天。接口的核心作用就是把“要做什么”和“具体怎么做”分开——比如ISmsService定义“发送短信”的行为,AliyunSmsService是具体实现,以后换服务商只需要加新的实现类、改下注册代码,其他调用ISmsService的地方完全不用动。直接写实现类的话,后期扩展或修改的成本会高到让你崩溃。
三种服务生命周期(Singleton/Scoped/Transient)怎么选?选错了会有什么问题?
得结合服务的“状态”和“使用场景”来选:Singleton适合无状态、全局通用的服务(比如日志、配置),整个应用只创建一个实例,选错了比如把购物车服务注册成Singleton,会导致不同用户的购物车数据串了;Scoped对应每个HTTP请求一个实例,适合和请求相关的有状态服务(比如用户会话、购物车),选错了比如把DbContext注册成Transient,会频繁创建销毁实例,影响性能;Transient是每次请求服务时都新建实例,适合轻量级、无状态的服务(比如短信、邮件发送),选错了比如把短信服务注册成Singleton,会导致多次发送的参数串了。我去年帮三个朋友调过这类问题, 下来“看状态、看场景”是关键。
注册服务时漏写接口了怎么办?比如只写了AddTransient()没加ISmsService
这会导致.NET Core的依赖注入容器“找不到”ISmsService的映射关系——容器里只存了AliyunSmsService的实例,但你在控制器里要注入ISmsService时,容器不知道“ISmsService对应的是AliyunSmsService”,就会报“无法解析服务类型”的错。解决办法很简单:把注册代码改成AddTransient()(根据你需要的生命周期选AddSingleton/AddScoped),这样容器就会建立“ISmsService→AliyunSmsService”的映射,注入的时候就能正确找到实例了。
构造函数注入和从IServiceProvider手动拿服务,哪种更推荐?
官方最推荐的是“构造函数注入”,因为它简单、安全,而且容器会自动处理所有依赖——你不用自己new实例,也不用管服务的生命周期。比如控制器里要用到ISmsService,直接在构造函数里加参数,容器会自动把实例传进来。而从IServiceProvider手动拿服务(比如后台任务里用scope.ServiceProvider.GetRequiredService()),只适合“特殊场景”:比如后台定时任务、中间件这类没法用构造函数注入的情况。但要注意,手动拿的时候一定要创建“作用域”(用CreateScope()),不然容易导致内存泄漏或者状态串了。
为什么Singleton服务里不能注入Scoped服务?会有什么后果?
因为Singleton服务是“整个应用一生只创建一个实例”,而Scoped服务是“每个HTTP请求创建一个实例”。如果在Singleton里注入Scoped服务(比如在Singleton的LogService里注入Scoped的DbContext),那么这个DbContext实例会跟着LogService一起“活”到应用结束——比如用户A的请求创建了DbContext,用户B的请求还在用同一个,就会导致用户数据串了(比如B能看到A的日志),或者内存越用越多(因为DbContext没被及时销毁)。我去年帮朋友调过这个问题,他把UserService注册成Singleton,里面注入了Scoped的DbContext,结果用户数据串了,改成Scoped后直接解决。所以一定要记住:Singleton里只能注入Singleton服务,不能碰Scoped或Transient。