# Best Practices This page covers recommended patterns and conventions for using ResultR effectively. ## Request Design ### Use Records for Requests Records provide immutability and value equality: ```csharp // Good: Immutable record public record CreateUserRequest(string Email, string Name) : IRequest; // Avoid: Mutable class public class CreateUserRequest : IRequest { public string Email { get; set; } // Mutable public string Name { get; set; } } ``` ### Name Requests by Intent Use clear, action-oriented names: ```csharp // Good: Clear intent public record CreateUserRequest(...) : IRequest; public record GetUserByIdRequest(...) : IRequest; public record DeactivateUserRequest(...) : IRequest; // Avoid: Vague names public record UserRequest(...) : IRequest; public record UserData(...) : IRequest; ``` ### Keep Requests Focused Each request should represent a single operation: ```csharp // Good: Single responsibility public record CreateUserRequest(string Email, string Name) : IRequest; public record UpdateUserEmailRequest(int UserId, string NewEmail) : IRequest; // Avoid: Multiple operations public record UserRequest( string? Email, string? Name, bool IsCreate, // Flag-driven behavior bool IsUpdate) : IRequest; ``` ## Handler Design ### One Handler Per Request Maintain a 1:1 relationship: ```csharp // Good: Dedicated handler public class CreateUserHandler : IRequestHandler { } public class GetUserHandler : IRequestHandler { } // Avoid: Multi-purpose handler (not possible in ResultR anyway) ``` ### Keep Handlers Thin Handlers should orchestrate, not contain all logic: ```csharp // Good: Delegates to services public class CreateUserHandler : IRequestHandler { private readonly IUserService _userService; public async ValueTask> HandleAsync( CreateUserRequest request, CancellationToken cancellationToken) { return await _userService.CreateUserAsync( request.Email, request.Name, cancellationToken); } } // Avoid: All logic in handler public class CreateUserHandler : IRequestHandler { public async ValueTask> HandleAsync(...) { // 200 lines of business logic, validation, database calls... } } ``` ### Use Constructor Injection Inject dependencies through the constructor: ```csharp // Good: Constructor injection public class CreateUserHandler : IRequestHandler { private readonly IUserRepository _repository; private readonly ILogger _logger; public CreateUserHandler(IUserRepository repository, ILogger logger) { _repository = repository; _logger = logger; } } ``` ## Validation ### Validate Early Use `ValidateAsync` for input validation: ```csharp public ValueTask 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()); } ``` ### Separate Input vs Business Validation - **Input validation** (in `ValidateAsync`): Format, required fields, length limits - **Business validation** (in `HandleAsync`): Uniqueness, permissions, business rules ```csharp public ValueTask 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> HandleAsync( CreateUserRequest request, CancellationToken cancellationToken) { // Business validation if (await _repository.EmailExistsAsync(request.Email, cancellationToken)) return Result.Failure("Email already registered"); // Create user... } ``` ## Result Handling ### Check Results Immediately Don't ignore results: ```csharp // Good: Check result var result = await _dispatcher.Dispatch(request); if (result.IsFailure) return Result.Failure(result.Error!); // Avoid: Ignoring result await _dispatcher.Dispatch(request); // What if it failed? ``` ### Use Explicit Failures for Expected Cases ```csharp // Good: Explicit failure for expected case var user = await _repository.GetByIdAsync(id, ct); if (user is null) return Result.Failure($"User {id} not found"); // Avoid: Throwing for expected case var user = await _repository.GetByIdAsync(id, ct) ?? throw new NotFoundException($"User {id} not found"); ``` ### Include Context in Error Messages ```csharp // Good: Contextual error return Result.Failure($"User {request.Id} not found"); return Result.Failure($"Cannot cancel order {orderId}: already shipped"); // Avoid: Generic error return Result.Failure("Not found"); return Result.Failure("Invalid operation"); ``` ## Project Organization ### Organize by Feature ``` src/ ├── MyApp.Application/ │ ├── Users/ │ │ ├── CreateUser/ │ │ │ ├── CreateUserRequest.cs │ │ │ └── CreateUserHandler.cs │ │ ├── GetUser/ │ │ │ ├── GetUserRequest.cs │ │ │ └── GetUserHandler.cs │ │ └── UpdateUser/ │ │ ├── UpdateUserRequest.cs │ │ └── UpdateUserHandler.cs │ └── Orders/ │ └── ... ``` ### Or Organize by Type ``` src/ ├── MyApp.Application/ │ ├── Requests/ │ │ ├── CreateUserRequest.cs │ │ ├── GetUserRequest.cs │ │ └── CreateOrderRequest.cs │ └── Handlers/ │ ├── CreateUserHandler.cs │ ├── GetUserHandler.cs │ └── CreateOrderHandler.cs ``` ## Testing ### Test Handlers Directly ```csharp [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); } ``` ### Test Validation Separately ```csharp [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()); var request = new CreateUserRequest(email, "Test"); // Act var result = await handler.ValidateAsync(request); // Assert Assert.True(result.IsFailure); Assert.Contains(expectedError, result.Error); } ``` ### Test Through Dispatcher for Integration ```csharp [Fact] public async Task CreateUser_Integration_WorksEndToEnd() { // Arrange var services = new ServiceCollection(); services.AddResultR(typeof(CreateUserHandler).Assembly); services.AddScoped(); var provider = services.BuildServiceProvider(); var dispatcher = provider.GetRequiredService(); // Act var result = await dispatcher.Dispatch( new CreateUserRequest("test@example.com", "Test")); // Assert Assert.True(result.IsSuccess); } ``` ## Performance ### Avoid Unnecessary Async If your operation is synchronous, wrap it efficiently: ```csharp // Good: Synchronous operation wrapped efficiently public ValueTask> HandleAsync( CalculateRequest request, CancellationToken cancellationToken) { var result = request.A + request.B; return new(Result.Success(result)); } // Avoid: Unnecessary async public async ValueTask> HandleAsync( CalculateRequest request, CancellationToken cancellationToken) { await Task.CompletedTask; // Unnecessary var result = request.A + request.B; return Result.Success(result); } ``` ### Pass CancellationToken Through Always pass the cancellation token to async operations: ```csharp public async ValueTask> 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); } ``` ## Summary | 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 |