.NET Result Pattern: Mikroservis ve Monolit Mimarilere Yönelik Modern Bir Yaklaşım
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
gotogibi 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)
Result'ı IResult'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:
Ö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 ArchitectureveDDDprensipleriyle mükemmel uyum sağlar.Standart ve Tutarlı API'ler:
ProblemDetailskullanı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;veyareturn 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 olanProductkaydı 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:ResultveResult<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:IProductServicearayüzünün somut implementasyonu. Örnekte statik bir liste kullansak da, gerçek bir projede bu sınıf veritabanı işlemleri içinIProductRepositorygibi bir arayüze bağımlı olurdu.DTOs/:CreateProductRequestgibi, 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.AppDbContextve 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>nesnesiniIResult'a (HTTP yanıtına) dönüştürenToHttpResultgibi extension metotları içerir. Bu, tamamen sunum katmanına ait bir sorumluluktur.Common/Errors.cs:ProblemDetailsnesneleri üreten ve HTTP durum kodlarına sıkıca bağlı olanErrorssı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
Apiprojesi,Applicationprojesine referans verir.Infrastructureprojesi,Applicationprojesine referans verir.Applicationprojesi,Domainprojesine referans verir.Domainprojesi 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.



