Home > Software engineering >  HttpClient Extension with Eventhandler
HttpClient Extension with Eventhandler

Time:10-05

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 { };

CodePudding user response:

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)> one rror)
{
...
    if (response.StatusCode != System.Net.HttpStatusCode.OK)
    {
        one rror(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#:

  • Return a tuple with the value and error, eg (T? value, string? error)
  • Create a record with the value and error
  • Create separate Success and Error classes that share a common IResult<T> interface

Pattern 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

CodePudding user response:

You could store the handlers in your extension class and do something like this ? Please note this code is not thread safe and need to be synchronized around dictionary and list access !

public static class HttpClientExtensions
{
    private static Dictionary<HttpClient, List<Action<HttpClientErrorEventArgs>>> Handlers { get; set; }

    static HttpClientExtensions()
    {
        Handlers = new Dictionary<HttpClient, List<Action<HttpClientErrorEventArgs>>>();
    }

    public async static Task<T> GetSomthingSpecialAsync<T>(this HttpClient client, string url)
    {
        ////code ....

        //I have an error and want to raise the HttpClientEventError
        HttpClientErrorEventArgs args = null;
        client.RaiseEvent(args);
        return default(T);


        ////code
    }

    public static void AddHandler(this HttpClient client, Action<HttpClientErrorEventArgs> handler)
    {
        var found = Handlers.TryGetValue(client, out var handlers);

        if (!found)
        {
            handlers = new List<Action<HttpClientErrorEventArgs>>();
            Handlers[client] = handlers;
        }

        handlers.Add(handler);
    }

    public static void RemoveHandler(this HttpClient client, Action<HttpClientErrorEventArgs> handler)
    {
        var found = Handlers.TryGetValue(client, out var handlers);

        if (found)
        {
            handlers.Remove(handler);

            if (handlers.Count == 0)
            {
                Handlers.Remove(client);
            }
        }
    }

    private static void RaiseEvent(this HttpClient client, HttpClientErrorEventArgs args)
    {
        var found = Handlers.TryGetValue(client, out var handlers);

        if (found)
        {
            foreach (var handler in handlers)
            {
                handler.Invoke(args);
            }
        }
    }
}
  • Related