Result Pattern ile Tutarlı Response Yapıları konusunu anlatan renkli 3D teknik illüstrasyon

Result Pattern Nedir? Neden Önemli?

Result Pattern, metotlarınızın başarılı mı yoksa hatalı mı sonuçlandığını tek bir yapı içinde tutmanızı sağlar. Böylece:

  • Tek Tip Dönüş: Hem veri hem de işlem sonucunu tek bir sınıfla dönebilirsiniz.
  • Hata Mesajı ve Kod: Client’a güvenli hata bilgisi, log’a uygun teknik context taşıyabilirsiniz; aynı ayrıntıyı iki hedefe otomatik vermeyin.
  • Akıcı Zincirleme: Birden çok işlemi ardışık çağırırken, hata durumunda akışı kesip geriye dönmek kolaylaşır.

Avantajlar & Dezavantajlar

Avantajlar

  1. Tutarlılık: Tüm servisler aynı Result<T> yapısını kullanır.
  2. Okunabilirlik: Hata kontrolü tek bir yerde, if-else’lerle dağılmaz.
  3. Zincirleme (Chaining): Failure sonraki adımı ancak caller kontrolü veya açık Bind/Map implementation’ı ile durdurur.

Dezavantajlar

  1. İlave Tip: Paylaşılan bir generic Result<T> tipi yeterlidir; her metot için yeni sınıf gerekmez.
  2. Öğrenme Eğrisi: Bazı geliştiriciler için yeni bir pattern’e alışmak zaman alabilir.

Pratik Not: Basit CRUD metotlarında doğrudan Task<T> dönmek yeterliyken, iş kuralları ve validasyon içeren metotlarda Result Pattern kullanmak uzun vadede kolaylık sağlar.

Result Pattern: C# örnek uygulama

1. Result Sınıfı Tasarımı

public enum ErrorKind
{
    Validation,
    NotFound,
    Conflict,
}

public class Result
{
    public bool IsSuccess { get; }
    public string? Error { get; }
    public ErrorKind? Kind { get; }

    protected Result(bool isSuccess, string? error, ErrorKind? kind)
    {
        if (isSuccess && (error is not null || kind is not null))
            throw new ArgumentException("Success hata taşıyamaz.");
        if (
            !isSuccess
            && (string.IsNullOrWhiteSpace(error) || kind is null)
        )
            throw new ArgumentException(
                "Failure hata mesajı ve türü gerektirir."
            );
        if (kind is { } value && !Enum.IsDefined(value))
            throw new ArgumentOutOfRangeException(nameof(kind));
        IsSuccess = isSuccess;
        Error = error;
        Kind = kind;
    }

    public static Result Success() => new(true, null, null);

    public static Result Failure(
        string error,
        ErrorKind kind = ErrorKind.Validation
    ) => new(false, error, kind);
}

public sealed class Result<T> : Result
{
    private readonly T? _value;
    public T Value =>
        IsSuccess
            ? _value!
            : throw new InvalidOperationException(
                "Failure sonucu Value içermez."
            );

    private Result(
        bool success,
        T? value,
        string? error,
        ErrorKind? kind
    )
        : base(success, error, kind) => _value = value;

    public static Result<T> Success(T value) =>
        new(true, value, null, null);

    public static new Result<T> Failure(
        string error,
        ErrorKind kind = ErrorKind.Validation
    ) => new(false, default, error, kind);
}

2. Servis Metodunda Kullanımı

public class UserService
{
    private readonly IUserRepository _repository;
    private readonly IMapper _mapper;
    private readonly IUnitOfWork _unitOfWork;

    public UserService(
        IUserRepository repository,
        IMapper mapper,
        IUnitOfWork unitOfWork
    )
    {
        _repository = repository;
        _mapper = mapper;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result<UserDto>> GetUserByIdAsync(
        int id,
        CancellationToken ct = default
    )
    {
        if (id <= 0)
            return Result<UserDto>.Failure("Geçersiz ID");
        var user = await _repository.GetByIdAsync(id, ct);
        if (user is null)
            return Result<UserDto>.Failure(
                "Kullanıcı bulunamadı.",
                ErrorKind.NotFound
            );
        return Result<UserDto>.Success(_mapper.Map<UserDto>(user));
    }

    public async Task<Result> UpdateUserEmailAsync(
        int id,
        string email,
        CancellationToken ct = default
    )
    {
        if (id <= 0)
            return Result.Failure("Geçersiz ID");
        if (
            string.IsNullOrWhiteSpace(email)
            || !System.Net.Mail.MailAddress.TryCreate(
                email.Trim(),
                out var address
            )
            || !string.Equals(
                email.Trim(),
                address.Address,
                StringComparison.Ordinal
            )
        )
            return Result.Failure("Geçersiz e-posta adresi.");
        var user = await _repository.GetByIdAsync(id, ct);
        if (user is null)
            return Result.Failure(
                "Kullanıcı bulunamadı.",
                ErrorKind.NotFound
            );
        user.Email = email.Trim();
        _repository.Update(user);
        await _unitOfWork.CommitAsync(ct);
        return Result.Success();
    }
}

Gerçek Projelerde Dikkat Edilmesi Gerekenler

