
Error handling is a crucial part of any software application. Traditionally, developers rely on exceptions to manage errors, but this often leads to cluttered code and performance overhead when used for control flow. The Result Pattern provides a structured way to return success or failure in .NET applications, making error handling explicit, predictable, and maintainable. By using a Result<T> type, developers can avoid unexpected exceptions, improve readability, and write more robust software. In this article, we’ll walk through a real-world business scenario where this pattern proves invaluable.
Business Scenario — Online Payment Processing
Imagine you’re building an e-commerce platform where users can place orders and make payments. When processing a payment, multiple things can go wrong:
- The credit card number is invalid
- The customer’s account has insufficient funds
- The payment gateway is temporarily unavailable
Instead of throwing exceptions for these cases, we can use the Result Pattern to handle success and failure in a clean, structured manner.
Implementing the Result Pattern — Handling Payment Failures and Success with Logging
This example demonstrates the Result Pattern combined with logging in an e-commerce payment system. The Result<T> class encapsulates success or failure outcomes, while logging tracks key events, such as payment validation, success, or errors. The PaymentService handles payment logic and validation, and the OrderController orchestrates the payment process, logging results for better traceability and easier debugging.
1️⃣ Set up Logging in ASP.NET Core
This code defines a .NET console application entry point using the Host class to configure and run the application.
The application uses dependency injection to manage services like logging and the two custom services (PaymentService and OrderController).
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddLogging();
services.AddSingleton<PaymentService>(); // Register PaymentService
services.AddSingleton<OrderController>(); // Register OrderController
});
}
Main: The entry point method that creates the host, builds it, and starts it by calling.Run().CreateHostBuilder: A static method that configures the host for the application. It usesHost.CreateDefaultBuilder()to set up default services and configurations.- Inside
ConfigureServices: The method configures services for dependency injection: AddLogging(): Adds logging services so that the application can log messages.AddSingleton<PaymentService>(): Registers thePaymentServiceas a singleton, meaning only one instance ofPaymentServicewill exist throughout the application’s lifetime.AddSingleton<OrderController>(): Registers theOrderControlleras a singleton, ensuring only one instance ofOrderControlleris used.
2️⃣ Defining the Result Pattern
This code defines a Result<T> class, which is used to encapsulate the outcome of an operation, including whether it was successful or not.
This pattern helps manage the success or failure of operations in a clean and type-safe manner.
public class Result<T>
{
public bool IsSuccess { get; private set; }
public bool IsFailure => !IsSuccess;
public T Value { get; private set; }
public string Error { get; private set; }
private Result(bool isSuccess, T value, string error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
public static Result<T> Success(T value) => new Result<T>(true, value, null);
public static Result<T> Failure(string error) => new Result<T>(false, default(T), error);
}
IsSuccess: A boolean that indicates if the operation was successful.IsFailure: A boolean that is the inverse ofIsSuccess.Value: The result data (of typeT) returned if the operation was successful.Error: A string containing an error message if the operation failed.
The class includes two static methods:
Success(T value): Creates aResultindicating success, with the provided value.Failure(string error): Creates aResultindicating failure, with the provided error message.
3️⃣ PaymentService with Logging
This PaymentService class handles payment processing. It validates the credit card number and amount, logging warnings if they’re invalid. If the inputs are valid, it simulates a payment gateway call.
public class PaymentService
{
private readonly ILogger<PaymentService> _logger;
public PaymentService(ILogger<PaymentService> logger)
{
_logger = logger;
}
public Result<string> ProcessPayment(string creditCardNumber, decimal amount)
{
if (string.IsNullOrWhiteSpace(creditCardNumber) || creditCardNumber.Length < 12)
{
_logger.LogWarning("Payment attempt failed: Invalid credit card number.");
return Result<string>.Failure("Invalid credit card number.");
}
if (amount <= 0)
{
_logger.LogWarning("Payment attempt failed: Invalid payment amount.");
return Result<string>.Failure("Invalid payment amount.");
}
// Simulate external API call (payment gateway)
bool paymentSuccess = new Random().Next(0, 2) == 1;
if (paymentSuccess)
{
_logger.LogInformation($"Payment processed successfully for card: {creditCardNumber.Substring(0, 4)}xxxx");
return Result<string>.Success("Payment processed successfully.");
}
else
{
_logger.LogError("Payment gateway is currently unavailable.");
return Result<string>.Failure("Payment gateway is currently unavailable.");
}
}
}
The method returns a Result<string>:
- On success, it logs a successful payment and returns a success result.
- On failure (either due to invalid inputs or a simulated payment gateway error), it logs the issue and returns a failure result with an appropriate error message.
4️⃣ OrderController with Logging
The OrderController class manages the checkout process. It uses the PaymentService to process payments and logs the flow.
public class OrderController
{
private readonly PaymentService _paymentService;
private readonly ILogger<OrderController> _logger;
public OrderController(PaymentService paymentService, ILogger<OrderController> logger)
{
_paymentService = paymentService;
_logger = logger;
}
public void Checkout(string creditCardNumber, decimal amount)
{
_logger.LogInformation("Starting payment process...");
var paymentResult = _paymentService.ProcessPayment(creditCardNumber, amount);
if (paymentResult.IsSuccess)
{
_logger.LogInformation("Payment was successful.");
Console.WriteLine(paymentResult.Value); // Display success message
}
else
{
_logger.LogError($"Payment failed: {paymentResult.Error}");
Console.WriteLine($"Payment failed: {paymentResult.Error}"); // Display error message
}
}
}
- It logs when the payment process starts.
- It calls
ProcessPaymentto validate and process the payment. - If successful, it logs the success and displays the success message.
- If the payment fails, it logs the error and displays the failure message.
5️⃣ Testing the Payment Process
This Program class sets up and runs the application.
It demonstrates how the payment process works and handles various outcomes based on the input values.
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
var controller = host.Services.GetRequiredService<OrderController>();
// Test successful payment
controller.Checkout("123456789012", 150.00m); // Valid card and amount
// Test payment failure due to invalid credit card number
controller.Checkout("123", 150.00m); // Invalid card number
// Test payment failure due to invalid amount
controller.Checkout("123456789012", -50.00m); // Invalid amount
}
}
- Builds the host and retrieves the
OrderControllerfrom the dependency injection container. - Tests the
Checkoutmethod with different payment scenarios: - A successful payment with a valid card and amount.
- A failed payment due to an invalid credit card number.
- A failed payment due to an invalid amount.
6️⃣ Logging Output Example
When running the above example, the logging output would look something like this:
[Information] Starting payment process...
[Information] Payment processed successfully for card: 1234xxxx
[Information] Payment was successful.
[Warning] Payment attempt failed: Invalid credit card number.
[Information] Starting payment process...
[Error] Payment failed: Invalid credit card number
[Warning] Payment attempt failed: Invalid payment amount.
[Information] Starting payment process...
[Error] Payment failed: Invalid payment amount
Benefits of using the Result Pattern
- Eliminates exception-based control flow: Instead of relying on costly exceptions, failures are handled gracefully.
- Improves readability: Developers can instantly see if a method succeeds or fails by checking
IsSuccess. - Encourages explicit error handling: Since failures are expected, they must be handled properly instead of being ignored.
- Makes code more testable: The pattern encourages returning results instead of side effects, making unit tests easier to write.
The Result Pattern is a simple yet powerful way to improve error handling in .NET applications. By encapsulating both success and failure, we create more robust, maintainable, and testable code. Whether you’re building payment systems, authentication flows, or external API integrations, this pattern helps manage errors effectively without relying on exceptions.