带有事件处理程序的 HttpClient 扩展

HttpClient Extension with Eventhandler

我正在尝试使用 EventHandler 扩展 HttpClient。 这可能吗?

我在 HttpClient 上有如下扩展:

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

但是如何定义 HttpClientErrorEvent? 我尝试了以下但它不是特定 HttpClient 的扩展:

public static event EventHandler<HttpClientErrorEventArgs> HttpClientErrorEvent = delegate { };

您可以将处理程序存储在您的扩展程序中 class 并执行类似的操作吗?请注意此代码不是线程安全的,需要围绕字典和列表访问进行同步!

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

不要使用事件来 return 错误。对于初学者,您将如何确定哪个请求引发了哪个错误?您必须围绕每个调用注册和取消注册事件处理程序,但您将如何处理并发调用?您将如何编写多个此类调用?

错误无论如何都不是事件。充其量,您必须像处理回调一样处理事件 - 在这种情况下,为什么不使用实际的回调?

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 的创建是为了让人们可以摆脱回调和事件。用事件组合多个异步调用几乎是不可能的,并且很难用回调来做到这一点。这就是为什么 很多 语言(C#、JavaScript、Dart,甚至在某种程度上是 C++)引入了 promises 和 async/await 来摆脱成功和错误回调。

您实际上可以 return 调用函数的结果或错误,而不是调用回调。这是一种嵌入到例如 F#、Rust 和 Go 中的功能性方式(通过元组)。在 C# 中有 很多 种方法可以做到这一点:

  • Return 包含值和错误的元组,例如 (T? value, string? error)
  • 使用值和错误创建一条记录
  • 创建单独的 Success 和 Error 类 共享一个公共 IResult<T> 接口

模式匹配可以与任何选项一起使用来检索错误或值,而无需大量 if 语句。

假设我们有一个特定的错误类型,HttpError。:

record HttpError(HttpStatusCode Status,string Message);

使用元组,方法变为:

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,....);
    }
}

并调用:

var (value,error)=await client.GetSomethingSpecialAsync(url);
if(error!=null)
{
    var (status,msg)=error;
    Console.WriteLine($"Calling {url} Failed with {status}:{msg}");
...
}

我们可以创建 Result 记录来代替元组:

record Result<T>(T? Value,HttpError? Error);

或分开类:

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);

在所有情况下都可以使用模式匹配来简化对结果的处理,例如:

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

具有特定的 Result<T>IResult<T> 类型可以轻松编写通用方法来处理成功、错误或组成函数链。例如,如果前一个成功,则以下可用于调用“下一个”函数,否则只传播“错误”:

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)
    }
}

这将允许创建调用管道:

var finalResult=doSomething(url)
                .ThenIfOk(value=>somethingElse(value))
                .ThenIfOk(....);

这种风格被称为Railway oriented programming,在函数式和数据流(管道)编程中很常见