  • Exception vs Result: İş mantığı hatalarını Result.Failure() ile yönetin; altyapı hatalarını (DbException vb.) ise global exception handler’da yakalayın.
  • Hata Kodu Standardı: Tek bir metin yerine Error’a enum veya kod (“USER_NOT_FOUND”) eklemek, front-end ile anlaşmayı kolaylaştırır.
  • Zincirleme Akış:
// Controller action içindeki response mapping kesiti:
var result = await service.UpdateUserEmailAsync(id, email, ct);
if (!result.IsSuccess)
    return result.Kind switch
    {
        ErrorKind.NotFound => NotFound(new { Error = result.Error }),
        ErrorKind.Conflict => Conflict(new { Error = result.Error }),
        _ => BadRequest(new { Error = result.Error }),
    };
return NoContent();
  • Loglama: Başarısız Result döndüğünde, gerekli context’i log’layın; credential, e-posta ve iç exception ayrıntısını client’a taşımayın.

Result Pattern ve HTTP response sınırı

using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Authorize]
[Route("api/account")]
public class AccountController : ControllerBase
{
    private readonly UserService _userService;

    public AccountController(UserService userService) =>
        _userService = userService;

    [HttpPut("{id:int}/email")]
    public async Task<IActionResult> UpdateEmail(
        int id,
        [FromBody] string email,
        CancellationToken ct
    )
    {
        if (id <= 0)
            return BadRequest(new { Error = "Geçersiz ID" });
        // Örnek contract: authenticated NameIdentifier claim’i local int user ID’dir.
        if (
            !int.TryParse(
                User.FindFirstValue(ClaimTypes.NameIdentifier),
                out var callerId
            )
            || callerId != id
        )
            return Forbid();
        var result = await _userService.UpdateUserEmailAsync(
            id,
            email,
            ct
        );
        if (!result.IsSuccess)
            return result.Kind switch
            {
                ErrorKind.NotFound => NotFound(
                    new { Error = result.Error }
                ),
                ErrorKind.Conflict => Conflict(
                    new { Error = result.Error }
                ),
                _ => BadRequest(new { Error = result.Error }),
            };
        return NoContent();
    }
}

İleri Okuma ve Kaynaklar

Sonuç

Result Pattern, .NET projelerinde hata yönetimini ve return akışını tutarlı hale getirir. Basit CRUD işlemlerinde ihtiyaç duymayabilirsiniz, ancak iş kuralları ve birçok validasyon içeren senaryolarda kodunuzun temiz ve okunabilir kalmasını sağlar.

Bu Result Pattern örneği success/failure invariant’ını korur; failure üzerinden Value okumak hata üretir. Success payload’ının nullable olup olmamasını T contract’ı belirler. ErrorKind beklenen Validation, NotFound ve Conflict sonuçlarını HTTP status’a çevirir; beklenmeyen infrastructure exception’ı veya cancellation’ı genel failure’a dönüştürmeyiz.

IUserRepository.GetByIdAsync ve IUnitOfWork.CommitAsync token kabul eden uygulama contract’larıdır; User/UserDto, AutoMapper configuration ve DI kayıtları ayrıca gerekir. MailAddress parse kontrolü adres sahipliğini veya mailbox teslimini kanıtlamaz. Gerçek e-posta değişiminde doğrulama, unique constraint ve uygun concurrency davranışını da tasarlayın.

Controller yalnız authenticated kullanıcının kendi integer ID’si için demo akışıdır. NameIdentifier mapping’iniz farklıysa güvenilir local ID eşlemesini uygulayın. Startup’ta gerçek authentication scheme, AddAuthorization, UseAuthentication/UseAuthorization ve MapControllers gerekir; Authorize attribute tek başına sistemi kurmaz. Daha geniş servis kullanımlarında ownership kuralını Application sınırında da koruyun.

Response kesiti controller action alternatifi, using’ler ise ilgili dosyaların başı içindir. Namespace’ler AutoMapper, ASP.NET Core MVC ve Claims API’sini içerir. Sonuç tipi global exception handling’in yerine geçmez; Exception Handling rehberi bu sorumluluğu açıklar.

By tanju.bozok

Senior .NET Engineer | Backend architecture, distributed systems & real-time communication