文档菜单
C# / .NET / SDK REFERENCE
ASP.NET Core SDK 指南
用于发送 Kakao 通知消息、好友消息、品牌消息与 SMS 的官方 ASP.NET Core 扩展包 —— 将客户端注册到依赖注入容器。
이 문서의 목차
- 패키지
- Sendgo.AspNetCore
- 언어
- C# / .NET
- 레지스트리
- NuGet
dotnet add package Sendgo.AspNetCore用于发送 Kakao 通知消息、品牌消息与短信的官方 ASP.NET Core 扩展包
Sendgo.AspNetCore 在 Sendgo.SDK 核心之上提供 AddSendgo 扩展方法,把 SendgoClient 注册为单例。
面向 .NET 8.0。
安装
dotnet add package Sendgo.AspNetCore
核心 Sendgo.SDK 会作为依赖一并安装。
注册客户端
从配置读取
// Program.cs
builder.Services.AddSendgo(builder.Configuration.GetSection("Sendgo"));
// appsettings.json
{
"Sendgo": {
"AccessKey": "your_access_key",
"SecretKey": "your_secret_key",
"KakaoSenderKey": "your_kakao_sender_key",
"SmsSenderKey": "your_sms_sender_key",
"ApiVersion": "v2"
}
}
不要把密钥提交到源码里的 appsettings.json:开发期用 user secrets,生产环境用环境变量或密钥保管服务:
dotnet user-secrets set "Sendgo:AccessKey" "your_access_key"
dotnet user-secrets set "Sendgo:SecretKey" "your_secret_key"
环境变量使用双下划线分隔:Sendgo__AccessKey。
使用 lambda
builder.Services.AddSendgo(options =>
{
options.AccessKey = builder.Configuration["Sendgo:AccessKey"]!;
options.SecretKey = builder.Configuration["Sendgo:SecretKey"]!;
options.KakaoSenderKey = builder.Configuration["Sendgo:KakaoSenderKey"];
options.ApiVersion = "v2";
});
两个重载注册的是同一个单例,因此令牌缓存在整个应用内共享。SendgoClient 实现了 IDisposable,DI 容器会在应用关闭时释放该单例 —— 不要用 using 包住它。
注入客户端
using Sendgo;
using Sendgo.Models;
public class OrderNotifier(SendgoClient sendgo, ILogger<OrderNotifier> logger)
{
public Task ConfirmedAsync(string phone, string orderNo, CancellationToken ct = default) =>
sendgo.SendAlimtalkAsync(new AlimtalkRequest
{
TemplateCode = "ORDER_CONFIRM_001",
Contacts = new[] { new Contact { PhoneNumber = phone, Var1 = orderNo } },
}, ct);
}
builder.Services.AddScoped<OrderNotifier>();
注意 Contact.PhoneNumber 在传输时序列化为 contact —— 属性名只是为了 C# 侧的可读性。
Minimal API 端点
app.MapPost("/notify", async (
NotifyRequest body,
SendgoClient sendgo,
CancellationToken ct) =>
{
await sendgo.SendAlimtalkAsync(new AlimtalkRequest
{
TemplateCode = "ORDER_CONFIRM_001",
Contacts = new[] { new Contact { PhoneNumber = body.Phone, Var1 = body.OrderNo } },
}, ct);
return Results.Accepted();
});
record NotifyRequest(string Phone, string OrderNo);
把端点自身的 CancellationToken 传下去,客户端中断请求时外发调用也会一并取消。
品牌消息
好友消息已于 2025-12-31 停止服务。 自 2026-01-01 起,Kakao 会将好友消息请求 自动改以品牌消息(自由形式)发出。
friendtalk仍可调用,且自由文本类型FT/FI/FW发往单个接收者的路径目前仍只有这一条 —— 基于模板的富文本类型 (FL/FC/FM/FP/FA)、非好友定向(N/I)与群发(F)请使用品牌消息。 品牌消息是好友消息的后继渠道:可触达非频道好友的接收者(Targeting = "N"),也可向已同意接收的全部频道好友群发(Targeting = "F")。消息类型与好友消息一一对应(FT→BT、FI→BI、FW→BW、FL→BL、FC→BC、FM→BM、FP→BP、FA→BA)—— 传入好友消息的代码,服务端会自动转换。
仅 v2 支持。请设置
ApiVersion = "v2"。
app.MapPost("/campaigns/targeted", async (
string phone, SendgoClient sendgo, CancellationToken ct) =>
{
var result = await sendgo.SendBrandMessageAsync(new BrandMessageRequest
{
Targeting = "M",
MessageType = "FL",
FriendTemplateUuid = "9cd5460b-6458-4edc-9b11-c26d3013c340",
Contacts = new[] { new Contact { PhoneNumber = phone, Var1 = "29,000元" } },
}, ct);
return Results.Ok(result);
});
app.MapPost("/campaigns/broadcast", async (SendgoClient sendgo, CancellationToken ct) =>
{
// 不传接收者列表 —— 由 Kakao 展开受众
var result = await sendgo.BroadcastBrandMessageAsync(new BrandMessageRequest
{
MessageType = "FW",
FriendTemplateUuid = "9cd5460b-6458-4edc-9b11-c26d3013c340",
}, ct);
return Results.Accepted(value: result);
}).RequireAuthorization("Admin");
app.MapGet("/campaigns", (SendgoClient sendgo, CancellationToken ct) =>
sendgo.GetBrandMessagesAsync(count: 10, ct: ct));
app.MapGet("/campaigns/{campaignId}", (string campaignId, SendgoClient sendgo, CancellationToken ct) =>
sendgo.GetBrandMessageAsync(campaignId, ct));
群发在上游为异步处理,因此发送响应只表示已被接受 —— 请轮询详情端点查看进度。群发可能触达整个好友列表,所以上面的示例给它加了 RequireAuthorization。
targeting 的取值含义:
| 值 | 含义 |
|---|---|
M |
频道好友中的指定接收者 |
N |
非频道好友的接收者 |
I |
按标识符指定的接收者 |
F |
已同意接收的全部频道好友(群发) |
后台发送
发送是一次外发 HTTP 调用,对调用方不必等待的场景请把它移出请求路径:
public class NotificationQueue
{
private readonly Channel<AlimtalkRequest> _channel =
Channel.CreateBounded<AlimtalkRequest>(1000);
public ValueTask EnqueueAsync(AlimtalkRequest request, CancellationToken ct) =>
_channel.Writer.WriteAsync(request, ct);
public IAsyncEnumerable<AlimtalkRequest> ReadAllAsync(CancellationToken ct) =>
_channel.Reader.ReadAllAsync(ct);
}
public class NotificationWorker(
NotificationQueue queue,
SendgoClient sendgo,
ILogger<NotificationWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var request in queue.ReadAllAsync(stoppingToken))
{
try
{
await sendgo.SendAlimtalkAsync(request, stoppingToken);
}
catch (SendgoException e) when (e.StatusCode < 500)
{
// 4xx 重试也不会成功 —— 丢弃并记录原因。
logger.LogError("Sendgo {Code}: {Message}", e.ErrorCode, e.Message);
}
catch (SendgoException e)
{
logger.LogWarning("Sendgo 临时故障 {Status},重新入队", e.StatusCode);
await queue.EnqueueAsync(request, stoppingToken);
}
}
}
}
builder.Services.AddSingleton<NotificationQueue>();
builder.Services.AddHostedService<NotificationWorker>();
Channel 只存在于进程内存中,因此进程重启会丢失未发送的消息。需要持久化时请改用真正的队列(Azure Service Bus、RabbitMQ 等)。
测试
SendgoClient 是 sealed 且内部自行创建 HttpClient,因此无法继承,也无法注入替身消息处理器。请在其前面放一层自己的接口来做替换 —— 这同时也让调用点不再依赖 SDK:
public interface IOrderNotifier
{
Task ConfirmedAsync(string phone, string orderNo, CancellationToken ct = default);
}
public class OrderNotifier(SendgoClient sendgo) : IOrderNotifier
{
public Task ConfirmedAsync(string phone, string orderNo, CancellationToken ct = default) =>
sendgo.SendAlimtalkAsync(new AlimtalkRequest
{
TemplateCode = "ORDER_CONFIRM_001",
Contacts = new[] { new Contact { PhoneNumber = phone, Var1 = orderNo } },
}, ct);
}
builder.Services.AddScoped<IOrderNotifier, OrderNotifier>();
public class TestFactory : WebApplicationFactory<Program>
{
public Mock<IOrderNotifier> Notifier { get; } = new();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.RemoveAll<IOrderNotifier>();
services.AddScoped(_ => Notifier.Object);
});
}
}
若某个端到端测试必须真正走一遍 SDK,请把 Sendgo:BaseUrl 指向本地桩服务(WireMock.Net,或另一个 WebApplication),而不是替换客户端。
异常处理
把 Sendgo 的错误映射为 HTTP 状态码,而不是让它变成 500:
using Sendgo.Exceptions;
app.UseExceptionHandler(handler => handler.Run(async context =>
{
var error = context.Features.Get<IExceptionHandlerFeature>()?.Error;
if (error is not SendgoException e)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
return;
}
context.Response.StatusCode = e.ErrorCode switch
{
// 是我们的配置有误,而非调用方的请求有误。
"INVALID_ACCESS_KEY" or "INVALID_SECRET_KEY" or "IP_NOT_ALLOWED"
=> StatusCodes.Status500InternalServerError,
"PAYMENT_REQUIRED" => StatusCodes.Status402PaymentRequired,
_ => e.StatusCode >= 500 ? StatusCodes.Status502BadGateway : StatusCodes.Status400BadRequest,
};
await context.Response.WriteAsJsonAsync(new { error = e.ErrorCode });
}));
请根据 ErrorCode 分支,而不要匹配错误消息文本 —— 消息文案可能变更,错误代码才是契约。
TOKEN_EXPIRED 与 TOKEN_MISMATCH 已在核心 SDK 内部处理:会重新签发令牌并自动重试该请求一次。
配置项
配置节直接绑定到 SendgoOptions。
| 键 | 是否必填 | 默认值 | 说明 |
|---|---|---|---|
Sendgo:AccessKey |
必填 | — | Sendgo 访问密钥 |
Sendgo:SecretKey |
必填 | — | Sendgo 私密密钥 |
Sendgo:KakaoSenderKey |
选填 | null |
Kakao 发送者资料密钥 |
Sendgo:SmsSenderKey |
选填 | null |
短信主叫号码密钥 |
Sendgo:ApiVersion |
选填 | v1 |
API 版本(v1 | v2) |
Sendgo:BaseUrl |
选填 | https://sendgo.io |
API 基础地址 |
短链接
短链接会缩短消息正文中的链接,并统计该链接是否被真正点击。 短信按字节计费,因此链接更短就意味着正文可以写更多内容。
仅 v2 支持。
再次缩短同一个原始 URL 会直接返回已有的链接。若希望按活动分别统计反应,
请使用 forceNew 生成新的短码。
deactivate 不会删除链接,只停止重定向。当已发送消息中的链接必须失效时使用它;
累计统计会保留,访问已停止的链接会返回 410 Gone。
app.MapPost("/shorten", async (
string targetUrl, SendgoClient sendgo, CancellationToken ct) =>
{
var created = await sendgo.CreateShortUrlAsync(new ShortUrlRequest
{
TargetUrl = targetUrl,
}, ct);
return Results.Ok(created);
});
app.MapGet("/shorten/{code}/stats", (string code, SendgoClient sendgo, CancellationToken ct) =>
sendgo.GetShortUrlStatsAsync(code, ct: ct));
stats 返回按日趋势(daily)以及按设备(byDevice)、来源(byReferer)、国家(byCountry)的分解。按日趋势读取的是预聚合表,因此点击量再大响应时间也保持稳定。
包信息
- 包名:
Sendgo.AspNetCore(NuGet) - 仓库:send-go/aspnetcore
- 注册表:https://www.nuget.org/packages/Sendgo.AspNetCore
- 许可:MIT
如何获取 API 密钥
登录 Sendgo 后,在 API/SDK → API 对接 菜单中签发访问密钥与私密密钥。
使用通知消息/好友消息需在 Kakao 渠道 登记发送者资料以获取 Sendgo:KakaoSenderKey;使用短信需在 发送号码 菜单登记主叫号码。
이 패키지로 할 수 있는 것
Send a Kakao Alimtalk — PHP, Node.js, Python, Java, Go examples →
Send Kakao Alimtalk with an approved template code. Runnable code for Laravel, Node.js, Python, PHP, Java, Go, Ruby and .NET, plus every required field and why sends fail.
Send SMS, LMS and MMS in South Korea — code examples →
Send Korean SMS (90 bytes), LMS (long text) and MMS (with images) through Sendgo. Type selection, byte counting, verification-code patterns and advertising rules.