Skip to content

HBartosch/Routya.ResultKit

Repository files navigation

CI CI NuGet NuGet NuGet AspNetCore NuGet AspNetCore .NET Standard Supports Nested Validation

πŸ“¦ Routya.ResultKit

**Lightweight result wrapper, validation and transformation toolkit for C# **
Brings clean Result<T> handling and extensible validation with custom attributes.


✨ Features

  • βœ… Consistent Result<T> response pattern
  • βœ… One-line .Validate() extension for request models
  • βœ… .Transform() extension for clean and safe object/result projection
  • βœ… Rich built-in and custom validation attributes
  • βœ… Works great with System.ComponentModel.Annotations
  • βœ… Validation for nested objects

πŸ“₯ Installation

dotnet add package Routya.ResultKit --version 2.1.0

Upgrading from v1.x? See the Migration Guide below.


πŸš€ Quick Start

1. Define Your Request Model (with Custom Validation)

using Routya.ResultKit.Attributes;
using System.ComponentModel.DataAnnotations;

  private enum UserRole { Admin, User, Guest }

    private class TestModel
    {
        [Required]
        public string Name { get; set; }

        [EmailAddress]
        public string Email { get; set; }

        [Range(18, 120)]
        public int Age { get; set; }

        [StringEnum(typeof(UserRole))]
        public string Role { get; set; }

        [Required]
        public string Password { get; set; }

        [Compare("Password")]
        public string ConfirmPassword { get; set; }

        public decimal MinPurchase { get; set; }

        [GreaterThan("MinPurchase")]
        public decimal MaxPurchase { get; set; }
    }

2. Validate and Return

using Routya.ResultKit;
using Routya.ResultKit.Validation;

app.MapPost("/users", (CreateUserRequest request) =>
{
    var validationResult = request.Validate();

    if (!validationResult.Success)
        return Results.BadRequest(validationResult.Error); // Returns ProblemDetails

    var user = new User { Id = 1, Name = request.Name, Email = request.Email };
    return Results.Ok(user);
});

βœ… Successful Response Example (200 OK)

{
	"id": 1,
	"name": "Henry",
	"email": "henry@example.com"
}

❌ Validation Error Response Example (422 Unprocessable Entity)

{
	"type": "urn:problem-type:validation-error",
	"title": "Validation Failed",
	"status": 422,
	"detail": "One or more validation errors occurred.",
	"errors": {
		"name": ["The Name field is required."],
		"email": ["The Email field is not a valid e-mail address."],
		"age": ["The field Age must be between 18 and 120."],
		"role": ["Role must be one of: Admin, User, Guest"],
		"confirmPassword": ["'ConfirmPassword' and 'Password' do not match."],
		"maxPurchase": ["MaxPurchase must be greater than MinPurchase"]
	}
}

Note: v2.0 uses RFC 7807 compliant ProblemDetails for all errors.


πŸ› οΈ Built-in Validation Attributes

Routya.ResultKit includes powerful validation attributes ready to use:

Attribute Purpose
[GreaterThan("OtherProp")] Ensure a property is greater than another
[LessThan("OtherProp")] Ensure a property is less than another
[RequiredIf("OtherProp", "Value")] Conditionally require a property
[RequiredIfEmpty("OtherProp")] Require a property if another is empty
[StringEnum(typeof(EnumType))] Ensure a string matches an Enum name
[MatchRegex("pattern")] Validate a string against a regex
[MinItems(count)] Validate minimum items in a collection
[MaxItems(count)] Validate maximum items in a collection
[ValidStartEndDateRange("Start", "End")] Validate that StartDate is before EndDate (This is a class level attribute)
[ValidDateTimeOffsetRange("End")] Validate DateTimeOffset ranges
[ValidDateTimeRange("End")] Validate DateTime ranges


πŸ” Transforming Models

Use .Transform(...) to reshape validated models or result data into domain entities or response objects β€” cleanly and safely.


βœ… Example 1: Basic Object Transformation

var request = "Hello";

var greeting = request.Transform(str => new Greeting
{
    Message = str,
    Length = str.Length
});
public class Greeting
{
    public string Message { get; set; }
    public int Length { get; set; }
}

βœ… Example 2: Full Validate β†’ Transform β†’ Result Flow

var result = request.Validate()
    .Transform(req => new CreateUserCommand
    {
        Name = req.Name,
        Email = req.Email,
        Role = Enum.Parse<UserRole>(req.Role, ignoreCase: true)
    });

βœ… Example 3: Transforming Result Output

var result = Result.Ok(user)
    .Transform(u => new UserResponse
    {
        Id = u.Id,
        Name = u.Name
    });

🧠 Why Use Transform(...)?

Benefit Description
βœ… Fluent Clean chaining after .Validate()
βœ… Safe When using Result it only transforms data if result is successful
βœ… Expressive Encourages intentional mapping logic
βœ… Lightweight Zero dependencies, pure functional mapping

