【问题标题】:Build query string for System.Net.HttpClient get为 System.Net.HttpClient 构建查询字符串
【发布时间】:2013-06-10 09:03:53
【问题描述】:

如果我希望使用 System.Net.HttpClient 提交 http get 请求,似乎没有添加参数的 api,这是正确的吗?

是否有任何简单的 api 可用于构建不涉及构建名称值集合和 url 编码这些查询字符串,然后最后将它们连接起来? 我希望使用类似 RestSharp 的 API(即 AddParameter(..))

【问题讨论】:

  • @Michael Perrenoud 您可能想重新考虑使用需要编码的字符的已接受答案,请参阅下面的解释

标签: c# .net http


【解决方案1】:

''' HttpClient 客户端 = new HttpClient(); var uri = Environment.GetEnvironmentVariable("API 的 URL");

var requesturi = QueryHelpers.AddQueryString(uri, "parameter_name",parameter_value); client.BaseAddress = new Uri(requesturi); ''' 然后您也可以添加请求标头,例如:

''' client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Add("x-api-key", secretValue); ''' 响应语法例如:

''' HttpResponseMessage 响应 = client.GetAsync(requesturi).Result; ''' 希望它对你有用。 #.netcore #csharp #visualstudio #httpclient #addquerystring

【讨论】:

    【解决方案2】:

    在 ASP.NET Core 项目中,您可以使用 QueryHelpers 类,该类在 ASP.NET Core 的 Microsoft.AspNetCore.WebUtilities 命名空间中可用,或在 .NET Standard 2.0 NuGet package 中用于其他使用者:

    // using Microsoft.AspNetCore.WebUtilities;
    var query = new Dictionary<string, string>
    {
        ["foo"] = "bar",
        ["foo2"] = "bar2",
        // ...
    };
    
    var response = await client.GetAsync(QueryHelpers.AddQueryString("/api/", query));
    

    【讨论】:

    • 令人讨厌的是,尽管使用此过程您仍然无法为同一个键发送多个值。如果您想将“bar”和“bar2”作为 foo 的一部分发送,这是不可能的。
    • 这是现代应用程序的绝佳答案,适用于我的场景,简单而干净。但是,我不需要任何转义机制 - 未经测试。
    • 此 NuGet 包面向 .NET 标准 2.0,这意味着您可以在完整的 .NET 框架 4.6.1+ 上使用它
    【解决方案3】:

    您可能想查看Flurl [披露:我是作者],这是一个流畅的 URL 构建器,带有可选的配套库,可将其扩展为成熟的 REST 客户端。

    var result = await "https://api.com"
        // basic URL building:
        .AppendPathSegment("endpoint")
        .SetQueryParams(new {
            api_key = ConfigurationManager.AppSettings["SomeApiKey"],
            max_results = 20,
            q = "Don't worry, I'll get encoded!"
        })
        .SetQueryParams(myDictionary)
        .SetQueryParam("q", "overwrite q!")
    
        // extensions provided by Flurl.Http:
        .WithOAuthBearerToken("token")
        .GetJsonAsync<TResult>();
    

    查看the docs 了解更多详情。 NuGet 上提供了完整的软件包:

    PM&gt; Install-Package Flurl.Http

    或只是独立的 URL 构建器:

    PM&gt; Install-Package Flurl

    【讨论】:

    • 为什么不扩展Uri 或者从你自己的类开始而不是string
    • 从技术上讲,我确实从我自己的 Url 课程开始。以上等价于new Url("https://api.com").AppendPathSegment... 我个人更喜欢字符串扩展,因为击键次数更少,并且在文档中对它们进行了标准化,但您可以采用任何一种方式。
    • 跑题了,但是真的很不错的库,看到这个我就用了。也感谢您使用 IHttpClientFactory。
    【解决方案4】:

    与 Rostov 的帖子一样,如果您不想在项目中包含对 System.Web 的引用,可以使用 System.Net.Http.Formatting 中的 FormDataCollection 并执行以下操作:

    使用System.Net.Http.Formatting.FormDataCollection

    var parameters = new Dictionary<string, string>()
    {
        { "ham", "Glaced?" },
        { "x-men", "Wolverine + Logan" },
        { "Time", DateTime.UtcNow.ToString() },
    }; 
    var query = new FormDataCollection(parameters).ReadAsNameValueCollection().ToString();
    

    【讨论】:

      【解决方案5】:

      对于那些不想在尚未使用它的项目中包含System.Web 的人,您可以使用System.Net.Http 中的FormUrlEncodedContent 并执行以下操作:

      键值对版本

      string query;
      using(var content = new FormUrlEncodedContent(new KeyValuePair<string, string>[]{
          new KeyValuePair<string, string>("ham", "Glazed?"),
          new KeyValuePair<string, string>("x-men", "Wolverine + Logan"),
          new KeyValuePair<string, string>("Time", DateTime.UtcNow.ToString()),
      })) {
          query = content.ReadAsStringAsync().Result;
      }
      

      字典版本

      string query;
      using(var content = new FormUrlEncodedContent(new Dictionary<string, string>()
      {
          { "ham", "Glaced?"},
          { "x-men", "Wolverine + Logan"},
          { "Time", DateTime.UtcNow.ToString() },
      })) {
          query = content.ReadAsStringAsync().Result;
      }
      

      【讨论】:

      • 为什么要使用 using 语句?
      • 可能会释放资源,但这太过分了。不要这样做。
      • 这可以通过使用 Dictionary 而不是 KVP 数组更简洁。然后使用初始化语法: { "ham", "Glazed?" }
      • @SeanB 这是个好主意,尤其是在使用某些东西添加动态/未知参数列表时。对于这个例子,因为它是一个“固定”列表,我不觉得字典的开销是值得的。
      • @Kody 你为什么说不要使用dispose?我总是丢弃,除非我有充分的理由不这样做,比如重用 HttpClient
      【解决方案6】:

      已接受答案的好部分,已修改为使用 UriBuilder.Uri.ParseQueryString() 而不是 HttpUtility.ParseQueryString():

      var builder = new UriBuilder("http://example.com");
      var query = builder.Uri.ParseQueryString();
      query["foo"] = "bar<>&-baz";
      query["bar"] = "bazinga";
      builder.Query = query.ToString();
      string url = builder.ToString();
      

      【讨论】:

      • 仅供参考:这需要引用System.Net.Http,因为ParseQueryString() 扩展方法不在System 中。
      【解决方案7】:

      为避免 taras.roshko 的回答中描述的双重编码问题并保持轻松使用查询参数的可能性,您可以使用 uriBuilder.Uri.ParseQueryString() 而不是 HttpUtility.ParseQueryString()

      【讨论】:

        【解决方案8】:

        如果我希望使用 System.Net.HttpClient 提交 http get 请求 好像没有加参数的api,这样对吗?

        是的。

        是否有任何简单的 api 可用于构建查询字符串 不涉及构建名称值集合和 url 编码 那些然后最后连接它们?

        当然:

        var query = HttpUtility.ParseQueryString(string.Empty);
        query["foo"] = "bar<>&-baz";
        query["bar"] = "bazinga";
        string queryString = query.ToString();
        

        会给你预期的结果:

        foo=bar%3c%3e%26-baz&bar=bazinga
        

        您可能还会发现 UriBuilder 类很有用:

        var builder = new UriBuilder("http://example.com");
        builder.Port = -1;
        var query = HttpUtility.ParseQueryString(builder.Query);
        query["foo"] = "bar<>&-baz";
        query["bar"] = "bazinga";
        builder.Query = query.ToString();
        string url = builder.ToString();
        

        会给你预期的结果:

        http://example.com/?foo=bar%3c%3e%26-baz&bar=bazinga
        

        您可以安全地提供给您的 HttpClient.GetAsync 方法。

        【讨论】:

        • 这在 .NET 中的 url 处理方面绝对是最好的。无需手动进行 url 编码和进行字符串连接或字符串构建器或其他任何操作。 UriBuilder 类甚至会使用 Fragment 属性为您处理带有片段 (#) 的 url。我见过很多人犯了手动处理 url 而不是使用内置工具的错误。
        • NameValueCollection.ToString() 通常不会生成查询字符串,并且没有文档说明对 ParseQueryString 的结果执行 ToString 会生成新的查询字符串,因此可能随时中断因为该功能无法保证。
        • HttpUtility 在 System.Web 中,在可移植运行时中不可用。奇怪的是,这个功能在类库中并不普遍可用。
        • 这个方案很卑鄙。 .Net 应该有适当的查询字符串生成器。
        • 最好的解决方案隐藏在内部类中,您只能通过调用传递空字符串的实用方法获得,这一事实不能完全称为优雅的解决方案。
        【解决方案9】:

        TL;DR:不要使用接受的版本,因为它在处理 unicode 字符方面完全被破坏了,并且永远不要使用内部 API

        我实际上发现接受的解决方案存在奇怪的双重编码问题:

        因此,如果您正在处理需要编码的字符,则可接受的解决方案会导致双重编码:

        • 查询参数使用NameValueCollection 索引器自动编码(这使用UrlEncodeUnicode,不是常规预期的UrlEncode(!)
        • 然后,当您调用 uriBuilder.Uri 时,它会使用构造函数创建新的 Uri会再编码一次(普通 url 编码)
        • 通过uriBuilder.ToString() 无法避免这种情况(即使这会返回正确的Uri,其中 IMO 至少是不一致的,可能是一个错误,但这是另一个问题)然后使用 HttpClient 方法接受字符串- 客户端仍然会从您传递的字符串中创建Uri,如下所示:new Uri(uri, UriKind.RelativeOrAbsolute)

        小而完整的复制品:

        var builder = new UriBuilder
        {
            Scheme = Uri.UriSchemeHttps,
            Port = -1,
            Host = "127.0.0.1",
            Path = "app"
        };
        
        NameValueCollection query = HttpUtility.ParseQueryString(builder.Query);
        
        query["cyrillic"] = "кирилиця";
        
        builder.Query = query.ToString();
        Console.WriteLine(builder.Query); //query with cyrillic stuff UrlEncodedUnicode, and that's not what you want
        
        var uri = builder.Uri; // creates new Uri using constructor which does encode and messes cyrillic parameter even more
        Console.WriteLine(uri);
        
        // this is still wrong:
        var stringUri = builder.ToString(); // returns more 'correct' (still `UrlEncodedUnicode`, but at least once, not twice)
        new HttpClient().GetStringAsync(stringUri); // this creates Uri object out of 'stringUri' so we still end up sending double encoded cyrillic text to server. Ouch!
        

        输出:

        ?cyrillic=%u043a%u0438%u0440%u0438%u043b%u0438%u0446%u044f
        
        https://127.0.0.1/app?cyrillic=%25u043a%25u0438%25u0440%25u0438%25u043b%25u0438%25u0446%25u044f
        

        如您所见,无论您使用uribuilder.ToString() + httpClient.GetStringAsync(string) 还是uriBuilder.Uri + httpClient.GetStringAsync(Uri),最终都会发送双重编码参数

        固定示例可能是:

        var uri = new Uri(builder.ToString(), dontEscape: true);
        new HttpClient().GetStringAsync(uri);
        

        但这使用 过时 Uri 构造函数

        P.S 在我最新的 Windows Server .NET 上,Uri 带有 bool doc 注释的构造函数说“已过时,dontEscape 始终为假”,但实际上按预期工作(跳过转义)

        所以它看起来像另一个错误......

        即使这是完全错误的 - 它会将 UrlEncodedUnicode 发送到服务器,而不仅仅是服务器期望的 UrlEncoded

        更新:还有一件事是,NameValueCollection 实际上是 UrlEncodeUnicode,它不应该再使用并且与常规 url.encode/decode 不兼容(请参阅NameValueCollection to URL Query?)。

        所以底线是:永远不要在NameValueCollection query = HttpUtility.ParseQueryString(builder.Query);上使用这个hack,因为它会弄乱你的unicode查询参数。只需手动构建查询并将其分配给 UriBuilder.Query,它将进行必要的编码,然后使用 UriBuilder.Uri 获取 Uri。

        使用不应该这样使用的代码伤害自己的主要例子

        【讨论】:

        • 你能在这个答案中添加一个完整的实用函数吗?
        • 我对此表示赞同:我通读了答案,但没有结论。有没有明确的答案?
        • 我也想看看这个问题的最终答案
        • 这个问题的最终答案是使用var namedValues = HttpUtility.ParseQueryString(builder.Query),但不是使用返回的NameValueCollection,而是立即将其转换为字典,如下所示:var dic = values.ToDictionary(x =&gt; x, x =&gt; values[x]); 向字典中添加新值,然后将其传递给FormUrlEncodedContent 的构造函数并在其上调用ReadAsStringAsync().Result。这为您提供了一个正确编码的查询字符串,您可以将其分配回 UriBuilder。
        • 对于那些询问没有双重编码问题的替代方案的人 - 只需使用 uriBuilder.Uri.ParseQueryString() 而不是 HttpUtility.ParseQueryString()
        【解决方案10】:

        我找不到比创建将字典转换为 QueryStringFormat 的扩展方法更好的解决方案。 Waleed A.K. 提出的解决方案也不错。

        按照我的解决方案:

        创建扩展方法:

        public static class DictionaryExt
        {
            public static string ToQueryString<TKey, TValue>(this Dictionary<TKey, TValue> dictionary)
            {
                return ToQueryString<TKey, TValue>(dictionary, "?");
            }
        
            public static string ToQueryString<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, string startupDelimiter)
            {
                string result = string.Empty;
                foreach (var item in dictionary)
                {
                    if (string.IsNullOrEmpty(result))
                        result += startupDelimiter; // "?";
                    else
                        result += "&";
        
                    result += string.Format("{0}={1}", item.Key, item.Value);
                }
                return result;
            }
        }
        

        还有他们:

        var param = new Dictionary<string, string>
                  {
                    { "param1", "value1" },
                    { "param2", "value2" },
                  };
        param.ToQueryString(); //By default will add (?) question mark at begining
        //"?param1=value1&param2=value2"
        param.ToQueryString("&"); //Will add (&)
        //"&param1=value1&param2=value2"
        param.ToQueryString(""); //Won't add anything
        //"param1=value1&param2=value2"
        

        【讨论】:

        • 此解决方案缺少正确的参数 URL 编码,并且不适用于包含“无效”字符的值
        • 欢迎更新答案并补充缺少的编码行,就一行代码!
        【解决方案11】:

        感谢“Darin Dimitrov”,这是扩展方法。

         public static partial class Ext
        {
            public static Uri GetUriWithparameters(this Uri uri,Dictionary<string,string> queryParams = null,int port = -1)
            {
                var builder = new UriBuilder(uri);
                builder.Port = port;
                if(null != queryParams && 0 < queryParams.Count)
                {
                    var query = HttpUtility.ParseQueryString(builder.Query);
                    foreach(var item in queryParams)
                    {
                        query[item.Key] = item.Value;
                    }
                    builder.Query = query.ToString();
                }
                return builder.Uri;
            }
        
            public static string GetUriWithparameters(string uri,Dictionary<string,string> queryParams = null,int port = -1)
            {
                var builder = new UriBuilder(uri);
                builder.Port = port;
                if(null != queryParams && 0 < queryParams.Count)
                {
                    var query = HttpUtility.ParseQueryString(builder.Query);
                    foreach(var item in queryParams)
                    {
                        query[item.Key] = item.Value;
                    }
                    builder.Query = query.ToString();
                }
                return builder.Uri.ToString();
            }
        }
        

        【讨论】:

          【解决方案12】:

          由于我必须重用这几次,我想出了这个类,它只是帮助抽象查询字符串的组成方式。

          public class UriBuilderExt
          {
              private NameValueCollection collection;
              private UriBuilder builder;
          
              public UriBuilderExt(string uri)
              {
                  builder = new UriBuilder(uri);
                  collection = System.Web.HttpUtility.ParseQueryString(string.Empty);
              }
          
              public void AddParameter(string key, string value) {
                  collection.Add(key, value);
              }
          
              public Uri Uri{
                  get
                  {
                      builder.Query = collection.ToString();
                      return builder.Uri;
                  }
              }
          
          }
          

          使用将简化为:

          var builder = new UriBuilderExt("http://example.com/");
          builder.AddParameter("foo", "bar<>&-baz");
          builder.AddParameter("bar", "second");
          var uri = builder.Uri;
          

          这将返回 uri: http://example.com/?foo=bar%3c%3e%26-baz&bar=second

          【讨论】:

            【解决方案13】:

            我正在开发的RFC 6570 URI Template library 能够执行此操作。根据该 RFC 为您处理所有编码。在撰写本文时,有一个 beta 版本可用,它不被认为是稳定的 1.0 版本的唯一原因是文档没有完全满足我的期望(请参阅问题 #17#18#32、@987654325 @)。

            您可以单独构建一个查询字符串:

            UriTemplate template = new UriTemplate("{?params*}");
            var parameters = new Dictionary<string, string>
              {
                { "param1", "value1" },
                { "param2", "value2" },
              };
            Uri relativeUri = template.BindByName(parameters);
            

            或者你可以建立一个完整的 URI:

            UriTemplate template = new UriTemplate("path/to/item{?params*}");
            var parameters = new Dictionary<string, string>
              {
                { "param1", "value1" },
                { "param2", "value2" },
              };
            Uri baseAddress = new Uri("http://www.example.com");
            Uri relativeUri = template.BindByName(baseAddress, parameters);
            

            【讨论】:

              【解决方案14】:

              或者只是使用我的 Uri 扩展

              代码

              public static Uri AttachParameters(this Uri uri, NameValueCollection parameters)
              {
                  var stringBuilder = new StringBuilder();
                  string str = "?";
                  for (int index = 0; index < parameters.Count; ++index)
                  {
                      stringBuilder.Append(str + parameters.AllKeys[index] + "=" + parameters[index]);
                      str = "&";
                  }
                  return new Uri(uri + stringBuilder.ToString());
              }
              

              用法

              Uri uri = new Uri("http://www.example.com/index.php").AttachParameters(new NameValueCollection
                                                                                         {
                                                                                             {"Bill", "Gates"},
                                                                                             {"Steve", "Jobs"}
                                                                                         });
              

              结果

              http://www.example.com/index.php?Bill=Gates&Steve=Jobs

              【讨论】:

              • 你没有忘记 URL 编码吗?
              • 这是使用扩展创建清晰、有用的助手的一个很好的例子。如果您将此与公认的答案结合起来,您就可以构建一个可靠的 RestClient
              【解决方案15】:

              Darin 提供了一个有趣而聪明的解决方案,这可能是另一种选择:

              public class ParameterCollection
              {
                  private Dictionary<string, string> _parms = new Dictionary<string, string>();
              
                  public void Add(string key, string val)
                  {
                      if (_parms.ContainsKey(key))
                      {
                          throw new InvalidOperationException(string.Format("The key {0} already exists.", key));
                      }
                      _parms.Add(key, val);
                  }
              
                  public override string ToString()
                  {
                      var server = HttpContext.Current.Server;
                      var sb = new StringBuilder();
                      foreach (var kvp in _parms)
                      {
                          if (sb.Length > 0) { sb.Append("&"); }
                          sb.AppendFormat("{0}={1}",
                              server.UrlEncode(kvp.Key),
                              server.UrlEncode(kvp.Value));
                      }
                      return sb.ToString();
                  }
              }
              

              所以在使用它时,你可以这样做:

              var parms = new ParameterCollection();
              parms.Add("key", "value");
              
              var url = ...
              url += "?" + parms;
              

              【讨论】:

              • 您可能希望在 for 循环内分别编码 kvp.Keykvp.Value,而不是在完整的查询字符串中(因此不编码 &amp;= 字符)。
              • 谢谢迈克。其他提议的解决方案(涉及 NameValueCollection)对我不起作用,因为我在 PCL 项目中,所以这是一个完美的选择。对于在客户端工作的其他人,server.UrlEncode 可以替换为WebUtility.UrlEncode
              猜你喜欢
              • 2016-05-10
              • 2012-01-17
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-10-23
              • 2013-08-15
              • 2018-12-26
              相关资源
              最近更新 更多