A lightweight, type-safe result handling library for .NET applications that eliminates the need for exception-based error handling in business logic.
- Overview
- Why Fox.ResultKit?
- When to Use
- Comparison with Alternatives
- Features
- Installation
- Usage
- Understanding Map vs Bind
- API Reference
- Design Principles
- Requirements
- Real-World Example
- Contributing
- License
- Author
- Project Status
- Support
Fox.ResultKit provides a clean and functional approach to handle operation results in .NET applications. Instead of throwing exceptions for expected failures, it uses the Result and Result<T> types to explicitly model success and failure states.
- Zero dependencies - No third-party packages, minimal footprint
- Simple API - Just
Result<T>and extension methods, nothing more - Perfect for domain logic - Clean separation of business rules and infrastructure
- Inspired by F# Result type and Scott Wlaschin's Railway Oriented Programming pattern
- Functional composition - Chain operations with
Map,Bind,Ensure,Tap,Match - Exception safety - Wrap unsafe code with
Try/TryAsync
- Nullable reference types - Full null-safety support
- Multi-targeting - Works on .NET 8, 9, and 10
- XML documentation - Complete IntelliSense experience
- Async-first - All functional extensions have async variants
- Modeling expected failures - Validation errors, business rule violations, "not found" scenarios
- Building clean domain logic - Avoid try-catch noise in business layer
- Explicit error handling - Method signatures should reveal possible failures
- Composing operations - Chain multiple operations with functional style
- Eliminating null checks - Replace nullable return values with explicit Result
- Truly exceptional situations - Out of memory, stack overflow, hardware failures
- Simple CRUD operations - If EF Core's built-in exception handling is sufficient
- Performance-critical hot paths - Result allocation has small overhead (though minimal)
- External API integration - If you need typed error codes/HTTP status mapping (consider FluentResults)
| Feature | Fox.ResultKit | FluentResults | CSharpFunctionalExtensions | LanguageExt |
|---|---|---|---|---|
| Dependencies | 0 | 2+ | 0 | 10+ |
| Learning Curve | β Easy | ββ Moderate | ββ Moderate | βββ Steep |
| API Complexity | Simple | Feature-rich | Moderate | Complex |
| Functional Extensions | β Map, Bind, Match | β Full | β Full | β Full + Monads |
| Multiple Errors | β ErrorsResult | β List | β List | β NEL |
| Typed Errors | β String only (convention-based) | β Custom | β Custom | β Custom |
| Async Support | β Full | β Full | β Full | β Full |
| Best For | Clean domain logic | Rich validation | DDD projects | FP enthusiasts |
Fox.ResultKit's niche: If you want Railway Oriented Programming without the complexity of LanguageExt or the feature bloat of FluentResults, Fox.ResultKit is the sweet spot.
- Type-safe result handling - Explicit success/failure modeling
- Generic support -
Result<T>for operations returning values - Exception handling utilities -
Try,TryAsyncfor safe execution - Result composition - Combine multiple results
- Lightweight - Zero external dependencies
- Modern .NET - Supports .NET 8, 9, and 10
- Nullable reference types - Full nullable annotation support
- XML documentation - Complete IntelliSense support
Install via NuGet Package Manager:
dotnet add package Fox.ResultKitOr via Package Manager Console:
Install-Package Fox.ResultKitFor MediatR integration (CQRS pipeline behavior):
dotnet add package Fox.ResultKit.MediatRSee Fox.ResultKit.MediatR documentation for usage details.
using Fox.ResultKit;
// Creating results
Result success = Result.Success();
Result failure = Result.Failure("Operation failed");
// Imperative style (property-based)
if (result.IsSuccess)
{
Console.WriteLine("Success!");
}
else
{
Console.WriteLine($"Error: {result.Error}");
}
// Functional style (recommended)
result.Match(
onSuccess: () => Console.WriteLine("Success!"),
onFailure: error => Console.WriteLine($"Error: {error}")
);// Creating results with values
Result<int> success = Result<int>.Success(42);
Result<int> failure = Result<int>.Failure("Invalid input");
// Imperative style (property-based)
if (result.IsSuccess)
{
Console.WriteLine($"Value: {result.Value}");
}
else
{
Console.WriteLine($"Error: {result.Error}");
}
// Functional style (recommended)
string output = result.Match(
onSuccess: value => $"Value: {value}",
onFailure: error => $"Error: {error}"
);
Console.WriteLine(output);// Functional composition with Map, Bind, Ensure, Tap
var result = await repository.FindByIdAsync(userId)
.ToResult(ResultError.Create("USER_NOT_FOUND", $"User {userId} not found"))
.Ensure(user => user.IsActive, ResultError.Create("USER_INACTIVE", "User is not active"))
.Map(user => new UserDto(user.Id, user.Email, user.IsActive))
.Tap(dto => logger.LogInformation("Retrieved user: {Email}", dto.Email));
// Pattern matching for HTTP responses
return result.Match<IActionResult>(
onSuccess: dto => Ok(dto),
onFailure: error =>
{
var (code, message) = ResultError.Parse(error);
return code switch
{
"USER_NOT_FOUND" => NotFound(new { error = message, code }),
"USER_INACTIVE" => StatusCode(403, new { error = message, code }),
_ => BadRequest(new { error = message, code = string.IsNullOrEmpty(code) ? null : code })
};
}
);// Functional validation pipeline
private static Result ValidateEmail(string email) =>
Result.Success()
.Ensure(() => !string.IsNullOrWhiteSpace(email), "Email is required")
.Ensure(() => email.Contains("@"), "Invalid email format");
private static Result ValidatePassword(string password) =>
Result.Success()
.Ensure(() => !string.IsNullOrWhiteSpace(password), "Password is required")
.Ensure(() => password.Length >= 8, "Password must be at least 8 characters");
// Combine multiple validations
var validationResult = ResultCombineExtensions.Combine(
ValidateEmail(email),
ValidatePassword(password)
);
// Convert to Result<T> with value if all validations pass
var result = validationResult.ToResult(new CreateUserRequest(email, password));For efficient fail-fast validation chains (stops at first error):
// Lazy evaluation - password validation only runs if email succeeds
var validation = ValidateEmail(email)
.Bind(() => ValidatePassword(password))
.Bind(() => ValidateAge(age));
if (validation.IsFailure)
{
return BadRequest(new { error = validation.Error });
}
// Continue with domain logic
var result = await CreateUserAsync(email, password);// Wrap unsafe operations
var result = ResultTryExtensions.Try(() =>
int.Parse("123"),
"Failed to parse number"
);
// Async version
var result = await ResultTryExtensions.TryAsync(async () =>
await GetDataFromApiAsync(),
"Failed to fetch data"
);
// Or use in a pipeline with Bind
var parsedResult = inputString
.ToResult("Input is null")
.Bind(input => ResultTryExtensions.Try(
() => int.Parse(input),
"Invalid number format"
));// From nullable
string? value = GetValue();
Result<string> result = value.ToResult("Value was null");
// From exception
try
{
PerformOperation();
}
catch (Exception ex)
{
Result<Data> result = ResultExceptionExtensions.FromException<Data>(ex);
}public class UserService(IUserRepository repository, ILogger<UserService> logger)
{
public async Task<Result<Guid>> CreateUserAsync(string email, string password, CancellationToken cancellationToken = default)
{
return await ValidateUserInput(email, password)
.ToResult((Email: email, Password: password))
.EnsureAsync(_ => CheckEmailNotExistsAsync(email, cancellationToken), ResultError.Create("USER_EMAIL_EXISTS", "Email already exists"))
.BindAsync(credentials => CreateAndSaveUserAsync(credentials.Email, credentials.Password, cancellationToken))
.TapAsync(user => Task.Run(() => logger.LogInformation("User created: {UserId} - {Email}", user.Id, user.Email)))
.TapFailureAsync(error => Task.Run(() => logger.LogError("User creation failed: {Error}", error)))
.MapAsync(user => Task.FromResult(user.Id));
}
public Result ValidateUserInput(string email, string password)
{
return ResultCombineExtensions.Combine(
ValidateEmail(email),
ValidatePassword(password));
}
private static Result ValidateEmail(string email)
{
return Result.Success()
.Ensure(() => !string.IsNullOrWhiteSpace(email), ResultError.Create("VALIDATION_EMAIL_REQUIRED", "Email is required"))
.Ensure(() => email.Contains('@'), ResultError.Create("VALIDATION_EMAIL_FORMAT", "Invalid email format"));
}
private static Result ValidatePassword(string password)
{
return Result.Success()
.Ensure(() => !string.IsNullOrWhiteSpace(password), ResultError.Create("VALIDATION_PASSWORD_REQUIRED", "Password is required"))
.Ensure(() => password.Length >= 8, ResultError.Create("VALIDATION_PASSWORD_LENGTH", "Password must be at least 8 characters"));
}
private async Task<bool> CheckEmailNotExistsAsync(string email, CancellationToken cancellationToken)
{
return await repository.FindByEmailAsync(email, cancellationToken) == null;
}
private async Task<Result<User>> CreateAndSaveUserAsync(string email, string password, CancellationToken cancellationToken)
{
var user = new User(email, password);
await repository.AddAsync(user, cancellationToken);
return Result<User>.Success(user);
}
}
// Controller using the service
[HttpPost]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status409Conflict)]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var result = await userService.CreateUserAsync(request.Email, request.Password, cancellationToken);
return result.Match<Guid, IActionResult>(
onSuccess: userId => Ok(new { userId }),
onFailure: error =>
{
var (code, message) = ResultError.Parse(error);
return code switch
{
"USER_EMAIL_EXISTS" => Conflict(new { error = message, code }),
_ => BadRequest(new { error = message, code = string.IsNullOrEmpty(code) ? null : code })
};
}
);
}Match with non-generic Result (DELETE endpoint example):
// Service method returning Result (no value for DELETE)
public async Task<Result> DeleteUserAsync(Guid userId, CancellationToken cancellationToken)
{
var user = await repository.GetByIdAsync(userId, cancellationToken);
if (user == null)
{
return Result.Failure("USER_NOT_FOUND: User not found");
}
await repository.DeleteAsync(user, cancellationToken);
return Result.Success();
}
// Controller using Match for Result (no value)
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteUser(Guid id, CancellationToken cancellationToken)
{
return await userService.DeleteUserAsync(id, cancellationToken)
.MatchAsync<IActionResult>(
onSuccess: () => Task.FromResult(NoContent()),
onFailure: error =>
{
var (code, message) = ResultError.Parse(error);
return Task.FromResult<IActionResult>(NotFound(new { error = message, code }));
}
);
}Fox.ResultKit uses simple string-based errors for lightweight design. However, you can embed structured error codes using the convention-based ResultError utility:
Use the format: "ERROR_CODE: Error message"
using Fox.ResultKit;
// Creating structured errors
Result.Failure(ResultError.Create("USER_EMAIL_EXISTS", "Email already exists"));
Result<User>.Failure(ResultError.Create("USER_NOT_FOUND", "User does not exist"));
// Parsing structured errors
var (code, message) = ResultError.Parse("USER_NOT_FOUND: User does not exist");
// code = "USER_NOT_FOUND"
// message = "User does not exist"
// Plain errors still work (backward compatible)
var (code2, message2) = ResultError.Parse("Simple error message");
// code2 = "" (empty)
// message2 = "Simple error message"[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request, CancellationToken cancellationToken)
{
var result = await userService.CreateUserAsync(request.Email, request.Password, cancellationToken);
return result.Match<Guid, IActionResult>(
onSuccess: userId => Ok(new { userId }),
onFailure: error =>
{
var (code, message) = ResultError.Parse(error);
return code switch
{
"USER_EMAIL_EXISTS" => Conflict(new { error = message, code }),
"USER_NOT_FOUND" => NotFound(new { error = message, code }),
"VALIDATION_EMAIL_REQUIRED" or "VALIDATION_EMAIL_FORMAT" => BadRequest(new { error = message, code }),
_ => BadRequest(new { error = message, code = string.IsNullOrEmpty(code) ? null : code })
};
}
);
}private static Result ValidateEmail(string email)
{
return Result.Success()
.Ensure(() => !string.IsNullOrWhiteSpace(email),
ResultError.Create("VALIDATION_EMAIL_REQUIRED", "Email is required"))
.Ensure(() => email.Contains('@'),
ResultError.Create("VALIDATION_EMAIL_FORMAT", "Invalid email format"));
}
public async Task<Result<UserDto>> GetUserDtoAsync(Guid userId, CancellationToken cancellationToken = default)
{
return (await repository.FindByIdAsync(userId, cancellationToken))
.ToResult(ResultError.Create("USER_NOT_FOUND", $"User {userId} not found"))
.Ensure(u => u.IsActive, ResultError.Create("USER_INACTIVE", "User is not active"))
.Map(u => new UserDto(u.Id, u.Email, u.IsActive, u.CreatedAt));
}- β Zero breaking changes - Pure convention, no API modifications
- β Flexible format - Use any code format (numeric, alphanumeric, hierarchical)
- β Opt-in - Ignore if you don't need error codes
- β Lightweight - No additional dependencies or complexity
- β HTTP mapping - Easy status code selection based on error type
- β I18n support - Error codes can be mapped to localized messages
- β Monitoring - Structured error codes for logging and alerting
See WebApi.Demo for complete implementation examples.
For better UX, you can collect all validation errors at once instead of failing fast on the first error.
Supports mixed Result and Result<T> types thanks to the IResult interface:
using Fox.ResultKit;
// Validation phase - collect ALL errors (mixed Result and Result<T> supported)
var validation = ErrorsResult.Collect(
ValidateEmail(email), // Result
ValidatePassword(password), // Result
ParseAge(ageInput) // Result<int>
);
if (validation.IsFailure)
{
// All errors available at once - better UX
var errors = validation.Errors
.Select(ResultError.Parse)
.Select(e => new { e.Code, e.Message })
.ToList();
return BadRequest(new { errors });
// Response: { "errors": [
// { "code": "VALIDATION_EMAIL_REQUIRED", "message": "Email is required" },
// { "code": "VALIDATION_PASSWORD_LENGTH", "message": "Password must be at least 8 characters" },
// { "code": "VALIDATION_AGE_MINIMUM", "message": "Must be at least 18 years old" }
// ]}
}
// Domain operations - fail-fast pipeline
var result = await Result.Success()
.EnsureAsync(() => CheckEmailNotExistsAsync(email), "Email already exists")
.BindAsync(() => CreateUserAsync(email, password));Phase 1: Input Validation β ErrorsResult.Collect() (show ALL errors)
Phase 2: Domain Operations β Result pipeline (fail-fast for business logic)
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
// 1. Validation phase - collect ALL input validation errors
var validation = ErrorsResult.Collect(
ValidateEmail(request.Email),
ValidatePassword(request.Password)
);
if (validation.IsFailure)
{
var errors = validation.Errors
.Select(ResultError.Parse)
.Select(e => new { e.Code, e.Message })
.ToList();
return BadRequest(new { errors });
}
// 2. Domain pipeline - fail-fast (business logic errors)
var result = await Result.Success()
.EnsureAsync(() => CheckEmailNotExistsAsync(request.Email),
ResultError.Create("USER_EMAIL_EXISTS", "Email already exists"))
.BindAsync(() => CreateAndSaveUserAsync(request));
return result.Match(
onSuccess: userId => CreatedAtAction(nameof(GetUser), new { id = userId }, new { userId }),
onFailure: error =>
{
var (code, message) = ResultError.Parse(error);
return code == "USER_EMAIL_EXISTS"
? Conflict(new { error = message, code })
: BadRequest(new { error = message, code });
}
);
}Why separate?
- β Input validation: Better UX (show all errors at once)
- β Domain logic: Fail-fast makes sense (e.g., if email exists, don't continue)
- β Clear separation: Input vs. business logic concerns
The difference between Map and Bind is fundamental to Railway Oriented Programming:
"Transform the value with a plain function"
Result<T> β Func<T, U> β Result<U>
β
Plain value (not Result!)- Function returns a plain value (
U) - Result wrapper is automatically added
- Use for: DTO mapping, calculations, string formatting, any pure transformation
Example:
Result<int> result = Result<int>.Success(5);
Result<string> mapped = result.Map(x => $"Value: {x}");
// β returns string, NOT Result<string>
// DTO mapping
Result<UserDto> dto = userResult.Map(user => new UserDto(user.Id, user.Email));"Chain another Result-returning operation"
Result<T> β Func<T, Result<U>> β Result<U>
β
Returns Result!- Function returns a Result (
Result<U>) - Prevents nested wrapping (
Result<Result<U>>) - Use for: validation chains, repository calls, any operation that can fail
Example:
Result<User> userResult = GetUser(id);
Result<Order> orderResult = userResult.Bind(user => GetOrder(user.Id));
// β returns Result<Order>, not Order
// Validation chain (fail-fast)
var validated = ValidateEmail(email)
.Bind(() => ValidatePassword(password))
.Bind(() => CheckEmailNotExists(email));| Aspect | Map | Bind |
|---|---|---|
| Function returns | U (plain value) |
Result<U> |
| Use case | Value transformation | Result operation chaining |
| Example function | x => x * 2 |
x => Validate(x) |
| FP alias | fmap, Select | flatMap, SelectMany |
Rule of thumb: If your function returns Result, use Bind. If it returns a plain value, use Map.
| Member | Description |
|---|---|
Result.Success() |
Creates a successful result |
Result.Failure(string error) |
Creates a failed result with error message |
IsSuccess |
Returns true if operation succeeded |
IsFailure |
Returns true if operation failed |
Error |
Error message (null if success) |
ThrowIfFailure() |
Throws exception if result is failure |
| Member | Description |
|---|---|
Result<T>.Success(T value) |
Creates a successful result with value |
Result<T>.Failure(string error) |
Creates a failed result with error message |
IsSuccess |
Returns true if operation succeeded |
IsFailure |
Returns true if operation failed |
Value |
The result value (throws if failure) |
Error |
Error message (null if success) |
ThrowIfFailure() |
Throws exception if result is failure |
| Method | Description |
|---|---|
ResultError.Create(string code, string message) |
Creates structured error string in "CODE: message" format |
ResultError.Parse(string error) |
Parses error into (Code, Message) tuple |
| Member | Description |
|---|---|
IsSuccess |
Returns true if operation succeeded |
IsFailure |
Returns true if operation failed |
Error |
Error message (null if success) |
Common interface for Result and Result<T>, enabling polymorphic error collection and mixed-type scenarios.
| Member | Description |
|---|---|
ErrorsResult.Success() |
Creates successful result with no errors |
ErrorsResult.Collect(params IResult[]) |
Collects multiple results (supports mixed Result and Result<T>), aggregates all errors |
IsSuccess |
Returns true if all operations succeeded |
IsFailure |
Returns true if any operation failed |
Errors |
Read-only list of all error messages |
ToResult() |
Converts to Result with combined error message |
| Method | Description |
|---|---|
Map<T, U>(this Result<T>, Func<T, U>) |
Transforms success value with function returning plain value (DTO mapping, calculations) |
MapAsync<T, U>(this Result<T>, Func<T, Task<U>>) |
Async transform with function returning plain value |
MapAsync<T, U>(this Task<Result<T>>, Func<T, Task<U>>) |
Async result to async transform with plain value return |
| Method | Description |
|---|---|
Bind(this Result, Func<Result>) |
Chains operation returning Result (fail-fast validation, prevents nesting) |
Bind<T, U>(this Result<T>, Func<T, Result<U>>) |
Chains operation returning Result (repository calls, validation) |
BindAsync<T, U>(this Result<T>, Func<T, Task<Result<U>>>) |
Async bind with Result-returning operation |
BindAsync<T, U>(this Task<Result<T>>, Func<T, Task<Result<U>>>) |
Async result to async bind with Result return |
| Method | Description |
|---|---|
Ensure<T>(this Result<T>, Func<T, bool>, string) |
Validates result value with predicate |
Ensure(this Result, Func<bool>, string) |
Stateless validation |
EnsureAsync<T>(this Result<T>, Func<T, Task<bool>>, string) |
Async validation |
EnsureAsync<T>(this Task<Result<T>>, Func<T, Task<bool>>, string) |
Async result to async validation |
| Method | Description |
|---|---|
Tap<T>(this Result<T>, Action<T>) |
Executes action on success (logging, etc.) |
TapAsync<T>(this Result<T>, Func<T, Task>) |
Async tap on success |
TapAsync<T>(this Task<Result<T>>, Func<T, Task>) |
Async result to async tap |
TapFailure<T>(this Result<T>, Action<string>) |
Executes action on failure |
TapFailureAsync<T>(this Result<T>, Func<string, Task>) |
Async tap on failure |
TapFailureAsync<T>(this Task<Result<T>>, Func<string, Task>) |
Async result to async tap failure |
| Method | Description |
|---|---|
Match<T, U>(this Result<T>, Func<T, U>, Func<string, U>) |
Handles both success and failure cases |
Match<U>(this Result, Func<U>, Func<string, U>) |
Pattern matching for non-generic Result |
MatchAsync<T, U>(this Task<Result<T>>, Func<T, Task<U>>, Func<string, Task<U>>) |
Async pattern matching for Result |
MatchAsync<U>(this Task<Result>, Func<Task<U>>, Func<string, Task<U>>) |
Async pattern matching for Result |
| Method | Description |
|---|---|
ToResult<T>(this Result<T>) |
Converts Result<T> to Result (discards value) |
ToResult<T>(this Result, T value) |
Converts Result to Result<T> with value |
ToResult<T>(this T? value, string errorIfNull) |
Converts nullable to Result<T> |
| Method | Description |
|---|---|
Combine(params Result[]) |
Combines multiple results, fails on first error |
Combine<T>(T value, params Result[]) |
Combines results and returns value if all succeed |
Combine<T>(params Result<T>[]) |
Combines generic results, returns last value |
| Method | Description |
|---|---|
Try<T>(Func<T>, string error) |
Wraps function execution, catches exceptions |
TryAsync<T>(Func<Task<T>>, string error) |
Async version of Try |
FromException<T>(Exception ex) |
Converts exception to Result<T> |
FromException<T>(Exception ex, bool includeInner, bool includeStack) |
Detailed exception conversion |
| Method | Description |
|---|---|
ResultError.Create(string code, string message) |
Creates formatted error string "CODE: message" |
ResultError.Parse(string error) |
Parses error into (Code, Message) tuple |
Convention format: "ERROR_CODE: Error message"
Example: ResultError.Create("USER_NOT_FOUND", "User does not exist") β "USER_NOT_FOUND: User does not exist"
See Error Code Convention section for usage examples.
- Explicit over implicit - Make success and failure explicit in the type system
- Railway-oriented programming - Enable fluent result composition
- Zero overhead - No external dependencies, minimal allocations
- Developer-friendly - Clear API, excellent IntelliSense support
- .NET 8.0 or higher
- C# 11 or higher (for file-scoped namespaces and modern features)
- Nullable reference types enabled (recommended)
See this package in action within a complete production-grade application: Fox.TaskFlow - A comprehensive demonstration showcasing real-world integration of seven Fox.*Kit packages in a task management system built with Clean Architecture, SOLID principles, and modern .NET 10 practices.
Fox.ResultKit is intentionally lightweight and feature-focused. The goal is to remain a simple, zero-dependency library for Railway Oriented Programming.
- β Bug fixes - Issues with existing functionality
- β Documentation improvements - Clarifications, examples, typo fixes
- β Performance optimizations - Without breaking API compatibility
- β New dependencies or third-party packages
- β Large feature additions that increase complexity
- β Breaking API changes
If you want to propose a significant change, please open an issue first to discuss whether it aligns with the project's philosophy.
The project enforces a strict build policy to ensure code quality:
- β No errors allowed - Build must be error-free
- β No warnings allowed - All compiler warnings must be resolved
- β No messages allowed - Informational messages must be suppressed or addressed
All pull requests must pass this requirement.
Fox.ValidationKit follows strict coding standards:
- Comprehensive unit tests required (xUnit + FluentAssertions)
- Maximum test coverage required - Aim for 100% line and branch coverage. Tests may only be omitted if they would introduce artificial complexity (e.g., testing unreachable code paths, framework internals, or compiler-generated code). Use
[ExcludeFromCodeCoverage]sparingly and only for justified cases. - XML documentation for all public APIs - Clear, concise documentation with examples
- Follow Microsoft coding conventions - See
.github/copilot-instructions.mdfor project-specific style - Zero warnings, zero errors build policy - Strict enforcement
- Follow the existing code style (see
.github/copilot-instructions.md) - Use file-scoped namespaces
- Enable nullable reference types
- Add XML documentation for public APIs
- Write unit tests for new features
- Fork the repository
- Create a feature branch from
main - Follow the coding standards in
.github/copilot-instructions.md - Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE.txt file for details.
KΓ‘roly AkΓ‘cz
- GitHub: @akikari
- Repository: Fox.ResultKit
See CHANGELOG.md for version history.
For issues, questions, or feature requests, please open an issue in the GitHub repository.