Skip to content

Extensions

4.3.1. 📡 OpenTelemetry Integration — WithActivity

Enriches an existing Activity span with result outcome metadata — Tap-style, returns the result unchanged:

using var activity = ActivitySource.StartActivity("GetUser");

Result<User> user = await _userService.GetAsync(id)
    .WithActivity(activity);    // or Activity.Current

Tags set on the activity:

Tag Value
result.outcome "success" or "failure"
result.error.type First error type name (on failure)
result.error.message First error message (on failure)
result.error.count Error count (only when > 1)

Activity status is set to ActivityStatusCode.Ok on success, ActivityStatusCode.Error on failure. Null-safe — no-op when activity is null. No extra NuGet dependency — uses BCL System.Diagnostics.Activity.


4.3.2. 📝 Structured Logging — WithLogger / LogOnFailure

Tap-style ILogger integration — log result outcomes without breaking the pipeline:

// Log every result: Debug on success, Warning/Error on failure
Result<User> user = await _userService.GetAsync(id)
    .WithLogger(_logger, "GetUser");

// Log only failures — success is silent
Result<Order> order = await _orderService.CreateAsync(dto)
    .LogOnFailure(_logger, "CreateOrder");

// Composable in pipelines
Result<UserDto> dto = await _userService.GetAsync(id)
    .WithLogger(_logger, "GetUser")
    .MapAsync(u => _mapper.Map(u));

Log levels per outcome:

Outcome Level When
Success Debug IsSuccess
Domain failure Warning IsFailure, no ExceptionError
Exception failure Error IsFailure, contains ExceptionError

Structured log properties on every failure entry: {OperationName}, {ErrorType}, {ErrorMessage}, {ErrorCount} (when > 1 error). Task<Result<T>> extensions accept CancellationToken.


4.3.3. 🔄 Railway Recovery — Recover / RecoverAsync

The counterpart to Bind: where Bind chains on the success path, Recover chains on the failure path. Transform any failure into a new Result — which can itself succeed or fail:

// Fallback to cache if the primary DB call fails
Result<User> user = await _userRepo.GetAsync(id)
    .Recover(errors => _cache.Get(id));

// Async fallback — secondary data source
Result<User> user = await _userRepo.GetAsync(id)
    .RecoverAsync(errors => _fallbackApi.GetUserAsync(id));

// Context-aware: skip recovery on ForbiddenError
Result<Document> doc = await FetchDocument(id)
    .Recover(errors => errors.Any(e => e is ForbiddenError)
        ? Result<Document>.Fail(errors)
        : _localCache.Get(id));

// Non-generic Result — command recovery
Result result = await DeleteUser(id)
    .Recover(errors => ArchiveUser(id));

The recovery func receives the full ImmutableList<IError> — enabling context-aware branching. Pass-through on success. Distinct from Catch<TException>: Catch targets only ExceptionError wrapping a specific exception type and always returns a failure; Recover handles any failure and can return success.


4.3.4. 🔍 Predicate Filtering — Filter / FilterAsync

Convert a successful result to a failure when a predicate on the value is not met. The error factory receives the value — enabling contextual error messages that embed actual data:

// Value-dependent error — the primary Filter use case
Result<User> activeUser = userResult
    .Filter(u => u.IsActive, u => new Error($"User '{u.Name}' is not active."));

// Static error — convenience overload
Result<Order> pending = orderResult
    .Filter(o => o.Status == OrderStatus.Pending, new ConflictError("Order", "status"));

// String message — convenience overload
Result<Product> inStock = productResult
    .Filter(p => p.Stock > 0, "Product is out of stock.");

// Async predicate (e.g. external validation service)
Result<Order> valid = await orderResult
    .FilterAsync(async o => await _validator.IsValidAsync(o),
                 o => new ValidationError("Order", o.Id.ToString(), "failed validation"));

Distinct from Ensure: Ensure takes a static Error fixed at the call site. Filter takes Func<T, IError> — the error is built from the value itself, enabling messages like "User 'John' is not active". Predicate exceptions are wrapped in ExceptionError. Pass-through on failure.