Skip to main content

Command Palette

Search for a command to run...

.NET Result Pattern: Mikroservis ve Monolit Mimarilere Yönelik Modern Bir Yaklaşım

Updated
9 min read

Giriş: Neden Result Pattern'e İhtiyacımız Var?

Yazılım geliştirmede hata yönetimi kritik bir konudur. Geleneksel olarak, özellikle C# gibi dillerde, hata durumlarını belirtmek için istisnalar (exceptions) kullanılır. Ancak istisnalar, beklenmedik ve programın akışını bozan istisnai durumlar için tasarlanmıştır. "Kullanıcı bulunamadı", "E-posta zaten kayıtlı" veya "Stok yetersiz" gibi durumlar, uygulamanın normal işleyişi içinde öngörülebilen ve beklenen başarısızlık senaryolarıdır.

Bu tür beklenen hataları istisnalarla yönetmek birkaç soruna yol açar:

  1. Performans Maliyeti: İstisna fırlatmak ve yakalamak (try-catch), normal kod akışına göre oldukça maliyetli bir operasyondur.

  2. Anlamsal Kargaşa: Kodun okunabilirliğini düşürür ve "hangi metodun ne tür bir beklenen hata fırlatabileceği" bilgisini metodun imzasından gizler.

  3. Kontrol Akışı Anti-Pattern'i: İstisnaları bir goto gibi kontrol akışı için kullanmak, kodun takibini zorlaştırır.

Result Pattern, bu sorunlara zarif bir çözüm sunar. Bir metodun sonucunu, başarı durumunda bir değer (value) ya da başarısızlık durumunda bir hata (error) içerebilen tek bir nesne içinde döndürmemizi sağlar.

Bu rehberde, ProblemDetails (RFC 7807) standardını kullanarak hem API'ler için tutarlı hem de servis katmanında esnek bir desen oluşturacağız.

Bölüm 1: Mimarinin Temel Taşları

Desenimizi oluşturacak temel sınıfları ve yardımcı yapıları adım adım inşa edelim.

1.1. ProblemDetails ve Standart Hata Fabrikası (Errors Sınıfı)

Hata nesnelerimizi standart hale getirmek için ASP.NET Core'un yerleşik ProblemDetails sınıfını kullanacağız. Bu, API'lerimizin RFC 7807 standardına uygun, tutarlı ve makine tarafından okunabilir hatalar üretmesini sağlar.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

public static class Errors
{
    // 404 Not Found
    public static ProblemDetails NotFound(string title, string detail) =>
        new()
        {
            Status = StatusCodes.Status404NotFound,
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
            Title = title,
            Detail = detail
        };

    // 400 Bad Request (özellikle validasyon hataları için)
    public static ProblemDetails BadRequest(string title, string detail, IReadOnlyDictionary<string, string[]>? validationErrors = null)
    {
        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            Title = title,
            Detail = detail,
        };

        if (validationErrors is not null)
        {
            problemDetails.Extensions.Add("validationErrors", validationErrors);
        }

        return problemDetails;
    }

    // 409 Conflict
    public static ProblemDetails Conflict(string title, string detail) =>
        new()
        {
            Status = StatusCodes.Status409Conflict,
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
            Title = title,
            Detail = detail
        };
}

1.2. Değer Döndürmeyen Sonuç: Result Sınıfı

void metotlar (örn: Update, Delete) için operasyonun sadece başarılı veya başarısız olduğunu bildiren temel sınıftır.

public class Result
{
    public bool IsSuccess { get; }
    public bool IsFailure => !IsSuccess;
    public ProblemDetails? Error { get; }

    protected Result(bool isSuccess, ProblemDetails? error)
    {
        if (isSuccess && error is not null || !isSuccess && error is null)
        {
            throw new InvalidOperationException("Invalid result state.");
        }
        IsSuccess = isSuccess;
        Error = error;
    }

    public static Result Success() => new(true, null);
    public static Result Failure(ProblemDetails error) => new(false, error);
    public static implicit operator Result(ProblemDetails error) => Failure(error);
}

1.3. Değer Döndüren Sonuç: Result<TValue> Sınıfı

Başarılı olduğunda bir değer taşıyan generic versiyondur.

public class Result<TValue> : Result
{
    private readonly TValue? _value;

    public TValue Value => IsSuccess
        ? _value!
        : throw new InvalidOperationException("Cannot access the value of a failed result.");

    private Result(TValue value) : base(true, null) => _value = value;
    private Result(ProblemDetails error) : base(false, error) => _value = default;

    public static Result<TValue> Success(TValue value) => new(value);
    public new static Result<TValue> Failure(ProblemDetails error) => new(error);

    // Implicit operator'ler sayesinde kod daha akıcı hale gelir.
    public static implicit operator Result<TValue>(TValue value) => Success(value);
    public static implicit operator Result<TValue>(ProblemDetails error) => Failure(error);

    // Sonucu fonksiyonel bir şekilde işlemek için Match metodu
    public TResponse Match<TResponse>(
        Func<TValue, TResponse> onSuccess,
        Func<ProblemDetails, TResponse> onFailure) =>
        IsSuccess ? onSuccess(Value) : onFailure(Error!);
}

Bölüm 2: Kullanım Senaryoları

Şimdi bu yapıları gerçekçi bir servis katmanı senaryosunda kullanalım.

2.1. Servis Arayüzü ve Modeller

public record Product(Guid Id, string Name, decimal Price, int Stock);
public record CreateProductRequest(string Name, decimal Price, int Stock);
public record UpdatePriceRequest(Guid Id, decimal NewPrice);

public interface IProductService
{
    Result<Product> GetById(Guid id);
    Result<Product> Create(CreateProductRequest request);
    Result UpdatePrice(UpdatePriceRequest request);
    Result Delete(Guid id);
}

2.2. ProductService Implementasyonu

public class ProductService : IProductService
{
    private static readonly List<Product> _products = [];

    public Result<Product> GetById(Guid id)
    {
        var product = _products.FirstOrDefault(p => p.Id == id);
        return product is not null
            ? product // Implicit conversion to Result<Product>.Success
            : Errors.NotFound("Product Not Found", $"Product with ID '{id}' was not found.");
    }

    public Result<Product> Create(CreateProductRequest request)
    {
        // 1. Validasyon
        var validationErrors = new Dictionary<string, string[]>();
        if (string.IsNullOrWhiteSpace(request.Name))
        {
            validationErrors.Add(nameof(request.Name), ["Product name cannot be empty."]);
        }
        if (request.Price <= 0)
        {
            validationErrors.Add(nameof(request.Price), ["Price must be positive."]);
        }
        if (validationErrors.Count > 0)
        {
            return Errors.BadRequest(
                "Validation Failed", 
                "One or more validation errors occurred.", 
                validationErrors);
        }

        // 2. İş Kuralı Kontrolü
        if (_products.Any(p => p.Name.Equals(request.Name, StringComparison.OrdinalIgnoreCase)))
        {
            return Errors.Conflict("Duplicate Product", $"A product with the name '{request.Name}' already exists.");
        }

        // 3. Başarılı Durum
        var newProduct = new Product(Guid.NewGuid(), request.Name, request.Price, request.Stock);
        _products.Add(newProduct);

        return newProduct;
    }

    public Result UpdatePrice(UpdatePriceRequest request)
    {
        // 1. Varlık kontrolü
        Result<Product> getResult = GetById(request.Id);
        if (getResult.IsFailure)
        {
            return getResult.Error!; // Varlık bulunamadı hatasını aynen geri döndür.
        }

        // 2. Validasyon
        if (request.NewPrice <= 0)
        {
            return Errors.BadRequest("Invalid Price", "New price must be a positive value.");
        }

        // 3. Başarılı Durum
        var existingProduct = getResult.Value;
        var updatedProduct = existingProduct with { Price = request.NewPrice };

        _products.Remove(existingProduct);
        _products.Add(updatedProduct);

        return Result.Success();
    }

    public Result Delete(Guid id)
    {
        var product = _products.FirstOrDefault(p => p.Id == id);
        if (product is null)
        {
            return Errors.NotFound("Product Not Found", $"Cannot delete. Product with ID '{id}' was not found.");
        }

        _products.Remove(product);
        return Result.Success();
    }
}

Bölüm 3: API Katmanı Entegrasyonu

Servis katmanından dönen Result nesnelerini standart HTTP yanıtlarına çevirelim.

3.1. Yaklaşım 1: Minimal API'ler (Modern ve Hafif)

ResultIResult'a dönüştüren bir yardımcı metot ve endpoint tanımları:

// Helper Class
public static class ApiExtensions
{
    public static IResult ToHttpResult<TValue>(this Result<TValue> result) =>
        result.Match(
            onSuccess: value => Results.Ok(value),
            onFailure: problem => Results.Problem(problem)
        );

    // Yeni oluşturulan kaynaklar için 201 Created yanıtı
    public static IResult ToCreatedHttpResult<TValue>(this Result<TValue> result, string location) where TValue : Product =>
        result.Match(
            onSuccess: value => Results.Created($"{location}/{value.Id}", value),
            onFailure: problem => Results.Problem(problem)
        );

    public static IResult ToHttpResult(this Result result)
    {
        if (result.IsFailure) return Results.Problem(result.Error!);
        // Başarılı Update/Delete işlemleri için genellikle 204 No Content döndürülür.
        return Results.NoContent();
    }
}

// Program.cs
// ... (Service registration)
builder.Services.AddScoped<IProductService, ProductService>();
// Route registration
app.MapGet("/products/{id:guid}", ([FromRoute] Guid id,[FromService] IProductService service) => service.GetById(id).ToHttpResult());
app.MapPost("/products", ([FromBody] CreateProductRequest req,[FromService] IProductService service) => service.Create(req).ToCreatedHttpResult("/products"));
app.MapPut("/products/price", ([FromBody] UpdatePriceRequest req,[FromService] IProductService service) => service.UpdatePrice(req).ToHttpResult());
app.MapDelete("/products/{id:guid}", ([FromRoute] Guid id,[FromService] IProductService service) => service.Delete(id).ToHttpResult());

app.Run();

3.2. Yaklaşım 2: MVC Controller'lar (Geleneksel ve Güçlü)

Tüm controller'ların miras alacağı bir ApiControllerBase oluşturarak tekrarı önleyebiliriz.

[ApiController]
public abstract class ApiControllerBase : ControllerBase
{
    protected IActionResult HandleResult<TValue>(Result<TValue> result) =>
        result.IsSuccess
            ? Ok(result.Value)
            : Problem(result.Error!);

    protected IActionResult HandleCreatedResult<TValue>(Result<TValue> result) where TValue : Product =>
        result.IsSuccess
            ? CreatedAtAction(nameof(GetProduct), new { id = result.Value.Id }, result.Value) // GetProduct metoduna referans verir
            : Problem(result.Error!);

    protected IActionResult HandleResult(Result result) =>
        result.IsSuccess
            ? NoContent()
            : Problem(result.Error!);

    // Bu, HandleCreatedResult'ın çalışması için gerekli.
    // Gerçek bir controller'da olması gerekir. Örnek olarak burada.
    [HttpGet("{id:guid}", Name = "GetProduct")]
    public virtual IActionResult GetProduct(Guid id) => throw new NotImplementedException();
}

[Route("api/[controller]")]
public class ProductsController(IProductService productService) : ApiControllerBase
{
    [HttpGet("{id:guid}")]
    public override IActionResult GetProduct(Guid id) =>
        HandleResult(productService.GetById(id));

    [HttpPost]
    public IActionResult CreateProduct(CreateProductRequest request) =>
        HandleCreatedResult(productService.Create(request));

    [HttpPut("price")]
    public IActionResult UpdatePrice(UpdatePriceRequest request) =>
        HandleResult(productService.UpdatePrice(request));

    [HttpDelete("{id:guid}")]
    public IActionResult DeleteProduct(Guid id) =>
        HandleResult(productService.Delete(id));
}

Sonuç ve Tasarımın Avantajları

Bu desen, uygulamanızın hata yönetimi stratejisini kökten iyileştirir:

  1. Öngörülebilir Kod: Metot imzaları (Result<T>), bir metodun hem başarılı bir değer hem de bir hata döndürebileceğini açıkça belirtir. Sürpriz istisnalar ortadan kalkar.

  2. Temiz Mimariler: Servis katmanı (iş mantığı) sunum katmanından (API) tamamen habersizdir. Sadece operasyonun sonucunu bildirir. API katmanı ise bu sonucu alıp HTTP dünyasına çevirmekle sorumludur. Bu, Clean Architecture ve DDD prensipleriyle mükemmel uyum sağlar.

  3. Standart ve Tutarlı API'ler: ProblemDetails kullanımı sayesinde tüm hatalarınız standart bir formatta olur, bu da istemci (frontend, mobil, diğer servisler) geliştirmeyi çok kolaylaştırır.

  4. Yüksek Performans: Beklenen hatalar için istisna fırlatma maliyetinden kaçınılır.

  5. Geliştirici Dostu: Implicit operator'ler ve yardımcı metotlar, deseni kullanmayı son derece kolay ve akıcı hale getirir. return product; veya return Errors.NotFound(...); gibi ifadelerle kodunuz temiz kalır.

Ek Bölüm: Örnek Proje Mimarisi ve Klasör Yapısı

Bu bölümde, anlattığımız Result Pattern'in ve ilgili sınıfların, modern, test edilebilir ve sürdürülebilir bir .NET projesinde nasıl organize edilebileceğini göreceğiz. Önerilen yapı, katmanların sorumluluklarını net bir şekilde ayıran Clean Architecture (Temiz Mimari) prensiplerini temel almaktadır.

Projenin Genel Klasör Yapısı (ASCII Tree)

/ResultPattern.sln
│
├── 📂 src/
│   │
│   ├── 📁 ResultPattern.Domain/
│   │   └── 📦 Entities/
│   │       └── 📜 Product.cs
│   │
│   ├── 📁 ResultPattern.Application/
│   │   ├── 📦 Abstractions/
│   │   │   └── 📜 IProductService.cs
│   │   ├── 📦 Core/
│   │   │   └── 📜 Result.cs             // Result ve Result<TValue> sınıfları
│   │   ├── 📦 DTOs/
│   │   │   ├── 📜 CreateProductRequest.cs
│   │   │   └── 📜 UpdatePriceRequest.cs
│   │   └── 📦 Services/
│   │       └── 📜 ProductService.cs
│   │
│   ├── 📁 ResultPattern.Infrastructure/
│   │   └── 📦 Persistence/
│   │       └── 📜 AppDbContext.cs       // Örnek: Entity Framework Core context'i
│   │       └── 📦 Repositories/
│   │           └── 📜 ProductRepository.cs  // IProductRepository implementasyonu
│   │
│   └── 📁 ResultPattern.Api/ (Presentation)
│       ├── 📦 Common/
│       │   ├── 📜 ApiExtensions.cs
│       │   └── 📜 Errors.cs             // ProblemDetails üreten fabrika sınıfı
│       ├── 📦 Controllers/
│       │   └── 📜 ProductsController.cs   // MVC yaklaşımı için
│       └── 📜 Program.cs                // Minimal API endpoint'leri ve servis kayıtları
│
└── 📂 tests/
    │
    ├── 📁 ResultPattern.Application.Tests/
    │   └── 📜 ProductServiceTests.cs
    │
    └── 📁 ResultPattern.Domain.Tests/
        └── 📜 ProductTests.cs

Katmanların Açıklamaları ve Sorumlulukları

1. 📖 ResultPattern.Domain

  • Amacı: Uygulamanızın kalbidir. İş kurallarını ve varlıkları (entities) içerir. Diğer hiçbir projeye bağımlılığı yoktur; tamamen saf ve bağımsızdır.

  • İçerik:

    • Entities/Product.cs: Uygulamanın temel iş nesnesi olan Product kaydı burada yer alır.

2. ⚙️ ResultPattern.Application

  • Amacı: Uygulamanın iş mantığını (use case'leri) yönetir. Domain katmanındaki varlıkları kullanarak operasyonları gerçekleştirir. Altyapı (Infrastructure) ve sunum (Api) katmanlarından habersizdir.

  • İçerik:

    • Core/Result.cs: Result ve Result<TValue> sınıfları burada bulunur. Çünkü bu yapılar, uygulama katmanındaki servislerin dönüş tipini belirleyen temel bir araçtır.

    • Abstractions/IProductService.cs: Servislerin kontratları (arayüzler) burada tanımlanır.

    • Services/ProductService.cs: IProductService arayüzünün somut implementasyonu. Örnekte statik bir liste kullansak da, gerçek bir projede bu sınıf veritabanı işlemleri için IProductRepository gibi bir arayüze bağımlı olurdu.

    • DTOs/: CreateProductRequest gibi, sunum katmanından uygulama katmanına veri taşımak için kullanılan Veri Transfer Nesneleri (Data Transfer Objects) burada yer alır.

3. 🔌 ResultPattern.Infrastructure

  • Amacı: Veritabanları, harici servisler, dosya sistemleri gibi dış dünyayla ilgili tüm detayları içerir. Application katmanında tanımlanan arayüzleri uygular.

  • İçerik:

    • Persistence/: Entity Framework Core, Dapper veya başka bir ORM ile ilgili tüm kodlar burada bulunur. AppDbContext ve repository implementasyonları bu katmanın parçasıdır.

4. 💻 ResultPattern.Api (Sunum Katmanı)

  • Amacı: Dış dünyadan gelen istekleri (örn: HTTP istekleri) kabul edip uygulama katmanına yönlendiren ve uygulama katmanından dönen sonuçları dış dünyaya uygun bir formatta (örn: JSON yanıtı) sunan katmandır.

  • İçerik:

    • Program.cs: Minimal API endpoint'lerinin tanımlandığı, servislerin (DI) kaydedildiği ve uygulamanın konfigürasyonunun yapıldığı yerdir.

    • Common/ApiExtensions.cs: Result<T> nesnesini IResult'a (HTTP yanıtına) dönüştüren ToHttpResult gibi extension metotları içerir. Bu, tamamen sunum katmanına ait bir sorumluluktur.

    • Common/Errors.cs: ProblemDetails nesneleri üreten ve HTTP durum kodlarına sıkıca bağlı olan Errors sınıfı için en uygun yer burasıdır.

    • Controllers/ProductsController.cs: Eğer Minimal API yerine MVC yaklaşımını tercih ediyorsanız, controller'larınız bu klasörde yer alır.

Proje Referansları (Dependency Flow)

Bu mimarideki en önemli kural, bağımlılıkların her zaman merkeze doğru olmasıdır.

Api ➡️ Application ➡️ Domain

  • Api projesi, Application projesine referans verir.

  • Infrastructure projesi, Application projesine referans verir.

  • Application projesi, Domain projesine referans verir.

  • Domain projesi hiçbir projeye referans vermez.

Bu yapı, Result Pattern'in "sorumlulukları ayırma" felsefesini proje geneline yayarak esnek, test edilebilir ve bakımı kolay bir uygulama geliştirmenizi sağlar.

More from this blog

D

Devlog of Uygar Öztürk Ceylan

8 posts

Laravel, .NET, Docker ve self-hosting üzerine notlarımı ve çözümlerimi paylaştığım kişisel yazılım blogu.