Skip to content

Effects Composition

Every [HttpClient] automatically generates an effects module — each HTTP method becomes a composable Eff<RT, T> with OpenTelemetry activity tracing and structured error mapping. No additional attributes are needed.

What's Generated

For a client like this:

[HttpClient(BaseAddress = "https://api.example.com")]
public partial class UsersClient
{
    [Get("/users/{id}")]
    public partial Task<User> GetUser(int id);

    [Post("/users")]
    public partial Task<User> CreateUser([Body] CreateUserRequest request);
}

The generator produces:

Capability Interface

// Generated: IHasUsersClient
public interface IHasUsersClient
{
    public IUsersClient UsersClient { get; }
}

Effects Module

// Generated: UsersClientEffects
public static partial class UsersClientEffects
{
    public static partial class UsersClient
    {
        private static readonly ActivitySource ActivitySource =
            new("MyApp.UsersClient", "1.0.0");

        public static Eff<RT, User> GetUser<RT>(int id)
            where RT : IHasUsersClient =>
            liftEff<RT, User>(async rt => await rt.UsersClient.GetUser(id))
                .MapFail(e => Error.New("UsersClient.GetUser failed", e))
                .WithActivity("UsersClient.GetUser", ActivitySource,
                    destination: "UsersClient");

        public static Eff<RT, User> CreateUser<RT>(CreateUserRequest request)
            where RT : IHasUsersClient =>
            liftEff<RT, User>(async rt => await rt.UsersClient.CreateUser(request))
                .MapFail(e => Error.New("UsersClient.CreateUser failed", e))
                .WithActivity("UsersClient.CreateUser", ActivitySource,
                    destination: "UsersClient");
    }
}

Each generated effect method includes:

  1. Eff lifting — wraps the Task<T> call in liftEff with runtime access
  2. Error mapping.MapFail() adds context to failures (which client and method failed)
  3. Activity tracing.WithActivity() creates OpenTelemetry spans for distributed tracing

Usage in Effect Pipelines

Compose HTTP calls with other effects using from/select:

using static UsersClientEffects.UsersClient;
using static AppStore.Orders;

public static Eff<AppRuntime, OrderConfirmation> PlaceOrder<RT>(
    CreateOrderRequest request) where RT : AppRuntime =>
    from user in GetUser<RT>(request.UserId)
    from order in Save<RT>(new Order(user, request))
    select new OrderConfirmation(order.Id);

Composing with Runtime

Add IHasUsersClient to your runtime to make the capability available:

[Runtime]
[Uses(typeof(UsersClient))]
public partial class AppRuntime;

The runtime generator discovers IHasUsersClient and wires it automatically.

Structured Error Handling

For APIs that return structured errors (e.g., Slack's ok: false responses), use ApiResponse<T> as the return type:

[HttpClient]
public partial class SlackClient
{
    [Post("/chat.postMessage")]
    public partial Task<ApiResponse<MessageResponse>> PostMessage(
        [Body] PostMessageRequest request);
}

ApiResponse<T> captures success/failure as data rather than exceptions. Convert to an Eff with .ToEff<RT>() or use the .UnwrapResponse() extension:

from response in SlackClientEffects.SlackClient.PostMessage<RT>(request)
from message in response.ToEff<RT>()  // fails with ApiError if !Success
select message;

// Or more concisely via UnwrapResponse:
SlackClientEffects.SlackClient.PostMessage<RT>(request).UnwrapResponse()

ApiResponse Types

ApiResponse<T> and ApiError live in Deepstaging.Http (in Deepstaging.Runtime):

// Success with data
ApiResponse<T>.Ok(data)

// Failure with structured error
ApiResponse<T>.Fail("channel_not_found", "Channel not found", httpStatus: 404)
ApiResponse<T>.Fail(new ApiError { Code = "...", Message = "...", HttpStatus = 400 })

// Non-generic for void-result APIs
ApiResponse.Ok()
ApiResponse.Fail("rate_limited", "Too many requests", httpStatus: 429)