-
Notifications
You must be signed in to change notification settings - Fork 0
Best Practices
Alan Barber edited this page Nov 29, 2025
·
1 revision
This page covers recommended patterns and conventions for using ResultR effectively.
Records provide immutability and value equality:
// Good: Immutable record
public record CreateUserRequest(string Email, string Name) : IRequest<User>;
// Avoid: Mutable class
public class CreateUserRequest : IRequest<User>
{
public string Email { get; set; } // Mutable
public string Name { get; set; }
}Use clear, action-oriented names:
// Good: Clear intent
public record CreateUserRequest(...) : IRequest<User>;
public record GetUserByIdRequest(...) : IRequest<User>;
public record DeactivateUserRequest(...) : IRequest<Result>;
// Avoid: Vague names
public record UserRequest(...) : IRequest<User>;
public record UserData(...) : IRequest<User>;Each request should represent a single operation:
// Good: Single responsibility
public record CreateUserRequest(string Email, string Name) : IRequest<User>;
public record UpdateUserEmailRequest(int UserId, string NewEmail) : IRequest<User>;
// Avoid: Multiple operations
public record UserRequest(
string? Email,
string? Name,
bool IsCreate, // Flag-driven behavior
bool IsUpdate) : IRequest<User>;Maintain a 1:1 relationship:
// Good: Dedicated handler
public class CreateUserHandler : IRequestHandler<CreateUserRequest, User> { }
public class GetUserHandler : IRequestHandler<GetUserRequest, User> { }
// Avoid: Multi-purpose handler (not possible in ResultR anyway)Handlers should orchestrate, not contain all logic:
// Good: Delegates to services
public class CreateUserHandler : IRequestHandler<CreateUserRequest, User>
{
private readonly IUserService _userService;
public async ValueTask<Result<User>> HandleAsync(
CreateUserRequest request,
CancellationToken cancellationToken)
{
return await _userService.CreateUserAsync(
request.Email,
request.Name,
cancellationToken);
}
}
// Avoid: All logic in handler
public class CreateUserHandler : IRequestHandler<CreateUserRequest, User>
{
public async ValueTask<Result<User>> HandleAsync(...)
{
// 200 lines of business logic, validation, database calls...
}
}Inject dependencies through the constructor:
// Good: Constructor injection
public class CreateUserHandler : IRequestHandler<CreateUserRequest, User>
{
private readonly IUserRepository _repository;
private readonly ILogger<CreateUserHandler> _logger;
public CreateUserHandler(IUserRepository repository, ILogger<CreateUserHandler> logger)
{
_repository = repository;
_logger = logger;
}
}Use ValidateAsync for input validation:
public ValueTask<Result> ValidateAsync(CreateUserRequest request)
{
if (string.IsNullOrWhiteSpace(request.Email))
return new(Result.Failure("Email is required"));
if (!request.Email.Contains('@'))
return new(Result.Failure("Invalid email format"));
return new(Result.Success());
}-
Input validation (in
ValidateAsync): Format, required fields, length limits -
Business validation (in
HandleAsync): Uniqueness, permissions, business rules
public ValueTask<Result> ValidateAsync(CreateUserRequest request)
{
// Input validation only
if (string.IsNullOrWhiteSpace(request.Email))
return new(Result.Failure("Email is required"));
return new(Result.Success());
}
public async ValueTask<Result<User>> HandleAsync(
CreateUserRequest request,
CancellationToken cancellationToken)
{
// Business validation
if (await _repository.EmailExistsAsync(request.Email, cancellationToken))
return Result<User>.Failure("Email already registered");
// Create user...
}Don't ignore results:
// Good: Check result
var result = await _dispatcher.Dispatch(request);
if (result.IsFailure)
return Result<Order>.Failure(result.Error!);
// Avoid: Ignoring result
await _dispatcher.Dispatch(request); // What if it failed?// Good: Explicit failure for expected case
var user = await _repository.GetByIdAsync(id, ct);
if (user is null)
return Result<User>.Failure($"User {id} not found");
// Avoid: Throwing for expected case
var user = await _repository.GetByIdAsync(id, ct)
?? throw new NotFoundException($"User {id} not found");// Good: Contextual error
return Result<User>.Failure($"User {request.Id} not found");
return Result<Order>.Failure($"Cannot cancel order {orderId}: already shipped");
// Avoid: Generic error
return Result<User>.Failure("Not found");
return Result<Order>.Failure("Invalid operation");src/
├── MyApp.Application/
│ ├── Users/
│ │ ├── CreateUser/
│ │ │ ├── CreateUserRequest.cs
│ │ │ └── CreateUserHandler.cs
│ │ ├── GetUser/
│ │ │ ├── GetUserRequest.cs
│ │ │ └── GetUserHandler.cs
│ │ └── UpdateUser/
│ │ ├── UpdateUserRequest.cs
│ │ └── UpdateUserHandler.cs
│ └── Orders/
│ └── ...
src/
├── MyApp.Application/
│ ├── Requests/
│ │ ├── CreateUserRequest.cs
│ │ ├── GetUserRequest.cs
│ │ └── CreateOrderRequest.cs
│ └── Handlers/
│ ├── CreateUserHandler.cs
│ ├── GetUserHandler.cs
│ └── CreateOrderHandler.cs
[Fact]
public async Task CreateUser_WithValidData_ReturnsSuccess()
{
// Arrange
var repository = new InMemoryUserRepository();
var handler = new CreateUserHandler(repository);
var request = new CreateUserRequest("test@example.com", "Test User");
// Act
var result = await handler.HandleAsync(request, CancellationToken.None);
// Assert
Assert.True(result.IsSuccess);
Assert.Equal("test@example.com", result.Value.Email);
}[Theory]
[InlineData("", "Email is required")]
[InlineData("invalid", "Invalid email format")]
public async Task Validate_WithInvalidEmail_ReturnsFailure(string email, string expectedError)
{
// Arrange
var handler = new CreateUserHandler(Mock.Of<IUserRepository>());
var request = new CreateUserRequest(email, "Test");
// Act
var result = await handler.ValidateAsync(request);
// Assert
Assert.True(result.IsFailure);
Assert.Contains(expectedError, result.Error);
}[Fact]
public async Task CreateUser_Integration_WorksEndToEnd()
{
// Arrange
var services = new ServiceCollection();
services.AddResultR(typeof(CreateUserHandler).Assembly);
services.AddScoped<IUserRepository, InMemoryUserRepository>();
var provider = services.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IDispatcher>();
// Act
var result = await dispatcher.Dispatch(
new CreateUserRequest("test@example.com", "Test"));
// Assert
Assert.True(result.IsSuccess);
}If your operation is synchronous, wrap it efficiently:
// Good: Synchronous operation wrapped efficiently
public ValueTask<Result<int>> HandleAsync(
CalculateRequest request,
CancellationToken cancellationToken)
{
var result = request.A + request.B;
return new(Result<int>.Success(result));
}
// Avoid: Unnecessary async
public async ValueTask<Result<int>> HandleAsync(
CalculateRequest request,
CancellationToken cancellationToken)
{
await Task.CompletedTask; // Unnecessary
var result = request.A + request.B;
return Result<int>.Success(result);
}Always pass the cancellation token to async operations:
public async ValueTask<Result<User>> HandleAsync(
GetUserRequest request,
CancellationToken cancellationToken)
{
// Good: Pass token
var user = await _repository.GetByIdAsync(request.Id, cancellationToken);
// Avoid: Ignoring token
var user = await _repository.GetByIdAsync(request.Id);
}| Do | Don't |
|---|---|
| Use records for requests | Use mutable classes |
| Keep handlers thin | Put all logic in handlers |
Validate early with ValidateAsync
|
Throw exceptions for validation |
| Check results immediately | Ignore results |
| Include context in errors | Use generic error messages |
Pass CancellationToken through |
Ignore cancellation |
| Test handlers directly | Only test through HTTP |
Built with ❤️ for the C# / DotNet community.