Skip to content

feature: Pluggable return-type adapters (Retrofit CallAdapter.Factory equivalent) #2165

Description

@glennawatson

Summary

Refit currently hardcodes the set of supported method return types in the request builder/source generator (Task, Task<T>, Task<IApiResponse<T>>, ApiResponse<T>, Stream, HttpResponseMessage, HttpContent, IAsyncEnumerable<T>, JsonDocument). Adding a new return shape today means editing core. This proposes a pluggable return-type adapter seam so users (and us) can map the underlying HTTP call to arbitrary wrapper types without touching core.

Prior art: Retrofit CallAdapter.Factory

Retrofit (the Java library Refit is modelled on) solves this with CallAdapter<R, T> + CallAdapter.Factory. The factory is consulted by declared return type, and ships as separate modules: RxJava (Observable<T>/Single<T>/Completable), Guava ListenableFuture<T>, Java8 CompletableFuture<T>, Scala Future. The core Call<T> stays transport-only; the adapter decides the surfaced type.

Motivation for Refit / ReactiveUI

  • Refit used to support IObservable<T> returns and dropped them. A proper seam would let us re-introduce IObservable<T> / Rx returns as an out-of-band package (e.g. Refit.Reactive) instead of bloating core — directly valuable for the ReactiveUI ecosystem.
  • Lets users adapt to their own wrappers (Result<T>, Option<T>, custom envelope types) without a PR to Refit.

Design notes / the hard part

The seam must work with the source generator, not just the reflection path — this is what makes it a real design effort rather than a small add. Sketch:

  • An interface roughly like IReturnTypeAdapter exposing bool CanHandle(Type returnType) and an adapt step from the internal call (the thing that produces Task<HttpResponseMessage> / deserialized body) to the target type.
  • The generator needs to either (a) emit a call into a registered adapter when it sees an unknown return type, or (b) recognise a known set of adapter-provided shapes at generate time. Decide whether adapters are compile-time discoverable (best for AOT/trim) or runtime-registered (more flexible, weaker AOT story). Lean compile-time to preserve Refit's AOT guarantees.
  • Registration surface should mirror RefitSettings.ContentSerializer (settings-level) and the RegisterGeneratedFactory pattern for the AOT path.

Acceptance criteria

  • A documented extension point that adds a new return type without modifying RequestBuilderImplementation / core.
  • Works in both the reflection and generated paths, with AOT/trim behaviour unchanged for users who don't use it.
  • A proof-of-concept adapter (e.g. IObservable<T> or Result<T>) living outside Refit core.

Pointers

  • Reflection return-type dispatch: src/Refit/RequestBuilderImplementation.Execution.cs, RestMethodInfoInternal.cs.
  • Generator return-type handling: src/InterfaceStubGenerator.Shared/*.
  • Retrofit reference: ~/square/retrofit/retrofit/src/main/java/retrofit2/CallAdapter.java and retrofit-adapters/.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions