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:
Performans Maliyeti: İstisna fırlatmak ve yakalamak (try-catch), normal kod akışına göre oldukça maliyetli bir operasyondur.
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.
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
{
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
};
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;
}
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);
public static implicit operator Result<TValue>(TValue value) => Success(value);
public static implicit operator Result<TValue>(ProblemDetails error) => Failure(error);
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
: Errors.NotFound("Product Not Found", $"Product with ID '{id}' was not found.");
}
public Result<Product> Create(CreateProductRequest request)
{
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);
}
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.");
}
var newProduct = new Product(Guid.NewGuid(), request.Name, request.Price, request.Stock);
_products.Add(newProduct);
return newProduct;
}
public Result UpdatePrice(UpdatePriceRequest request)
{
Result<Product> getResult = GetById(request.Id);
if (getResult.IsFailure)
{
return getResult.Error!;
}
if (request.NewPrice <= 0)
{
return Errors.BadRequest("Invalid Price", "New price must be a positive value.");
}
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)
Result'ı IResult'a dönüştüren bir yardımcı metot ve endpoint tanımları:
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)
);
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!);
return Results.NoContent();
}
}
builder.Services.AddScoped<IProductService, ProductService>();
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)
: Problem(result.Error!);
protected IActionResult HandleResult(Result result) =>
result.IsSuccess
? NoContent()
: Problem(result.Error!);
[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:
Ö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.
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.
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.
Yüksek Performans: Beklenen hatalar için istisna fırlatma maliyetinden kaçınılır.
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
2. ⚙️ ResultPattern.Application
3. 🔌 ResultPattern.Infrastructure
4. 💻 ResultPattern.Api (Sunum Katmanı)
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.