ARPL 1.0.7
See the version list below for details.
dotnet add package ARPL --version 1.0.7
NuGet\Install-Package ARPL -Version 1.0.7
<PackageReference Include="ARPL" Version="1.0.7" />
<PackageVersion Include="ARPL" Version="1.0.7" />
<PackageReference Include="ARPL" />
paket add ARPL --version 1.0.7
#r "nuget: ARPL, 1.0.7"
#addin nuget:?package=ARPL&version=1.0.7
#tool nuget:?package=ARPL&version=1.0.7
ARPL (Advanced Result Pattern Library)
A lightweight C# library providing robust discriminated unions for error handling and functional programming patterns. ARPL offers two main types: Either<L,R>
for generic discriminated unions and SResult<R>
for specialized success/error handling.
If you find ARPL helpful, please give it a star โญ! It helps the project grow and improve.
Why ARPL? ๐ค
- โจ Type-safe error handling without exceptions
- ๐ Rich functional methods for composing operations
- ๐ฏ Explicit error cases in method signatures
- ๐ฆ Collection of errors support out of the box
- ๐ Chainable operations with fluent API
- ๐งช Testable code with predictable flows
Table of Contents ๐
- The Result Pattern
- Features
- Getting Started
- Error Handling
- Bespoke Errors
- Implicit Conversions
- StaticFactory Helpers
- Type Features
- Functional Methods
- Best Practices
- Demo Application
- Benchmarking
- Contributing
- License
The Result Pattern ๐ฏ
The Result Pattern is an elegant alternative to traditional exception handling that makes error cases explicit in your code. Instead of throwing exceptions that can be caught anywhere in the call stack, methods return a Result type that can represent either success or failure.
Traditional Exception Handling
public User CreateUser(string email, string password)
{
try
{
// Validate email
if (string.IsNullOrEmpty(email))
throw new ValidationException("Email is required");
if (!IsValidEmail(email))
throw new ValidationException("Invalid email format");
// Validate password
if (string.IsNullOrEmpty(password))
throw new ValidationException("Password is required");
// Create and save user
var user = new User(email, password);
await _repository.Save(user);
return user;
}
catch (ValidationException ex)
{
_logger.LogWarning(ex, "Validation failed when creating user");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error creating user");
throw;
}
}
Using ARPL's Result Pattern
public SResult<User> CreateUser(string email, string password)
{
// Validate inputs
if (string.IsNullOrEmpty(email))
return Fail<User>(Errors.New("Email is required"));
if (string.IsNullOrEmpty(password))
return Fail<User>(Errors.New("Password is required"));
if (!IsValidEmail(email))
return Fail<User>(Errors.New("Invalid email format"));
// Create and save user
try
{
var user = new User(email, password);
_repository.Save(user);
return Success(user);
}
catch (Exception ex)
{
return Fail<User>(Errors.New(ex, "Failed to create user")); //Unexpected Error
}
}
Benefits of the Result Pattern
Explicit Error Handling: Error cases are part of the method signature, making it clear that a method can fail and forcing error handling at compile time.
Type Safety: The compiler ensures you handle both success and error cases through pattern matching, preventing runtime surprises.
No Exceptions: Better performance by avoiding the overhead of exception handling and stack traces. Exceptions are reserved for truly exceptional cases.
Composable: Easy to chain operations using functional methods like
Map
andBind
, making the code more readable and maintainable.Rich Error Types: Built-in support for different error types and error collections, allowing for more granular error handling.
Predictable Flow: All possible outcomes are clearly defined and handled in a structured way, making the code easier to reason about.
Better Testing: Easier to test error cases since they're explicit in the type system rather than relying on exception handling.
Features ๐
- Either<L,R> - A generic discriminated union that can hold one of two possible types
- SResult<R> - A specialized result type for handling success/error scenarios
- Implicit conversions between
Either<Error,R>
andSResult<R>
- Pattern matching support for elegant value handling
- Type-safe error handling without exceptions
- Functional programming friendly design
Getting Started ๐
Installation
Install via NuGet:
Install-Package ARPL
Basic Usage
Either<L,R>
A generic discriminated union that can hold one of two possible types:
// Create Either instances
var right = Either<string, int>.Right(42); // Success path
var left = Either<string, int>.Left("error"); // Error path
// Check which value is present
if (right.IsRight)
Console.WriteLine($"Value: {right.RightValue}"); // 42
if (left.IsLeft)
Console.WriteLine($"Value: {left.LeftValue}"); // error
// Pattern match to handle both cases
var message = left.Match(
left => $"Error: {left}",
right => $"Value: {right}");
SResult<R>
A specialized result type for success/error scenarios:
// Create results
var success = SResult<int>.Success(42); // Success case
var error = SResult<int>.Fail(Errors.New("Invalid input")); // Error case
// Check result type
if (success.IsSuccess)
Console.WriteLine($"Value: {success.SuccessValue}");
else
Console.WriteLine($"Error: {success.ErrorValue.Message}");
// Pattern match for handling
var message = error.Match(
error => $"Failed: {error.Message}",
value => $"Success: {value}");
Error
A rich error type with support for messages, codes, and chaining:
// Create errors
var simple = Errors.New("Something went wrong");
var coded = Errors.New("Invalid input", "INVALID_INPUT");
var chained = simple + coded;
// Access error details
Console.WriteLine(simple.Message); // "Something went wrong"
Console.WriteLine(coded.Code); // "INVALID_INPUT"
Console.WriteLine(chained.Message) // ["Something went wrong","Invalid input"]
Error Handling
ARPL provides a flexible error handling system that allows you to work with both single errors and collections of errors. The Error
class serves as the base for all error types, and the ErrorCollection
allows you to aggregate multiple errors together.
Single Errors
// Create a simple error
var error = Errors.New("Invalid input", "ERR001");
// Create an unexpected error from an exception
var unexpectedError = Errors.New(new Exception("Database connection failed"));
// Check error types
if (error.HasErrorOf<ExpectedError>())
Console.WriteLine("This is an expected error");
// Check exception types
if (unexpectedError.HasExceptionOf<DbException>())
Console.WriteLine("This is a database error");
Multiple Errors
When you need to collect and combine multiple errors, use ErrorCollection
:
// Start with an empty error collection
var errors = Errors.EmptyError();
// Add errors as they are found
errors.Add(Errors.New("Invalid email", "VAL001"));
errors.Add(Errors.New("Password too short", "VAL002"));
// You can also combine errors using the + operator
var error1 = Errors.New("Field required", "VAL003");
var error2 = Errors.New("Invalid format", "VAL004");
var combined = error1 + error2; // Implicit creates a new ErrorCollection
combined += Errors.New("Missing argument", "VAL005");
// Enumerate through errors
foreach (var error in combined.AsEnumerable())
{
Console.WriteLine($"{error.Code}: {error.Message}");
}
// Get error count
Console.WriteLine($"Total errors: {combined.Count}"); // 3
// Check if collection has specific error
var hasValidationError = combined.AsEnumerable().Any(e => e.Code.StartsWith("VAL"));
// Use in result types
return SResult<User>.Error(errors); // Works with both single Error and ErrorCollection
Bespoke Errors
ARPL allows you to create custom error types by extending the Error
class. This enables you to create domain-specific errors that carry meaningful context for your application:
public record NotFoundError : Error
{
public NotFoundError(string entityType, string identifier)
{
EntityType = entityType;
Identifier = identifier;
}
public string EntityType { get; }
public string Identifier { get; }
public override string Message => $"{EntityType} with id {Identifier} was not found";
public override bool IsExpected => true;
}
// Usage example:
public async Task<SResult<User>> GetUserById(string userId)
{
var user = await _repository.FindUserById(userId);
if (user == null)
return SResult<User>.Error(new NotFoundError("User", userId));
return SResult<User>.Success(user);
}
// Pattern matching with custom error
var result = await GetUserById("123");
var message = result.Match(
fail => fail is NotFoundError nf
? $"Could not find {nf.EntityType} {nf.Identifier}"
: "Unknown error",
success => $"Found user: {success.Name}"
);
Bespoke errors provide several benefits:
- Type-safe error handling with pattern matching
- Rich error context specific to your domain
- Clear distinction between expected and unexpected errors
- Consistent error handling across your application
Implicit Conversions
ARPL supports implicit conversions between Either<Error,R>
and SResult<R>
, making it seamless to work with both types:
// Convert from Either to SResult
Either<Error, int> either = Either<Error, int>.Right(42);
SResult<int> result = either; // Implicit conversion
// Convert from SResult to Either
SResult<int> sresult = SResult<int>.Success(42);
Either<Error, int> converted = sresult; // Implicit conversion
Note: The implicit conversion only works for
Either<Error, R>
andSResult<R>
. Attempting to convert other types will throw an exception.
StaticFactory Helpers
For a more functional and concise style, ARPL provides the StaticFactory
class, which offers utility methods to create instances of SResult
and Either
in a direct and expressive way:
using static Arpl.Core.StaticFactory;
// Create a success result
var success = Success(42); // SResult<int>
// Create a failure result
var fail = Fail<int>(new Error("fail")); // SResult<int>
// Create an Either with a left value
var left = Left<string, int>("error"); // Either<string, int>
// Create an Either with a right value
var right = Right<string, int>(42); // Either<string, int>
Available Methods
Success<T>(T value)
: Creates a successfulSResult<T>
.Fail<T>(Error value)
: Creates a failedSResult<T>
.Left<L, R>(L value)
: Creates anEither<L, R>
with a left value.Right<L, R>(R value)
: Creates anEither<L, R>
with a right value.
These methods make it easier to create values for functional flows and tests, making your code cleaner and more readable.
Type Features
Either<L,R>
Left(L value)
- Creates a new Either instance containing a left valueRight(R value)
- Creates a new Either instance containing a right valueIsLeft
- Indicates if the instance contains a left valueIsRight
- Indicates if the instance contains a right valueLeftValue
- Gets the left value (if present)RightValue
- Gets the right value (if present)Match
- Pattern matching for transforming or handling the contained valueMatchAsync
- Asynchronous pattern matching for handling the contained valueMap
- Transforms the right value using a mapping function (if present)MapAsync
- Transforms the right value using an async mapping function (if present)Bind
- Chains operations that return Either (monadic bind)BindAsync
- Asynchronously chains operations that return EitherApply
- Transforms both left and right values into a new EitherApplyAsync
- Asynchronously transforms both left and right values into a new EitherSequence
- Transforms a collection of Either into an Either of collectionSequenceAsync
- Asynchronously transforms a collection of Either into an Either of collectionTraverse
- Maps and sequences in one stepTraverseAsync
- Asynchronously maps and sequences in one step
SResult<R>
Success(R value)
- Creates a new success resultError(Error value)
- Creates a new error resultIsSuccess
- Indicates if the result represents successIsFail
- Indicates if the result represents an errorSuccessValue
- Gets the success valueErrorValue
- Gets the error valueMatch
- Pattern matching for transforming or handling the resultMatchAsync
- Asynchronous pattern matching for handling the resultMap
- Transforms the success value using a mapping function (if present)MapAsync
- Transforms the success value using an async mapping function (if present)Bind
- Chains operations that return SResult (monadic bind)BindAsync
- Asynchronously chains operations that return SResultApply
- Transforms both error and success values into a new SResultApplyAsync
- Asynchronously transforms both error and success values into a new SResultSequence
- Transforms a collection of SResult into an SResult of collectionSequenceAsync
- Asynchronously transforms a collection of SResult into an SResult of collectionTraverse
- Maps and sequences in one stepTraverseAsync
- Asynchronously maps and sequences in one step
Error
New(string message)
- Creates a new expected error with a messageNew(string message, string code)
- Creates a new expected error with a message and codeNew(Exception ex)
- Creates a new unexpected error from an exceptionNew(Exception ex, string message, string code)
- Creates a new unexpected error with a message and a codeMessage
- Gets the error messageCode
- Gets the error code (if present)Exception
- Gets the exception (if present)IsExpected
- Indicates if the error was expectedHasErrorOf<T>()
- Checks if the error is of type THasExceptionOf<T>()
- Checks if the error's exception is of type T
Functional Methods ๐งฎ
ARPL provides a rich set of functional methods to compose and transform values:
Map & MapAsync
Transform the success/right value while preserving the context:
// Map a successful value
SResult<int> result = SResult<int>.Success(42);
var doubled = result.Map(x => x * 2); // Success(84)
// Map with Either
Either<Error, int> either = Either<Error, int>.Right(42);
var doubled = either.Map(x => x * 2); // Right(84)
// Async mapping
var asyncResult = await result.MapAsync(async x => {
await Task.Delay(100); // Simulate async work
return x * 2;
});
Bind & BindAsync
Chain operations that might fail:
// Simple validation chain
SResult<int> Parse(string input) =>
int.TryParse(input, out var number)
? Success(number)
: Fail<int>(Errors.New("Invalid number"));
SResult<int> Validate(int number) =>
number > 0
? Success(number)
: Fail<int>(Errors.New("Number must be positive"));
// Chain operations with Bind
var result = Parse("42")
.Bind(Validate)
.Map(x => x * 2); // Success(84)
Match & MatchAsync
Pattern match to handle both success and error cases:
// Handle validation result
var result = ValidateAge(age);
var message = result.Match(
error => $"Invalid age: {error.Message}",
age => $"Age {age} is valid");
// Format API response
var apiResult = await GetUserAsync(id);
var response = apiResult.Match(
error => new ErrorResponse { Code = error.Code, Message = error.Message },
user => new UserResponse { Id = user.Id, Name = user.Name });
// With Either for custom error handling
var parseResult = TryParseJson<UserData>(json);
var data = parseResult.Match(
error => new UserData { IsValid = false, Error = error },
success => success with { IsValid = true });
Sequence & SequenceAsync
Transform a collection of results into a result of collection:
// Sequence a list of results
var results = new[] {
SResult<int>.Success(1),
SResult<int>.Success(2),
SResult<int>.Success(3)
};
var combined = results.Sequence(); // Success([1,2,3])
// If any fails, the whole operation fails
var mixed = new[] {
SResult<int>.Success(1),
SResult<int>.Fail(Errors.New("Oops")),
SResult<int>.Success(3)
};
var failed = mixed.Sequence(); // Fail("Oops")
Traverse & TraverseAsync
Map and sequence in one step:
// Parse a list of strings into numbers
var strings = new[] { "1", "2", "3" };
var numbers = strings.Traverse(str =>
int.TryParse(str, out var num)
? Success(num)
: Fail<int>(Errors.New($"Invalid number: {str}")));
// Async traversal
var urls = new[] { "url1", "url2" };
var contents = await urls.TraverseAsync(async url => {
try {
var content = await httpClient.GetStringAsync(url);
return Success(content);
}
catch (Exception ex) {
return Fail<string>(Errors.New(ex, $"Failed to fetch {url}"));
}
});
Apply & ApplyAsync
Transform both success and error cases:
// Convert errors to user-friendly messages
var result = SResult<int>.Fail(Errors.New("INVALID_INPUT"));
var friendly = result.Apply(
error => SResult<string>.Success($"Please try again: {error.Message}"),
value => SResult<string>.Success($"Your number is {value}"));
// With Either for custom error handling
var either = Either<int, string>.Left(404);
var handled = either.Apply(
status => Either<string, string>.Right($"Error {status}"),
content => Either<string, string>.Right($"Content: {content}"));
Best Practices
- Use
Either<L,R>
when you need a generic discriminated union - Use
SResult<R>
for specific success/error handling scenarios - Leverage pattern matching with
Match
for clean and safe value handling - Prefer using the type system for error handling instead of exceptions
Anti-Patterns to Avoid
- โ Don't mix exceptions with Results
// Bad
public SResult<User> GetUser(int id)
{
if (id <= 0)
throw new ArgumentException("Invalid id"); // Don't throw!
var user = _repository.GetById(id);
return user == null
? Fail<User>(Errors.New("User not found"))
: Success(user);
}
// Good
public SResult<User> GetUser(int id)
{
if (id <= 0)
return Fail<User>(Errors.New("Invalid id"));
var user = _repository.GetById(id);
return user == null
? Fail<User>(Errors.New("User not found"))
: Success(user);
}
- โ Don't ignore the Result value
// Bad
await CreateUser(request); // Result ignored!
// Good
var result = await CreateUser(request);
if (result.IsFail)
_logger.LogError("Failed to create user: {Errors}", result.ErrorValue);
- โ Don't use Result for expected flow control
// Bad - using Result for normal flow
public SResult<decimal> GetDiscount(User user)
{
return user.IsPremium
? Success(0.1m)
: Success(0m);
}
// Good - use normal return
public decimal GetDiscount(User user)
{
return user.IsPremium ? 0.1m : 0m;
}
Demo Application
The repository includes a sample Web API project that demonstrates how to use ARPL in a real-world scenario. The demo implements a simple Person management API with proper error handling and functional programming patterns.
Features
- CRUD operations for Person entity
- Validation using Either<ValidateError, T>
- Error handling with SResult<T>
- HTTP response handling with custom HttpResult
Running the Demo
- Navigate to the sample directory:
cd sample/SampleWebApi
- Run the application:
dotnet run
- Open your browser at:
- API: http://localhost:5297
- Swagger UI: http://localhost:5297/swagger
API Endpoints
- GET /api/person - List all persons
- GET /api/person/{id} - Get person by id
- POST /api/person - Create new person
Example Request
curl -X POST http://localhost:5297/api/person \
-H "Content-Type: application/json" \
-d '{"name":"John Doe","age":30}'
Benchmarking
Feature | ARPL | FluentResults | OneOf | ErrorOr |
---|---|---|---|---|
Generic Discriminated Union | โ
Either<L,R> |
โ | โ | โ |
Result Type | โ
SResult<R> |
โ | โ | โ |
Multiple Errors | โ | โ | โ | โ |
Functional Methods | โ | โ | โ | โ |
Async Support | โ | โ | โ | โ |
Pattern Matching | โ | โ | โ | โ |
Implicit Conversions | โ | โ | โ | โ |
No Dependencies | โ | โ | โ | โ |
ARPL combines the best of worlds:
- Generic discriminated unions like OneOf
- Rich error handling like FluentResults/ErrorOr
- Full functional programming support
- Seamless async/await integration
Contributing ๐ค
Contributions are welcome! Feel free to submit issues and pull requests.
License ๐
This project is licensed under the MIT License - see the LICENSE file for details.
Disclaimer: This README was generated by Windsurf AI.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net6.0
- No dependencies.
-
net7.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ARPL:
Package | Downloads |
---|---|
ARPL.AspNetCore
A lightweight C# library providing robust discriminated unions for error handling and functional programming patterns. ARPL offers two main types: Either<L,R> for generic discriminated unions and SResult<R> for specialized success/error handling. |
GitHub repositories
This package is not used by any popular GitHub repositories.