I'm trying to extend the HttpClient with an EventHandler. Is this possible?
I have an Extension on HttpClient as follows:
public static class HttpClientExtensions
{
public async static Task<T> GetSomthingSpecialAsync<T>(this HttpClient client, string url)
{
using var response = await client.GetAsync(url);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
//I have an error and want to raise the HttpClientEventError
HttpClientErrorEvent(null, new HttpClientErrorEventArgs()
{
StatusCode = response.StatusCode,
Message = $"{response.StatusCode } {(int)response.StatusCode } "
});
return default(T);
}
response.EnsureSuccessStatusCode();
[... ]
}
}
public class HttpClientErrorEventArgs : EventArgs
{
public System.Net.HttpStatusCode StatusCode { get; set; }
public string Message { get; set; }
}
But how do I define the HttpClientErrorEvent? I tried the following but it is not an extension to a specific HttpClient:
public static event EventHandler<HttpClientErrorEventArgs> HttpClientErrorEvent = delegate { };
Don't use an event to return errors. For starters, how are you going to identify which request raised which error? You'd have to register and unregister event handlers around each call but how would you handle concurrent calls? How would you compose multiple such calls?
Errors aren't events anyway. At best, you'd have to handle the event as if it was a callback - in which case why not use an actual callback?
public async static Task<T> GetSomethingSpecialAsync<T>(this HttpClient client, string url,Action<(HttpStatusCode Status,string Message)> onError)
{
...
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
onError(response.Status,....);
return default;
}
}
...
var value=await client.GetSomethingSpesialAsync(url,
(status,msg)=>{Console.WriteLine($"Calling {url} Failed with {status}:{msg}");}
);
async/await
was created so people can get rid of callbacks and events though. It's almost impossible to compose multiple async calls with events, and hard enough to do so with callbacks. That's why a lot of languages (C#, JavaScript, Dart, even C++ in a way ) introduced promises and async/await
to get rid of both the success and error callback.
Instead of calling a callback you can actually return either a result or an error from your function. This is a functional way embedded in eg F#, Rust and Go (through tuples). There are a lot of ways to do this in C#:
(T? value, string? error)
IResult<T>
interfacePattern matching can be used with any option to retrieve either the error or value without a ton of if
statements.
Let's say we have a specific error type, HttpError.:
record HttpError(HttpStatusCode Status,string Message);
Using tuples, the method becomes:
public async static Task<(T value,HttpError error> GetSomethingSpecialAsync<T>(this HttpClient client,string url)
{
...
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
return (default,new HttpError(response.Status,....);
}
}
And called :
var (value,error)=await client.GetSomethingSpecialAsync(url);
if(error!=null)
{
var (status,msg)=error;
Console.WriteLine($"Calling {url} Failed with {status}:{msg}");
...
}
Instead of a tuple, we can create a Result
record:
record Result<T>(T? Value,HttpError? Error);
Or separate classes:
interface IResult<T>
{
bool IsSuccess{get;}
}
record Success<T>(T Value):IResult<T>
{
public bool IsSuccess=>true;
}
record Error<T>(HttpError Error):IResult<T>
{
public bool IsSuccess => false;
}
public async static Task<IResult<T>> GetSomethingSpecialAsync<T>(this HttpClient client,string url){...}
var result=await client.GetSomethingSpecialAsync(url);
In all cases pattern matching can be used to simplify handling the result, eg:
var result=await client.GetSomethingSpecialAsync<T>(url);
switch (result)
{
case Error<T> (status,message):
Console.WriteLine($"Calling {url} Failed with {Status}:{Message}");
break;
case Success<T> (value):
...
break;
}
Having a specific Result<T>
or IResult<T>
type makes it easy to write generic methods to handle success, errors or compose a chain of functions. For example, the following could be used to call the "next" function if the previous one succeeded, otherwise just propagate the "error" :
IResult<T> ThenIfOk(this IResult<T> previous,Func<T,IResult<T>> func)
{
return previous switch
{
Error<T> error=>error,
Success<T> ok=>func(ok.Value)
}
}
This would allow creating a pipeline of calls :
var finalResult=doSomething(url)
.ThenIfOk(value=>somethingElse(value))
.ThenIfOk(....);
This style is called Railway oriented programming and is very common in functional and dataflow (pipeline) programming