The question basically boils to
- Should any consumer of this interface need such degree of freedom as to be able to produce any value from
HttpResponse
- Will it lead to easy-to-maintain-design, or will it be a proverbial callback hell
Should any consumer of that interface need such degree of freedom as to be able to produce any value from HttpResponse
If we assume that we need to create many WebApiClients like
public class FooApiClient: IFooApiClient
{
private readonly IWebRequestWrapper _webRequestWrapper;
public FooApiClient(IWebRequestWrapper webRequestWrapper) =>
_webRequestWrapper = webRequestWrapper ?? throw new ArgumentNullException();
public FooBaseData GetBaseData(string id) =>
_webRequestWrapper.DoRequest(id, response => new FooBaseData...);
public FooAdvancedData GetAdvancedData(string id) =>
_webRequestWrapper.DoRequest(id, response => new FooAdvancedData...);
}
Though unless we really need some custom HttpResponseProcessing, I'd rather expected that API to have swagger to allow those API clients to be generated automatically.
and our HttpResponse processing logic is at least slightly non-trivial (I mean not just get Json, parse if HttpStatusCode.Success/throw if anything else)
or if we have some generic(defined at run-time) endpoints
var result = _webRequestWrapper.DoRequest(
TextBox1.Text,
response => Deserializer.Deserializer(GetType(ComboxBox1.Type), ...)
then it is a pretty reasonable design
- It is composable (you can easily reuse it anywhere)
- It makes consumer classes testable (that non-trivial HttpResponse processing logic that produces your actual domain objects)
- Well, not with the real HttpResponse but I assume that it was not meant literally
- In the worst case we will just have use some one-to-one wrapper MyHttpResponse that can be easily created/mocked.
- It is concise and easy to use
Basically IEnumerable.Select for WebAPI calls.
Then what are the disadvantages?
- Well, it is yet another abstraction layer. You gain some composability and testability, but also have to maintain one more class and interface. Overall not a bad deal, but still, unless you really need/will use that composability and testability, its a price paid for little-to-nothing.
- It protects against someone forgetting to dispose WebResponse, but it doesn't protect the same person from forgetting that it will be disposed, so response should not/cannot be cached and value cannot be lazy initialized. Though you may say that it is obvious and the problem stands basically the same for inlined
using (...HttpClient.Get
, any abstraction makes things more difficult to spot.
- Again, if we really want to avoid such risks, we can use some wrapper class that is created as a full snapshot of the received HttpResponse. Though it will lead to a small (though usually irrelevant) performance loss.
Will it lead to easy-to-maintain-design, or will it be a proverbial callback hell
As the first comment probably meant by its vehemence is that callbacks lead to callback hell. And that's true - those should be avoided at any cost.
But in this case it is a synchronous callback, not the dreaded asynchronous one from JavaScript.
Unless you nest another callbacks inside it, it is not much different from the regular Enumerable.Select
that can be misused just as well.
Overall
As the second question is a non-issue, we are left the first one.
So if
- There is very little logic in callbacks (no need to test it)
- And/Or there are very few distinct callbacks
- And/Or callbacks are often reused (code duplication)
Then it probably makes little sense to have such interface. Strongly typed API clients are the way to go (especially if they can be just generated with NSwagStudio or the likes).
But if none of those points apply (or apply very weakly), then such abstraction is a perfectly viable solution.
As always there are no absolute answers - everything depends on the exact situation.
about the testable part I really thought it gets harder to test this way instead of returning a specific dto
I meant cases when
our HttpResponse processing logic is at least slightly non-trivial (I mean not just get Json, parse if HttpStatusCode.Success/throw if anything else)
For example,
DoRequest("PeculiarApi", response =>
{
if ((response.StatusCode == HttpStatusCode.Success) ||
(response.StatusCode == HttpStatusCode.Conflict))
{
if (response.Headers.TryGet(Headers.ContentType, out var contentType) &&
(contentType == ContentType.ApplicationJson))
{
return JsonConvert.Deserialize<TModel>(response.Content);
}
throw new Exception("No content type");
}
if (response.StatusCode == (HttpStatusCode)599) // Yes, that's what they return
{
return XmlConvert.Deserialize<T599Model>(response.Content).Errors[2];
}
...
}
is a piece of non-trivial logic.
This logic can be easily tested through mocked IWebRequestWrapper
.
Can it be tested with regular HttpClient? Yes, but that will require some boilerplate that is not needed with IWebRequestWrapper
.
By itself it is not worth much (not a deal-breaker), but if we already have to use IWebRequestWrapper
it is at least a minor simplification.