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/.
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.FactoryRetrofit (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), GuavaListenableFuture<T>, Java8CompletableFuture<T>, ScalaFuture. The coreCall<T>stays transport-only; the adapter decides the surfaced type.Motivation for Refit / ReactiveUI
IObservable<T>returns and dropped them. A proper seam would let us re-introduceIObservable<T>/ Rx returns as an out-of-band package (e.g.Refit.Reactive) instead of bloating core — directly valuable for the ReactiveUI ecosystem.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:
IReturnTypeAdapterexposingbool CanHandle(Type returnType)and an adapt step from the internal call (the thing that producesTask<HttpResponseMessage>/ deserialized body) to the target type.RefitSettings.ContentSerializer(settings-level) and theRegisterGeneratedFactorypattern for the AOT path.Acceptance criteria
RequestBuilderImplementation/ core.IObservable<T>orResult<T>) living outside Refit core.Pointers
src/Refit/RequestBuilderImplementation.Execution.cs,RestMethodInfoInternal.cs.src/InterfaceStubGenerator.Shared/*.~/square/retrofit/retrofit/src/main/java/retrofit2/CallAdapter.javaandretrofit-adapters/.