SMS servisi src/DiyanetCleanArchitecture.Infrastructure.Services.Sms projesindedir ve yalnızca OTP (tek kullanımlık kod) SMS’i gönderir. Sağlayıcı NetGSM REST v2 OTP API’sidir (/sms/rest/v2/otp).
Arayüz
public interface IOtpSmsService
{
Task SendAsync ( string message , Phone to );
Task SendAsync ( OtpCode otpCode , Phone to );
Task SendAsync ( string message , params Phone [] to );
}
OtpCode overload’u mesajı sabit bir kalıpla kendisi oluşturur (ASCII-only):
public async Task SendAsync ( OtpCode otpCode , Phone to )
{
string message = $"DiyanetCleanArchitecture OTP kodunuz: { otpCode . Value } Kimseyle paylasmayin." ;
await SendAsync ( message , new [] { to });
}
ASCII zorunluluğu
NetGSM OTP API’si Türkçe karakter kabul etmez. Gönderimden önce mesaj ASCII kontrolünden geçer; değilse SmsServiceException fırlatılır:
if ( ! message . IsAscii ())
throw new SmsServiceException ( "OTP SMS ASCII karakterler içermelidir (Türkçe karakter kullanılamaz)." );
OTP mesaj metinlerinde “ı, ş, ğ, ü, ö, ç” gibi karakterler kullanılamaz. Yukarıdaki sabit mesajda da bilinçli olarak “paylasmayin” yazılmıştır.
İstek/yanıt modeli
İstek gövdesi NetGSM’in beklediği alan adlarıyla (küçük harf) serialize edilir. Telefon numarası Phone.ToNetGsmFormat() ile NetGSM formatına çevrilir:
var request = new GsmOtpRequest
{
msgheader = _options . MessageHeader ,
msg = message , // ASCII only
no = phone . ToNetGsmFormat (),
appname = null
};
var response = await _client . PostAsync ( _options . OtpPath , content );
var body = await response . Content . ReadAsStringAsync ();
GsmOtpResponse ? result = TryDeserialize ( body );
Esas başarı kontrolü HTTP status değil, gövdedeki code alanıdır . code != "00" ise iş hatası olarak değerlendirilir:
if ( result is not null && result . code != "00" )
{
throw new SmsServiceException (
$"OTP SMS başarısız. Code: { result . code } , Desc: { result . description } . " +
GsmOtpErrors . ToMessage ( result . code ));
}
if ( ! response . IsSuccessStatusCode )
throw new SmsServiceException ( $"OTP SMS HTTP hatası. Status: { response . StatusCode } , Body: { body } " );
NetGSM hata kodları
GsmOtpErrors.ToMessage(code) kod → mesaj eşlemesini yapar:
Code Anlam 00Başarılı 20Mesaj içeriği veya uzunluğu hatalı 30API kullanıcı bilgileri hatalı veya IP kısıtı 40 / 41Gönderici adı (msgheader) hatalı 50 / 52Telefon numarası hatalı 60OTP SMS paketi tanımlı değil 70Parametreler hatalı 100NetGSM sistem hatası
Ortam bazlı telefon çözümleme — SmsPhoneResolver
Production’da gerçek numaralara gönderim yapılır; Development/Staging’de ise yalnızca config’teki test numaralarına gönderilir (gerçek kullanıcılara SMS gitmesini engellemek için):
public Phone [] Resolve ( params Phone [] to )
{
if ( _environment . IsProduction ())
return to ;
if ( _environment . IsDevelopment () || _environment . IsStaging ())
{
if ( ! _options . Phones . Any ())
throw new SmsServiceException ( "Tanımlı telefon numarası yok. Services:Sms:Phones" );
return _options . Phones . Select ( p => new Phone ( p )). Distinct (). ToArray ();
}
throw new SmsServiceException ( "Bilinmeyen environment" );
}
Config — Services:Sms
{
"Services" : {
"Sms" : {
"BaseUrl" : "https://api.gsm.com.tr" ,
"OtpPath" : "/sms/rest/v2/otp" ,
"Username" : "myusername" ,
"Password" : "mypassword" ,
"MessageHeader" : "myheader" ,
"Encoding" : "TR" ,
"TimeoutSeconds" : 10 ,
"RetryCount" : 1 ,
"Phones" : [ "5443590815" ]
}
}
}
NetGSM API kök adresi. HttpClient BaseAddress olarak ayarlanır.
OtpPath
string
default: "/sms/rest/v2/otp"
OTP endpoint yolu.
Basic Auth kimlik bilgileri (Base64’lenip Authorization: Basic header’a yazılır).
Onaylı gönderici adı (msgheader).
Production dışı ortamlarda gönderim yalnızca bu numaralara yapılır.
Polly HTTP retry sayısı (300ms × deneme lineer backoff).
DI kaydı — HttpClient + Polly + Basic Auth
public static IServiceCollection AddSmsService ( this IServiceCollection services , IConfiguration configuration )
{
services . Configure < GsmOptions >( o => configuration . GetSection ( "Services:Sms" ). Bind ( o ));
services . AddTransient < SmsPhoneResolver >();
services . AddHttpClient < IOtpSmsService , OtpSmsService >(( sp , client ) =>
{
var options = sp . GetRequiredService < IOptions < GsmOptions >>(). Value ;
var credentials = Convert . ToBase64String (
Encoding . UTF8 . GetBytes ( $" { options . Username } : { options . Password } " ));
client . BaseAddress = new Uri ( options . BaseUrl );
client . DefaultRequestHeaders . Authorization = new AuthenticationHeaderValue ( "Basic" , credentials );
client . Timeout = TimeSpan . FromSeconds ( options . TimeoutSeconds );
})
. AddPolicyHandler (( sp , _ ) =>
{
var retry = sp . GetRequiredService < IOptions < GsmOptions >>(). Value . RetryCount ;
return HttpPolicyExtensions
. HandleTransientHttpError ()
. WaitAndRetryAsync ( retry , r => TimeSpan . FromMilliseconds ( 300 * r ));
});
// sms-provider health check — [External]: SMS down → /health/ready bozulmaz
services . AddHealthChecks (). AddUrlGroup ( uri : smsUri , name : "sms-provider" ,
failureStatus : HealthStatus . Degraded ,
tags : new [] { HealthCheckTags . External , HealthCheckTags . Sms },
timeout : TimeSpan . FromSeconds ( 5 ));
return services ;
}
Email Servisi OTP’nin e-posta kanalı.
Value Objects Phone.ToNetGsmFormat() ve OtpCode.Generate() value object’leri.