Skip to content

feat(host): support multiple response types with same envelope base #212

Description

@j-d-ha

Summary

Implement support for handlers that can return multiple different response types within the same envelope flavor, similar to ASP.NET Core Minimal API's Results<TResult1, TResult2, ...> pattern. This would allow a single handler to return different typed responses (e.g., success vs. error) while maintaining the same base envelope type.

Motivation

Currently, handlers must specify a single response envelope type. However, many real-world scenarios require returning different response types from the same handler (e.g., Ok<T> vs BadRequest<TError>). The workaround requires creating wrapper types manually, which adds boilerplate and reduces type safety.

Proposed Solution

Introduce a Response<TResult1, TResult2, ...> type and envelope-specific variants (e.g., ApiGatewayResponse<TResult1, TResult2>) that:

  1. Accept multiple generic type parameters for different response body types
  2. Use implicit operators to convert from each specific envelope type
  3. Delegate serialization to the active result envelope
  4. Maintain the same base envelope type across all variants

Example Usage

```csharp
lambda.MapHandler(
Response<ApiGatewayResponseEnvelope, ApiGatewayResponseEnvelope> (
[Event] ApiGatewayRequestEnvelope request,
ILogger logger
) =>
{
logger.LogInformation("In Handler. Payload: {Payload}", request.Body);

    if (request.BodyContent == null)
        return ApiGatewayResponse.BadRequest(new ErrorDetails("bummer"));

    return ApiGatewayResponse.Ok(
        new Response($"Hello {request.BodyContent?.Name}!", DateTime.UtcNow)
    );
}

);
```

Implementation Pattern

```csharp
public class Response<TResult1, TResult2> : IResponseEnvelope
where TResult1 : IResponseEnvelope
where TResult2 : IResponseEnvelope
{
private Response(IResponseEnvelope activeResult)
{
Result = activeResult;
}

public IResponseEnvelope Result { get; }

public static implicit operator Response<TResult1, TResult2>(TResult1 result) => new(result);
public static implicit operator Response<TResult1, TResult2>(TResult2 result) => new(result);

public void PackPayload(EnvelopeOptions options) => Result.PackPayload(options);

}

public class ApiGatewayResponse<TResult1, TResult2> : IResponseEnvelope
{
private ApiGatewayResponse(IResponseEnvelope activeResult)
{
Result = activeResult;
}

public IResponseEnvelope Result { get; }

public static implicit operator ApiGatewayResponse<TResult1, TResult2>(
    ApiGatewayResponseEnvelope<TResult1> result
) => new(result);

public static implicit operator ApiGatewayResponse<TResult1, TResult2>(
    ApiGatewayResponseEnvelope<TResult2> result
) => new(result);

public void PackPayload(EnvelopeOptions options) => Result.PackPayload(options);

}
```

Requirements

  • Support 2-6 generic type parameters (matching ASP.NET Core Minimal API pattern)
  • Create envelope-specific response types for common envelopes:
    • ApiGatewayResponse<T1, T2, ...>
    • SqsResponse<T1, T2, ...> (if applicable)
    • SnsResponse<T1, T2, ...> (if applicable)
  • Create generic Response<T1, T2, ...> for custom envelopes
  • Ensure proper serialization delegation to active result
  • Add comprehensive unit tests covering:
    • Implicit conversions from each type parameter
    • Proper envelope serialization
    • Type constraints enforcement
  • Update documentation with examples
  • Consider source generator for reducing boilerplate (optional enhancement)

Design Considerations

  1. Type Constraints: All type parameters must implement IResponseEnvelope to ensure proper serialization
  2. Envelope Consistency: All variants within a single Response<...> must share the same base envelope type (e.g., all ApiGatewayResponseEnvelope<T>)
  3. Serialization: The wrapper delegates PackPayload to the active result, maintaining correct envelope structure
  4. Discovery: The Lambda host infrastructure must recognize these wrapper types and properly unwrap/serialize them

Alternative Approaches Considered

  1. Single polymorphic type: Would lose compile-time type safety
  2. Manual wrapper per handler: Current workaround, adds boilerplate
  3. OneOf-style discriminated union: More complex, doesn't match ASP.NET Core patterns

Related

This aligns with ASP.NET Core Minimal API's Results<TResult1, TResult2, ...> pattern, providing familiar semantics for developers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions