
Security News
npm Adopts OIDC for Trusted Publishing in CI/CD Workflows
npm now supports Trusted Publishing with OIDC, enabling secure package publishing directly from CI/CD workflows without relying on long-lived tokens.
A modern functional-style operation result handling library for .NET applications. Provides a more elegant alternative to exception handling with strongly-typed Results that support chaining, mapping, binding, and other functional operations. Simplifies error flow control and improves code readability.
A modern operation result handling library designed with a functional style approach, helping developers handle various operation results more elegantly. By using the Result pattern instead of exceptions, it enables more controllable and predictable error handling processes.
dotnet add package Linger.Results
// Create success results
var success = Result.Success();
var successWithValue = Result.Success(42);
// Create failure results
var failure = Result.Failure("Operation failed");
var failureWithError = Result.Failure(new Error("ErrorCode", "Detailed error message"));
// Create not found results
var notFound = Result.NotFound();
// Define a method that returns user information
public Result<User> GetUser(int id)
{
var user = _repository.FindById(id);
if (user == null)
return Result<User>.NotFound($"User with ID {id} not found");
return Result<User>.Success(user);
}
// Elegant syntax with implicit conversion
public Result<User> GetUserWithValidation(string email)
{
// Validate email
if (string.IsNullOrEmpty(email))
{
// Directly return Result.Failure, automatically converts to Result<User>
return Result.Failure(new Error("ValidationError", "Email cannot be empty"));
}
var user = _repository.FindByEmail(email);
if (user == null)
{
// This also converts automatically
return Result.NotFound("User not found");
}
return Result<User>.Success(user);
}
// Usage example
var result = GetUser(123);
if (result.IsSuccess)
{
var user = result.Value; // Value is only accessible when result is successful
Console.WriteLine($"Found user: {user.Name}");
}
else
{
Console.WriteLine($"Error: {string.Join(", ", result.Errors.Select(e => e.Message))}");
}
// Use Match method to handle different result states
string displayName = result.Match(
user => $"User: {user.Name}",
errors => $"Error: {string.Join(", ", errors.Select(e => e.Message))}"
);
// Use TryGetValue to safely access result value
if (result.TryGetValue(out var user))
{
// Successfully obtained user
Console.WriteLine($"User: {user.Name}");
}
// Use ValueOrDefault to get value or default value
var safeUser = result.ValueOrDefault; // null when failed
// Specify default value
var userOrGuest = result.GetValueOrDefault(new User { Name = "Guest" });
// Use extension methods for method chaining
var finalResult = GetUser(123)
.Map(user => user.Email)
.Bind(email => SendEmail(email))
.Ensure(success => success, new Error("EmailError", "Email sending failed"));
// Async operations
var result = await GetUserAsync(123)
.MapAsync(async user => await GetUserPreferencesAsync(user))
.BindAsync(async prefs => await UpdatePreferencesAsync(prefs));
// Create results based on boolean conditions
public Result ValidatePassword(string password)
{
return Result.Create(password.Length >= 8)
.Ensure(() => password.Any(char.IsUpper), new Error("Password", "Password must contain uppercase letters"))
.Ensure(() => password.Any(char.IsDigit), new Error("Password", "Password must contain digits"));
}
Linger.Results provides powerful implicit conversion features to make your code more concise and elegant. It supports three types of implicit conversions:
public Result<User> CreateUser(CreateUserRequest request)
{
// Validate username
if (string.IsNullOrEmpty(request.Username))
{
// Directly return Result.Failure, automatically converts to Result<User>
return Result.Failure("Username cannot be empty");
}
// Check email format
if (!IsValidEmail(request.Email))
{
// Using custom error object, also converts automatically
return Result.Failure(new Error("Email.Invalid", "Invalid email format"));
}
// Check if user already exists
if (UserExists(request.Username))
{
// NotFound also supports implicit conversion
return Result.NotFound("Username is already taken");
}
// Success case
var user = new User { Username = request.Username, Email = request.Email };
return Result<User>.Success(user);
}
public Result ProcessUser(int userId)
{
// Get user (returns Result<User>)
Result<User> userResult = GetUser(userId);
// Automatically converts to Result, loses specific value but preserves status and error info
Result processResult = userResult;
if (processResult.IsSuccess)
{
// Execute processing logic
return Result.Success();
}
// Error information is preserved
return processResult;
}
public Result<User> GetDefaultUser()
{
var defaultUser = new User { Name = "Default User", Email = "default@example.com" };
// Object automatically converts to successful Result<User>
return defaultUser;
}
public Result<string> GetConfigValue(string key)
{
string value = _configuration[key];
// If value is null, automatically creates failure result
// If value is not null, automatically creates success result
return value; // Equivalent to Result<string>.Create(value)
}
// Demonstrates different return types that can implicitly convert to Result<User>
private Result<User> GetUserById(int id)
{
return id switch
{
1 => _testUser, // User → Result<User>
0 => Result.Success(), // Result → Result<User>
_ => Result.Failure("User not found") // Result → Result<User>
};
}
// Can freely convert between different result types
private Result ProcessUserData(Result<User> userResult)
{
// Result<User> → Result
return userResult;
}
### API Design Principles
After optimization, Linger.Results follows these design principles:
1. **Clear API Boundaries**: `Result` class focuses on non-generic operations, `Result<T>` class handles operations with return values
2. **Implicit Conversion Support**: Supports natural conversion from `Result` to `Result<T>`, but avoids unexpected value conversions
3. **Type Safety**: Ensures type correctness at compile time, avoiding runtime errors
4. **Concise Syntax**: Reduces boilerplate code and improves development efficiency
### Important Notes on Implicit Conversion
⚠️ **Important Notes**:
- `Result<T>` → `Result` conversion will **lose value information**, as non-generic Result doesn't store specific values
- In `T` → `Result<T>` conversion, if value is `null`, it automatically creates a failure result
- Accessing `.Value` property on a failed `Result<T>` will throw `InvalidOperationException`
- Recommended to use `.ValueOrDefault` or `.TryGetValue()` for safe value access
```csharp
// Correct usage examples
Result<User> userResult = GetUser(123);
// ✅ Safe value access
if (userResult.TryGetValue(out var user))
{
Console.WriteLine($"User: {user.Name}");
}
// ✅ Using default value
var safeUser = userResult.ValueOrDefault;
// ❌ Dangerous: Will throw exception if result is failed
var user = userResult.Value; // May throw InvalidOperationException
// Use Try method to catch exceptions and convert to results
var result = ResultFunctionalExtensions.Try(
() => SomeOperationThatMightThrow(),
ex => ex.ToError()
);
// Combine multiple results, succeeds only when all results succeed
var combinedResult = Result.Combine(
ValidateUsername(request.Username),
ValidateEmail(request.Email),
ValidatePassword(request.Password)
);
if (combinedResult.IsSuccess)
{
// All validations passed
return Result.Success(new User { /* ... */ });
}
// Define domain-specific error types
public static class UserErrors
{
public static readonly Error NotFound = new("User.NotFound", "User not found");
public static readonly Error InvalidCredentials = new("User.InvalidCredentials", "Invalid username or password");
public static readonly Error DuplicateEmail = new("User.DuplicateEmail", "Email is already in use");
}
// Use custom errors
public Result<User> Authenticate(string username, string password)
{
var user = _repository.FindByUsername(username);
if (user == null)
return Result.Failure(UserErrors.NotFound); // Implicit conversion to Result<User>
if (!ValidatePassword(password, user.PasswordHash))
return Result.Failure(UserErrors.InvalidCredentials); // Implicit conversion to Result<User>
return Result<User>.Success(user);
}
public async Task<Result<OrderConfirmation>> ProcessOrder(Order order)
{
// Chain process order workflow
return await ValidateOrder(order)
.BindAsync(async validOrder =>
{
// Choose different processing paths based on payment method
if (validOrder.PaymentMethod == PaymentMethod.CreditCard)
return await ProcessCreditCardPayment(validOrder);
else if (validOrder.PaymentMethod == PaymentMethod.BankTransfer)
return await ProcessBankTransfer(validOrder);
else
return Result<OrderConfirmation>.Failure("Unsupported payment method");
})
.TapAsync(async confirmation =>
{
// Execute side effect operations on success, but don't change the result
await SendConfirmationEmail(confirmation);
await UpdateInventory(order);
});
}
The default ResultStatus
enum includes Ok
, NotFound
, and Error
. You can extend this enum to include more statuses as needed:
// Create a partial class extension in your project
namespace Linger.Results
{
public enum ResultStatus
{
// Existing statuses
Ok,
NotFound,
Error,
// New custom statuses
Unauthorized,
Forbidden,
Conflict,
ValidationError
}
}
Prefer Result and Result over exceptions:
Maintain consistency in return values:
Use meaningful error codes:
Leverage implicit conversion to simplify code:
Result<T>
, you can directly return Result.Failure()
or Result.NotFound()
Use API correctly:
Result<T>.Success()
, Result<T>.Failure()
methods directlyResult
to Result<T>
instead of relying on removed forwarding methodsLeverage method chaining:
For Web APIs:
Aspect | Result Pattern | Exception Mechanism |
---|---|---|
Visibility | Explicit return type, visible at compile time | Implicit throwing, known only at runtime |
Performance | Better, no stack capture overhead | Worse, especially in high-frequency call scenarios |
Composability | Excellent, supports chaining and composition operations | Weak, requires multiple try-catch layers |
Type Safety | Strong typing, compiler assistance | Weak typing, based on string matching |
Use Cases | Business logic, expected errors | Program errors, unexpected exceptions |
MIT
FAQs
A modern functional-style operation result handling library for .NET applications. Provides a more elegant alternative to exception handling with strongly-typed Results that support chaining, mapping, binding, and other functional operations. Simplifies error flow control and improves code readability.
We found that linger.results demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
npm now supports Trusted Publishing with OIDC, enabling secure package publishing directly from CI/CD workflows without relying on long-lived tokens.
Research
/Security News
A RubyGems malware campaign used 60 malicious packages posing as automation tools to steal credentials from social media and marketing tool users.
Security News
The CNA Scorecard ranks CVE issuers by data completeness, revealing major gaps in patch info and software identifiers across thousands of vulnerabilities.