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:
- Eff lifting — wraps the
Task<T>call inliftEffwith runtime access - Error mapping —
.MapFail()adds context to failures (which client and method failed) - 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:
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)