建立System.Net.HttpClient get的查询字符串


184

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

是否有任何简单的API可以用来构建查询字符串,而无需构建名称值集合并对其进行url编码,然后最终将它们串联起来?我希望使用类似RestSharp的api的东西(即AddParameter(..))


@Michael Perrenoud,您可能想重新考虑将接受的答案与需要编码的字符一起使用,请参阅下面的说明
非法移民

Answers:


309

如果我希望使用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方法。


9
就.NET中的URL处理而言,这绝对是最好的选择。无需手动进行url编码并进行字符串连接或字符串构建器等操作。UriBuilder类甚至#可以使用Fragment属性为您处理带有片段()的网址。我已经看到很多人在手动处理网址而不是使用内置工具方面犯了错误。
Darin Dimitrov 2013年

6
NameValueCollection.ToString()通常不会生成查询字符串,也没有文档说明ToString对的结果执行ParseQueryString会导致新的查询字符串,因此由于无法保证该功能,因此随时可能中断。
马修

11
HttpUtility位于System.Web中,在便携式运行时上不可用。奇怪的是,该功能在类库中并不普遍可用。
克里斯·埃尔德雷奇

82
这种解决方案是卑鄙的。.Net应该具有适当的querystring构建器。
Kugel 2013年

8
最好的解决方案隐藏在内部类中,您只能通过调用传入空字符串的实用程序方法才能将其隐藏,这一事实不能完全称为一种优雅的解决方案。
Kugel 2014年

79

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

键值对版本

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语句?
伊恩·沃伯顿

可能会释放资源,但这是最重要的。不要这样
Kody

5
通过使用Dictionary <string,string>而不是KVP数组,可以更加简洁。然后使用以下初始化语法:{“ ham”,“ Glazed?” }
Sean B

@SeanB是个好主意,尤其是在使用某些东西添加动态/未知参数列表时。对于此示例,由于它是“固定”列表,所以我觉得字典的开销并不值得。
罗斯托夫

6
@Kody为什么说不使用dispose?除非我有充分的理由不这样做,否则我总是处置,例如重用HttpClient
丹·弗里德曼

41

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构造函数

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

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

甚至这是完全错误的-它向服务器发送UrlEncodedUnicode,而不仅仅是服务器期望的UrlEncoded

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

因此,最重要的是:永远不要使用此技巧,NameValueCollection query = HttpUtility.ParseQueryString(builder.Query);因为它会弄乱您的unicode查询参数。只需手动构建查询并将其分配给UriBuilder.Query将进行必要编码的查询,然后使用即可获取Uri UriBuilder.Uri

通过使用不应像这样使用的代码来伤害自己的主要示例


16
您能在此答案中添加完整的实用程序功能吗?
mafu

8
我第二个数学家:我读了答案,但没有结论。是否有明确的答案?
理查德·格里菲斯

3
我也想看到这个问题的明确答案
Pones

解决此问题的明确答案是使用var namedValues = HttpUtility.ParseQueryString(builder.Query),但不要立即使用返回的NameValueCollection,而是将其立即转换为Dictionary,如下所示:var dic = values.ToDictionary(x => x, x => values[x]); 向字典中添加新值,然后将其传递给的构造函数FormUrlEncodedContent并对其进行调用ReadAsStringAsync().Result。这样可以为您提供正确编码的查询字符串,您可以将其分配回UriBuilder。
Triynko '16

实际上,可以使用NamedValueCollection.ToString来代替所有这些,但是仅当您更改了阻止ASP.NET使用'%uXXXX'编码的app.config / web.config设置时:<add key="aspnet:DontUsePercentUUrlEncoding" value="true" />。我不会依赖此行为,因此最好使用FormUrlEncodedContent类,如先前的答案所示:stackoverflow.com/a/26744471/88409
Triynko

41

在ASP.NET Core项目中,可以使用QueryHelpers类。

// using Microsoft.AspNetCore.WebUtilities;
var query = new Dictionary<string, string>
{
    ["foo"] = "bar",
    ["foo2"] = "bar2",
    // ...
};

var response = await client.GetAsync(QueryHelpers.AddQueryString("/api/", query));

2
令人烦恼的是,尽管通过此过程,您仍然无法为同一个键发送多个值。如果要将“ bar”和“ bar2”作为foo的一部分发送,则不可能。
m0g

2
对于现代应用程序来说,这是一个很好的答案,可以在我的场景中使用,简单干净。但是,我不需要任何转义机制-未经测试。
帕特里克·斯塔夫

此NuGet软件包针对.NET标准2.0,这意味着您可以在完整的.NET Framework 4.6.1+上使用它
eddiewould

24

您可能想看看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>();

查看文档以获取更多详细信息。完整的软件包可在NuGet上找到:

PM> Install-Package Flurl.Http

或只是独立的URL构建器:

PM> Install-Package Flurl


2
为什么不扩展Uri或从您自己的课开始而不是string
mpen 2014年

2
从技术上讲,我确实从我自己的Url课开始。上面的内容等同于new Url("https://api.com").AppendPathSegment...Personally我更喜欢使用字符串扩展名,因为它减少了击键次数,并且在文档中对其进行了标准化,但是您可以选择这两种方式。
Todd Menier 2014年

脱离主题,但真的不错的lib,我在看到这个之后就在使用它。也感谢您使用IHttpClientFactory。
Ed S.

4

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

使用 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();

3

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;

5
你会想编码kvp.Keykvp.Value内部分别for循环,而不是完整的查询字符串(因此不编码&,和=字符)。
马修·

谢谢迈克。我提出的其他解决方案(涉及NameValueCollection)对我不起作用,因为我在PCL项目中,所以这是一个完美的选择。对于在客户端工作的其他用户,server.UrlEncode可以用WebUtility.UrlEncode
BCA

2

或者只是使用我的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


27
您是否忘记了URL编码?
Kugel 2013年

1
这是使用扩展创建清晰,有用的帮助程序的一个很好的例子。如果将此与已接受的答案结合起来,则说明您正在构建可靠的RestClient
Emran 2014年

2

我正在开发的RFC 6570 URI模板库能够执行此操作。所有编码均根据该RFC为您处理。在撰写本文时,beta版本已经可用,并且不被认为是稳定的1.0版本的唯一原因是该文档无法完全满足我的期望(请参阅问题#17#18#32#43)。

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

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

1

由于必须重用这几次,因此提出了此类,该类仅用于抽象查询字符串的组成方式。

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


1

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


1

可接受答案的好一部分,已修改为使用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.HttpParseQueryString()System
Sunny Patel

0

感谢“ 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();
    }
}

-1

除了创建将字典转换为QueryStringFormat的扩展方法外,我找不到更好的解决方案。Waleed AK提出的解决方案也很好。

按照我的解决方案:

创建扩展方法:

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"

1
此解决方案缺少参数的正确URL编码,并且不适用于包含“无效”字符的值
Xavier Poinas

随时更新答案并添加缺少的编码行,这只是一行代码!
Diego Mendes
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.