ResponseState 1.0.0

dotnet add package ResponseState --version 1.0.0
                    
NuGet\Install-Package ResponseState -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ResponseState" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ResponseState" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="ResponseState" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ResponseState --version 1.0.0
                    
#r "nuget: ResponseState, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ResponseState@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ResponseState&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=ResponseState&version=1.0.0
                    
Install as a Cake Tool

ResponseState

🇹🇷 Türkçe | 🇬🇧 English


<a id="türkçe"></a>

🇹🇷 Türkçe

Clean Architecture .NET uygulamaları için tasarlanmış, standart ve tutarlı bir yanıt durumu (response state) yönetim kütüphanesi. Başarılı ve hatalı durumları tek tip bir sarmalayıcı (wrapper) üzerinden, önceden tanımlı durum kodları (StateCode) ile ifade etmenizi sağlar.

Tasarım Felsefesi: Mesaj Değil, StateCode Merkezli

Bu kütüphanenin temel fikri, servis katmanının serbest metin hata mesajları üretmesi değil, önceden tanımlanmış (veya kendi uygulamanızın türettiği) bir StateCode'a işaret etmesidir. Her StateCode zaten kendi HTTP kodunu, alt kodunu, önem seviyesini ve insan-okunur mesajını taşır:

return ResponseState<UserDto>.Fail(StateCode.NotFound); // ✅ önerilen kullanım

Serbest metin mesaj (Fail(string message)) yalnızca hiçbir StateCode'un karşılamadığı, tek seferlik/istisnai durumlar için bir kaçış kapısıdır — her zaman genel StateCode.Error (400) ile döner ve akışın normal yolu değildir. Bir durumu birden fazla yerde kullanacaksanız, serbest metin yazmak yerine kendi StateCode'unuzu tanımlayın (aşağıda örneği var).

Bazı StateCode'ların mesajı {0} gibi yer tutucular içerir (ör. StateCode.NotDeletedHasRelations) — bunlar özel bir "mesaj parametresi" değildir; ilgili constructor'lara params object[] olarak geçilen, string.Format ile doldurulan şablon argümanlarıdır. Aşağıda "Şablonlu (parametreli) StateCode mesajları" bölümünde ayrıntısı var.

Kurulum

dotnet add package ResponseState

Temel Yapı Taşları

Tip Açıklama
StateCode HTTP kodu, alt kod, mesaj, başarı ve önem seviyesi taşıyan, genişletilebilir durum kodu. 1xx-5xx standart HTTP kodları ve uygulama-özel kodlar (StateCode.NotFound, StateCode.ValidationError vb.) hazır gelir.
Status Bir StateCode'dan üretilen, JSON'a serileştirilebilen somut sonuç (Code, SubCode, Name, Message, Success, Level).
ResponseState / ResponseState<T> Servis/controller dönüş tipi. İçerik taşıyan (<T>) ve taşımayan sürümleri var; Success(...)/Fail(...) statik factory metotlarıyla üretilir.
StateException / ValidationException Bir StateCode ile ilişkilendirilmiş istisnalar; genellikle bir ASP.NET Core IExceptionHandler tarafından yakalanıp HTTP yanıtına çevrilir.
StateGuard Guard-clause (ön koşul) yardımcıları — null/boş/aralık kontrollerinde StateException fırlatır.
FluentValidationExtensions FluentValidation doğrulama hatalarını ValidationException'a çeviren genişletme metodu.

Kullanım

1. StateCode ile başarı/başarısızlık (önerilen yol)
public async Task<ResponseState<UserDto>> GetUserAsync(int id)
{
    var user = await _userRepository.GetByIdAsync(id);

    if (user is null)
        return ResponseState<UserDto>.Fail(StateCode.NotFound);

    return ResponseState<UserDto>.Success(user);
}
2. Serbest metin mesajla başarısızlık (istisnai durum)
// Sadece hiçbir StateCode'un karşılamadığı, tek seferlik durumlar için.
// Her zaman StateCode.Error (400) ile döner.
return ResponseState<UserDto>.Fail("Bu istek için beklenmedik bir durum oluştu.");
3. Şablonlu (parametreli) StateCode mesajları

StateCode.NotDeletedHasRelations'ın mesajı "Kayıt silinemedi. İlişkili veriler bulundu:\n- {0}" şeklindedir. Bu yer tutucuyu doldurmak Fail(StateCode) ile mümkün değildir — çünkü o metot şablon doldurmaz. Bunun için constructor'ı doğrudan kullanmanız gerekir:

public ResponseState DeleteCategory(int id)
{
    var relatedProducts = _productRepository.GetByCategoryId(id);

    if (relatedProducts.Any())
    {
        var relatedNames = string.Join("\n- ", relatedProducts.Select(p => p.Name));
        // {0} yer tutucusu, params object[] argümanlarıyla string.Format ile doldurulur:
        return new ResponseState(StateCode.NotDeletedHasRelations, relatedNames);
    }

    _categoryRepository.Delete(id);
    return ResponseState.Success();
}

ResponseState<T> için içerikli eşdeğeri: new ResponseState<T>(stateCode, content, args).

4. Kendi StateCode'unuzu tanımlamak

Uygulamanıza özel durumlar için StateCode'dan türeyin (alanlar static readonly olmalı):

public class ApplicationStateCode : StateCode
{
    public static readonly StateCode ApplicantAlreadyHired =
        new(409, 50001, "Bu aday zaten işe alınmış.", false, MessageLevel.Error);
}

// Kullanım:
return ResponseState<bool>.Fail(ApplicationStateCode.ApplicantAlreadyHired);

Status, uygulama başlangıcında (ve sonradan yüklenen assembly'lerde) bu tür alt sınıfları otomatik tarar; Status.Name alanı "ApplicantAlreadyHired" olarak doğru şekilde çözülür.

5. StateGuard ile guard-clause kullanımı
public void UpdateSalary(Employee employee, decimal newSalary)
{
    StateGuard.ThrowIfNull(employee);
    StateGuard.ThrowIfOutOfRange(newSalary, 0, 1_000_000);
    StateGuard.ThrowIfTrue(employee.IsTerminated, StateCode.Conflict,
        "İşten ayrılmış bir çalışanın maaşı güncellenemez.");

    employee.Salary = newSalary;
}
6. FluentValidation entegrasyonu
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
    public CreateUserValidator()
    {
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
    }
}

public async Task<ResponseState<UserDto>> CreateUserAsync(CreateUserRequest request)
{
    // Geçersizse alan bazlı hatalarla ValidationException fırlatır.
    await _validator.ValidateAndThrowCustom(request);

    var user = await _userRepository.CreateAsync(request);
    return ResponseState<UserDto>.Success(user);
}
7. İstisnaları HTTP yanıtına çevirme (ASP.NET Core IExceptionHandler)
public class StateExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken ct)
    {
        if (exception is ValidationException validationEx)
        {
            httpContext.Response.StatusCode = validationEx.StateCode.Code;
            await httpContext.Response.WriteAsJsonAsync(new
            {
                Status = new Status(validationEx.StateCode),
                validationEx.ValidationErrors
            }, ct);
            return true;
        }

        if (exception is StateException stateEx)
        {
            httpContext.Response.StatusCode = stateEx.StateCode.Code;
            var response = ResponseState.Fail(stateEx.StateCode);
            response.Status.Message = stateEx.DetailMessage; // özel mesaj varsa üzerine yazılır
            await httpContext.Response.WriteAsJsonAsync(response, ct);
            return true;
        }

        return false;
    }
}

Sürüm Notları (v1.0.0)

Önceki 10.x sürümüne göre:

  • Düzeltildi: Status'un StateCode adı çözümleme mekanizması artık sonradan yüklenen assembly'leri de tarıyor ve alt sınıfları (ör. ApplicationStateCode) doğru tanıyor (eski sürümde belirli assembly yükleme sıralarında sessizce "UnknownStateCode"'a düşebiliyordu).
  • Eklendi: Success(...)/Fail(...) statik factory metotları (README'de vaat edilip kodda eksik olan metotlar artık gerçek).
  • Eklendi: Tam XML dokümantasyonu, 15 birim testi.
  • İyileştirildi: Thread-safe önbellek (ConcurrentDictionary), readonly durum kodu alanları, nullable-safety düzeltmeleri.
  • Public API yüzeyi geriye dönük uyumludur.

Lisans

Bu proje MIT Lisansı altında lisanslanmıştır.


<a id="english"></a>

🇬🇧 English

A standard and consistent response state management library for Clean Architecture .NET applications. It lets you express success and failure through a single wrapper type, backed by predefined status codes (StateCode).

Design Philosophy: StateCode-First, Not Message-First

The core idea is that your service layer should point to a predefined (or your own app's derived) StateCode, not author free-text error messages. Every StateCode already carries its own HTTP code, sub-code, severity level, and human-readable message:

return ResponseState<UserDto>.Fail(StateCode.NotFound); // ✅ recommended

Free-text messages (Fail(string message)) are an escape hatch for one-off cases no existing StateCode covers — they always return the generic StateCode.Error (400) and are not the normal path. If a condition recurs in more than one place, define your own StateCode instead of writing free text (example below).

Some StateCode messages contain placeholders like {0} (e.g. StateCode.NotDeletedHasRelations) — these are not a special "message parameter"; they're params object[] template arguments filled via string.Format on the constructors that accept them. See "Templated StateCode messages" below.

Installation

dotnet add package ResponseState

Core Building Blocks

Type Description
StateCode Extensible status code carrying an HTTP code, sub-code, message, success flag, and severity level. Ships with standard 1xx-5xx HTTP codes plus application-level codes (StateCode.NotFound, StateCode.ValidationError, etc.).
Status A concrete, JSON-serializable result produced from a StateCode (Code, SubCode, Name, Message, Success, Level).
ResponseState / ResponseState<T> Your service/controller return type. Content-bearing (<T>) and content-less variants, produced via Success(...)/Fail(...) static factories.
StateException / ValidationException Exceptions associated with a StateCode, typically caught by an ASP.NET Core IExceptionHandler and turned into an HTTP response.
StateGuard Guard-clause helpers — throw StateException on null/empty/out-of-range preconditions.
FluentValidationExtensions Extension method turning FluentValidation failures into a ValidationException.

Usage

public async Task<ResponseState<UserDto>> GetUserAsync(int id)
{
    var user = await _userRepository.GetByIdAsync(id);

    if (user is null)
        return ResponseState<UserDto>.Fail(StateCode.NotFound);

    return ResponseState<UserDto>.Success(user);
}
2. Free-text message failure (exceptional case)
// Only for one-off cases no StateCode covers. Always returns StateCode.Error (400).
return ResponseState<UserDto>.Fail("An unexpected condition occurred for this request.");
3. Templated StateCode messages

StateCode.NotDeletedHasRelations's message is "Cannot delete record. Related data found:\n- {0}". You cannot fill that placeholder via Fail(StateCode) — it doesn't do template formatting. Use the constructor directly instead:

public ResponseState DeleteCategory(int id)
{
    var relatedProducts = _productRepository.GetByCategoryId(id);

    if (relatedProducts.Any())
    {
        var relatedNames = string.Join("\n- ", relatedProducts.Select(p => p.Name));
        // The {0} placeholder is filled via string.Format using the params object[] args:
        return new ResponseState(StateCode.NotDeletedHasRelations, relatedNames);
    }

    _categoryRepository.Delete(id);
    return ResponseState.Success();
}

For ResponseState<T>, the equivalent is new ResponseState<T>(stateCode, content, args).

4. Defining your own StateCode

Derive from StateCode for application-specific conditions (fields must be static readonly):

public class ApplicationStateCode : StateCode
{
    public static readonly StateCode ApplicantAlreadyHired =
        new(409, 50001, "This applicant has already been hired.", false, MessageLevel.Error);
}

// Usage:
return ResponseState<bool>.Fail(ApplicationStateCode.ApplicantAlreadyHired);

Status automatically scans such subclasses (at startup and for assemblies loaded later); Status.Name correctly resolves to "ApplicantAlreadyHired".

5. Guard clauses with StateGuard
public void UpdateSalary(Employee employee, decimal newSalary)
{
    StateGuard.ThrowIfNull(employee);
    StateGuard.ThrowIfOutOfRange(newSalary, 0, 1_000_000);
    StateGuard.ThrowIfTrue(employee.IsTerminated, StateCode.Conflict,
        "Cannot update the salary of a terminated employee.");

    employee.Salary = newSalary;
}
6. FluentValidation integration
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
    public CreateUserValidator()
    {
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
    }
}

public async Task<ResponseState<UserDto>> CreateUserAsync(CreateUserRequest request)
{
    // Throws ValidationException with field-level errors if invalid.
    await _validator.ValidateAndThrowCustom(request);

    var user = await _userRepository.CreateAsync(request);
    return ResponseState<UserDto>.Success(user);
}
7. Turning exceptions into HTTP responses (ASP.NET Core IExceptionHandler)
public class StateExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken ct)
    {
        if (exception is ValidationException validationEx)
        {
            httpContext.Response.StatusCode = validationEx.StateCode.Code;
            await httpContext.Response.WriteAsJsonAsync(new
            {
                Status = new Status(validationEx.StateCode),
                validationEx.ValidationErrors
            }, ct);
            return true;
        }

        if (exception is StateException stateEx)
        {
            httpContext.Response.StatusCode = stateEx.StateCode.Code;
            var response = ResponseState.Fail(stateEx.StateCode);
            response.Status.Message = stateEx.DetailMessage; // overridden if a custom message was set
            await httpContext.Response.WriteAsJsonAsync(response, ct);
            return true;
        }

        return false;
    }
}

Release Notes (v1.0.0)

Compared to the previous 10.x line:

  • Fixed: Status's StateCode name resolution now scans assemblies loaded after startup too, and correctly recognizes subclasses (e.g. ApplicationStateCode) — the old version could silently fall back to "UnknownStateCode" depending on assembly load order.
  • Added: Success(...)/Fail(...) static factory methods (previously documented but missing from the code).
  • Added: Full XML documentation, 15 unit tests.
  • Improved: Thread-safe cache (ConcurrentDictionary), readonly status code fields, nullable-safety fixes.
  • The public API surface remains backward-compatible.

License

This project is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 90 8/19/2026