You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
move "async Task<RestResponse<T>> ExecuteAsync<T>" from Extension to IRestClient #2384
Is the "natural" way to do a Request: everyting is TypeSave. But If you try to Mock IRestClient, you can only mock the Non-Typed Request-Reponse method.
In a Test-Scenario, we have to add the Serializers and use compute to Serialize & Deserialize, there is no full control on the response - the deserializers try to do the work - and an easy workaround with own serializer/deserialiser is not easy.
I had a long look at the sources, but using a custom Serializer did not worked out well. So I ended up with this Blog-Post unittest-with-restsharp
Yes, this works but is not intuitive.
Describe the solution you'd like
Having Task<RestResponse<T>> ExecuteAsync<T> in IRestClient would enable us to simple Mock the call. No additional Setup, No Serialization during the Test.
As far as I see, the ExtensionMethod can be moved to the RestClient-Implementation without breaking anything. All other ExtensionMethods will work, IRestClient-Implementation / Mocks will work.
You could move the method to the IRestClient Implementaion, extend the interface and remove the ExtensionMethod- without any side effects.
Describe alternatives you've considered
Considered? Mocking the Tests is complex. I considered to remove RestSharp - but my time is limited ;-) I did a complex initialization and hope my (future) colleagues will understand this in 2 years...
If I was unclear / any questions - may need some days for me to answer, but I'm here
Hi @Xynratron, thanks for the thoughtful write-up — and especially for being explicit about the workaround you ended up with. That kind of detail is genuinely useful.
I want to engage with this carefully because the friction you're describing is real, but I don't think putting ExecuteAsync<T> on IRestClient is the right fix. Let me explain why, and then suggest a few paths that should make your tests much less painful.
Why I'm reluctant to move it onto the interface
It's a derived operation, not a primitive.ExecuteAsync<T> is literally ExecuteAsync(request, ct) followed by Serializers.Deserialize<T>(...). Both of those are already on IRestClient. Promoting the composition onto the interface duplicates capability that's already exposed — and locks the composition shape (and its overloads) into the public contract forever.
Interface members are a permanent commitment. Every alternative IRestClient implementation in the wild — test doubles, decorators, in-process fakes, third-party adapters — would need to implement the new member. That's a binary-breaking change for exactly the same population (people writing fakes/mocks) that this proposal is trying to help.
Slippery slope. If ExecuteAsync<T> belongs on the interface "because it's the natural way to use it", the same argument applies to GetAsync<T>, PostAsync<T>, PostJsonAsync<T>, and the rest of the typed convenience surface. There's a reason all of those live as extensions — they keep the contract narrow and the implementations free.
The "no side effects" claim is a bit optimistic. Source-compatible for callers, yes. But it is a breaking change at the implementer boundary.
So the real problem isn't where ExecuteAsync<T> lives — it's that Moq can't intercept extension methods. That's a Moq limitation, and there are several ways around it that don't require expanding the abstraction surface.
Approaches that should make your tests painless
Mock the primitive, not the helper.IRestClient.ExecuteAsync(request, ct)is mockable. Return a RestResponse whose Content is the JSON payload you want, and let the real SystemTextJsonSerializer deserialize it — that's the path of least resistance, and you get free coverage of the serializer wiring at the same time. If you want hand-crafted control, set Content = \"{...}\" directly; the deserializer is fast and deterministic.
Stub RestResponse<T> end-to-end. If you specifically don't want serialization to run, use a tiny test-only IRestClient (not a mock — just a class) that implements the two interface methods and returns whatever RestResponse you want. ~10 lines, no mocking framework needed, easier to reason about than Mock<> setups.
Wrap RestSharp behind your own domain interface. This is the pattern I'd recommend long-term regardless of mocking: define an IFooApiClient with the operations your app actually performs, implement it once with RestSharp, and mock IFooApiClient in tests. The HTTP boundary then lives in your code, where you control it, and your tests don't depend on RestSharp's evolution at all.
Switch frameworks for this specific case. NSubstitute, FakeItEasy, and Moq.AutoMock all have the same extension-method limitation, but the blog post you linked shows the workaround works. If the friction is the boilerplate, options (2) and (3) above eliminate it entirely.
Where I could be convinced otherwise
If there were a real pipeline reason the typed path couldn't be composed from the non-typed one — e.g., a typed streaming or chunked-deserialization path that fundamentally bypasses RestResponse materialization — that would justify a new interface member. As things stand today, ExecuteAsync<T> is pure sugar over two interface methods, and I'd rather keep IRestClient minimal.
Happy to keep talking if any of the mitigations above don't fit your scenario — knowing more about what specifically broke when you tried a custom serializer would help me point at the right one.
Activity
alexeyzimarev commentedon Jun 2, 2026
Hi @Xynratron, thanks for the thoughtful write-up — and especially for being explicit about the workaround you ended up with. That kind of detail is genuinely useful.
I want to engage with this carefully because the friction you're describing is real, but I don't think putting
ExecuteAsync<T>onIRestClientis the right fix. Let me explain why, and then suggest a few paths that should make your tests much less painful.Why I'm reluctant to move it onto the interface
ExecuteAsync<T>is literallyExecuteAsync(request, ct)followed bySerializers.Deserialize<T>(...). Both of those are already onIRestClient. Promoting the composition onto the interface duplicates capability that's already exposed — and locks the composition shape (and its overloads) into the public contract forever.IRestClientimplementation in the wild — test doubles, decorators, in-process fakes, third-party adapters — would need to implement the new member. That's a binary-breaking change for exactly the same population (people writing fakes/mocks) that this proposal is trying to help.ExecuteAsync<T>belongs on the interface "because it's the natural way to use it", the same argument applies toGetAsync<T>,PostAsync<T>,PostJsonAsync<T>, and the rest of the typed convenience surface. There's a reason all of those live as extensions — they keep the contract narrow and the implementations free.So the real problem isn't where
ExecuteAsync<T>lives — it's that Moq can't intercept extension methods. That's a Moq limitation, and there are several ways around it that don't require expanding the abstraction surface.Approaches that should make your tests painless
IRestClient.ExecuteAsync(request, ct)is mockable. Return aRestResponsewhoseContentis the JSON payload you want, and let the realSystemTextJsonSerializerdeserialize it — that's the path of least resistance, and you get free coverage of the serializer wiring at the same time. If you want hand-crafted control, setContent = \"{...}\"directly; the deserializer is fast and deterministic.RestResponse<T>end-to-end. If you specifically don't want serialization to run, use a tiny test-onlyIRestClient(not a mock — just a class) that implements the two interface methods and returns whateverRestResponseyou want. ~10 lines, no mocking framework needed, easier to reason about thanMock<>setups.IFooApiClientwith the operations your app actually performs, implement it once with RestSharp, and mockIFooApiClientin tests. The HTTP boundary then lives in your code, where you control it, and your tests don't depend on RestSharp's evolution at all.Where I could be convinced otherwise
If there were a real pipeline reason the typed path couldn't be composed from the non-typed one — e.g., a typed streaming or chunked-deserialization path that fundamentally bypasses
RestResponsematerialization — that would justify a new interface member. As things stand today,ExecuteAsync<T>is pure sugar over two interface methods, and I'd rather keepIRestClientminimal.Happy to keep talking if any of the mitigations above don't fit your scenario — knowing more about what specifically broke when you tried a custom serializer would help me point at the right one.