πŸ” Bonus: Works with Both Objects and Result

TOut Transform<TIn, TOut>(this TIn input, Func<TIn, TOut> selector)

Result<TOut> Transform<TIn, TOut>(this Result<TIn> result, Func<TIn, TOut> selector)

🌐 HTTP Status Code Support

Result carries semantic HTTP intent with automatic status code handling:

// Success status codes
Result<User>.Ok(user);              // 200 OK
Result<User>.Created(user);         // 201 Created
Result<User>.Accepted(user);        // 202 Accepted
Result<User>.NoContent();           // 204 No Content

// Redirect status codes
Result<string>.Redirect(location);           // 302 Found (temporary)
Result<string>.RedirectPermanent(location);  // 301 Moved Permanently

// Error status codes (automatic from factory methods)
Result<User>.NotFound("User not found");     // 404 Not Found
Result<User>.BadRequest("Invalid data");     // 400 Bad Request
Result<User>.Unauthorized("Not authenticated"); // 401 Unauthorized

NoContent Example - DELETE Operations

[HttpDelete("users/{id}")]
public IActionResult DeleteUser(int id)
{
    var user = _repository.FindById(id);
    if (user == null)
        return Result<User>.NotFound($"User {id} not found").ToActionResult(HttpContext);
    
    _repository.Delete(user);
    return Result<User>.NoContent().ToActionResult(HttpContext); // Returns 204
}

Redirect Examples

// Temporary redirect (302) - for moved resources
[HttpGet("docs")]
public IActionResult RedirectToDocs()
{
    return Result<string>.Redirect("https://routya.github.io/").ToActionResult(HttpContext);
}

// Permanent redirect (301) - for permanently moved endpoints
[HttpGet("old-users")]
public IActionResult OldEndpoint()
{
    var newLocation = $"{Request.Scheme}://{Request.Host}/api/users";
    return Result<string>.RedirectPermanent(newLocation).ToActionResult(HttpContext);
}

// HEAD request with NoContent
[HttpHead("users/check-email")]
public IActionResult CheckEmailExists([FromQuery] string email)
{
    var exists = _repository.EmailExists(email);
    return exists 
        ? Result<User>.NoContent().ToActionResult(HttpContext)
        : Result<User>.NotFound("Email not found").ToActionResult(HttpContext);
}

Note: When using the Routya.ResultKit.AspNetCore package, ToActionResult() and ToHttpResult() automatically use the appropriate status code from the Result. See ASP.NET Core Integration for details.


πŸ”„ Migrating from v1.x to v2.0

v2.0 introduces RFC 7807 ProblemDetails and a cleaner API. Here's what changed:

Key Changes

  1. ProblemDetails replaces old error format

    // ❌ v1.x - Simple error dictionary
    Result.Fail("Validation Failed", 400, errors)
    
    // βœ… v2.0 - RFC 7807 ProblemDetails
    Result.Fail(ProblemDetailsBuilder.ValidationError("Validation Failed")
        .WithErrors(errors)
        .Build())
  2. Result factory methods renamed

    // ❌ v1.x
    Result.Success(data)
    Result.Failure("Error", 400)
    
    // βœ… v2.0
    Result.Ok(data)
    Result.Created(data)  // New: Sets 201 status
    Result.Accepted(data) // New: Sets 202 status
    Result.NoContent()    // New in v2.1: Sets 204 status
    Result.Redirect(location)        // New in v2.1: Sets 302 status
    Result.RedirectPermanent(location) // New in v2.1: Sets 301 status
    Result.Fail(problemDetails)
  3. Validation returns ProblemDetails

    // ❌ v1.x
    var result = request.ValidateObject();
    if (!result.Success)
        return result; // Returned Result<T>
    
    // βœ… v2.0
    var result = request.Validate();
    if (!result.Success)
        return result.Error; // Returns ProblemDetails
  4. ASP.NET Core Integration (New Package)

    dotnet add package Routya.ResultKit.AspNetCore
    // Automatic conversion to IResult/IActionResult
    return result.ToActionResult(HttpContext);
    
    // Automatic exception handling
    builder.Services.AddResultKitProblemDetails();
    app.UseResultKitExceptionHandler();

Quick Migration Steps

  1. Update package: dotnet add package Routya.ResultKit --version 2.1.0
  2. Replace Result.Success β†’ Result.Ok
  3. Replace Result.Failure β†’ Result.Fail (use ProblemDetailsBuilder)
  4. Replace ValidateObject() β†’ Validate()
  5. For ASP.NET Core, add Routya.ResultKit.AspNetCore package

πŸ“– Full Migration Guide - Complete migration documentation with examples


πŸ“š Documentation & Resources


About

Lightweight result wrapper and validation toolkit for C#. Brings clean Result<T> handling and extensible validation with custom attributes.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

Contributors

Languages