A comprehensive demonstration of the Result Pattern in .NET 10, showing how to handle errors gracefully without throwing exceptions in a real-world ASP.NET Core Minimal API.
The Result pattern is a functional programming approach that makes failures explicit in method signatures. Instead of throwing exceptions, you return a Result object that represents either success or failure, making error handling predictable and performant.
- Performance: No exception overhead for expected failures
- Explicit Error Handling: Errors are part of method signatures, making them impossible to ignore
- Predictable: Clear success/failure paths with no hidden control flow
- Clean Code: No scattered try-catch blocks throughout your application
- Composable: Results can be chained and transformed easily
- Type Safety: Compile-time guarantees about error handling
// Non-generic Result for operations without return values
Result operationResult = Result.Success();
Result failureResult = Result.Failure("Something went wrong");
// Generic Result<T> for operations that return values
Result<User> userResult = Result<User>.Success(user);
Result<User> notFoundResult = Result<User>.Failure("User not found");public async Task<Result<User>> GetUserAsync(int id)
{
if (id <= 0)
return Result<User>.Failure("Invalid user ID");
var user = await _repository.GetByIdAsync(id);
return user is not null
? Result<User>.Success(user)
: Result<User>.Failure("User not found");
}
// Using the result
var result = await GetUserAsync(123);
if (result.IsSuccess)
{
Console.WriteLine($"Found user: {result.Value.Name}");
}
else
{
Console.WriteLine($"Error: {result.Error}");
}The demo includes custom extensions to convert Results to appropriate HTTP responses:
// Converts Result<T> to HTTP responses with proper status codes
return result.ToHttpResponse(); // 200 OK or 400 Bad Request
// For operations that can return 404 Not Found
return result.ToHttpResponseWithNotFound(); // 200 OK, 400 Bad Request, or 404 Not FoundThe demo includes a complete User management API demonstrating the Result pattern in action:
-
GET
/api/users/{id}- Get user by ID- Returns 200 with user data on success
- Returns 404 when user not found
- Returns 400 for invalid ID (β€ 0)
-
POST
/api/users- Create new user- Returns 200 with created user on success
- Returns 400 for validation errors (duplicate email, invalid age)
-
PUT
/api/users/{id}- Update user- Returns 200 with updated user on success
- Returns 404 when user not found
- Returns 400 for validation errors
-
DELETE
/api/users/{id}- Delete user- Returns 200 on successful deletion
- Returns 404 when user not found
Create User Request:
{
"name": "John Doe",
"email": "john@example.com",
"age": 30
}Update User Request:
{
"name": "John Updated",
"age": 31
}User Response:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"age": 30
}Error Response:
{
"message": "Email already exists"
}-
Clone the repository
git clone <repository-url> cd result-pattern-101
-
Build and run the application
# From the root directory dotnet build dotnet run --project src/ResultPattern.API -
Access the API
- API will be available at:
https://localhost:7055 - Swagger documentation:
https://localhost:7055/openapi/v1.json - Use the provided
API.httpfile for testing requests
- API will be available at:
The project includes an API.http file with comprehensive test cases covering:
- β Success scenarios: Valid operations that work as expected
- β Failure scenarios: Invalid inputs, not found cases, and validation errors
- π Edge cases: Boundary conditions and error handling
- Create a user:
POST /api/userswith valid data - Get the user:
GET /api/users/1to retrieve the created user - Update the user:
PUT /api/users/1with new data - Try invalid operations: Test error cases like invalid IDs or duplicate emails
- Delete the user:
DELETE /api/users/1
Request:
POST /api/users
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com",
"age": 30
}Response (200 OK):
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"age": 30
}Request:
POST /api/users
Content-Type: application/json
{
"name": "Jane Smith",
"email": "john@example.com", // Duplicate email
"age": 25
}Response (400 Bad Request):
{
"message": "Email already exists"
}Request:
GET /api/users/999Response (404 Not Found):
{
"message": "User not found"
}result-pattern-101/
βββ ResultPattern.sln # Solution file
βββ global.json # .NET SDK configuration
βββ Directory.Build.props # Centralized build properties
βββ src/
βββ ResultPattern.API/
βββ ResultPattern.API.csproj # Project file (.NET 10)
βββ API.http # HTTP test requests
βββ Program.cs # Application entry point & Minimal API setup
βββ appsettings.json # Configuration
βββ appsettings.Development.json # Development configuration
βββ Common/
β βββ Result.cs # Core Result pattern implementation
βββ Extensions/
β βββ ResultExtensions.cs # HTTP response extensions
βββ Models/
β βββ ApiModels.cs # Request/Response models
βββ Services/
β βββ UserRepository.cs # Data access using Result pattern
β βββ UserService.cs # Business logic using Result pattern
βββ Properties/
βββ launchSettings.json # Launch configuration
Core Result pattern implementation featuring:
- Generic and non-generic Result types
- Implicit conversion operators
- Fluent API for creating success/failure results
HTTP response extensions that convert Results to appropriate HTTP status codes:
ToHttpResponse()- 200 OK or 400 Bad RequestToHttpResponseWithNotFound()- 200 OK, 400 Bad Request, or 404 Not Found
Business logic layer demonstrating:
- Input validation using the Result pattern
- Async operations with Result return types
- Composition of multiple Result-based operations
Data access layer showing:
- In-memory storage with Result-based operations
- Async database simulation
- Common data access patterns with Results
ASP.NET Core Minimal API setup featuring:
- Dependency injection configuration
- Route mapping with Result-to-HTTP conversion
- Clean separation of concerns
No exception overhead for expected failures like "user not found" or validation errors.
Method signatures clearly indicate what can go wrong:
Task<Result<User>> GetUserByIdAsync(int id) // Can fail
Task<Result> DeleteUserAsync(int id) // Can failNo hidden exceptions - all error paths are explicit and handled at the call site.
HTTP endpoints automatically return appropriate status codes based on Result state.
Results can be chained and transformed easily without nested try-catch blocks.
The demo showcases several advanced Result pattern techniques:
- Repository Pattern Integration: Data access layer returns Results
- Service Layer Composition: Business logic combines multiple Result operations
- HTTP Response Mapping: Automatic conversion from Results to HTTP responses
- Validation Integration: Input validation using the Result pattern
- Async/Await Support: Full async support throughout the pipeline
β
Use Results for expected failures (validation, not found, business rule violations)
β
Keep exception handling for unexpected failures (network issues, database connectivity)
β
Make error messages user-friendly and actionable
β
Use appropriate HTTP status codes based on Result state
β
Leverage implicit conversions for clean code
β
Compose Results using extension methods
The Result pattern provides a clean, performant, and predictable way to handle errors in modern .NET applications, making your code more robust and maintainable.
This project is licensed under the MIT License, which allows you to freely use, modify, and distribute the code. See the LICENSE file for full details.