Skip to main content
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:
CodeAnlam
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" ]
    }
  }
}
BaseUrl
string
NetGSM API kök adresi. HttpClient BaseAddress olarak ayarlanır.
OtpPath
string
default:"/sms/rest/v2/otp"
OTP endpoint yolu.
Username / Password
string
Basic Auth kimlik bilgileri (Base64’lenip Authorization: Basic header’a yazılır).
MessageHeader
string
Onaylı gönderici adı (msgheader).
Phones
string[]
Production dışı ortamlarda gönderim yalnızca bu numaralara yapılır.
RetryCount
int
default:"3"
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.