编译System.Net.HttpClient的查询字符串

如果我想使用System.Net.HttpClient提交一个http获取请求,似乎没有api来添加参数,这是正确的吗?

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

280122 次浏览
如果我想使用System.Net.HttpClient提交一个http get请求 似乎没有API来添加参数,这是正确的吗?< / p >

是的。

是否有任何简单的api可用来构建查询字符串 不涉及构建名称值集合和url编码 然后最后把它们连接起来?< / p >

肯定的:

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方法。

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;

或者简单地使用我的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

你可能想看看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

我正在开发的rfc6570 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);

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

keyvaluepair版本

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

由于我必须重用这几次,所以我提出了这个类,它只是帮助抽象查询字符串是如何组成的。

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 < / p >

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

我找不到比创建一个扩展方法来将Dictionary转换为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"

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

实际上,我在公认的解决方案中发现了奇怪的双编码问题:

所以,如果你处理的字符需要编码,接受的解决方案导致双重编码:

  • 查询参数通过使用NameValueCollection索引器(并且使用了__ABC1,而不是常规的UrlEncode(!))自动编码
  • 然后,当你调用uriBuilder.Uri时,它使用构造函数哪一个是再次编码创建新的Uri(普通url编码)
  • 这不能通过执行uriBuilder.ToString()来避免(即使这返回正确的Uri,在我看来至少是不一致的,可能是一个bug,但这是另一个问题),然后使用HttpClient方法接受字符串- 客户端仍然从你传递的字符串中创建__ABC1,像这样: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.在我最新的。net在Windows服务器上,Uri构造函数与bool doc注释说“过时,dontEscape总是假的”,但实际上工作如预期(跳过转义)

所以它看起来像另一个bug…

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

更新:还有一件事是,NameValueCollection实际上做UrlEncodeUnicode,这是不应该再使用的,与常规url不兼容。encode/decode(参见NameValueCollection到URL查询?)。

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

使用不应该这样使用的代码而伤害自己的最好例子

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

在一个ASP。NET Core项目可以使用QueryHelpers类,在ASP. NET的Microsoft.AspNetCore.WebUtilities命名空间中可用。NET Core,或其他消费者的.NET标准2.0 NuGet包:

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


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

接受答案的好部分,修改为使用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();

与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();
HttpClient client = new HttpClient();
var uri = Environment.GetEnvironmentVariable("URL of Api");
                

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

响应语法eg:

HttpResponseMessage response = client.GetAsync(requesturi).Result;

希望它对你有用。

我的答案与公认的答案/其他答案在全球范围内并无不同。我只是尝试为Uri类型创建一个扩展方法,它接受可变数量的参数。

public static class UriExtensions
{
public static Uri AddParameter(this Uri url, params (string Name, string Value)[] @params)
{
if (!@params.Any())
{
return url;
}


UriBuilder uriBuilder = new(url);


NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query);


foreach (var param in @params)
{
query[param.Name] = param.Value.Trim();
}


uriBuilder.Query = query.ToString();


return uriBuilder.Uri;
}
}

使用的例子:

var uri = new Uri("http://someuri.com")
.AddParameter(
("p1.name", "p1.value"),
("p2.name", "p2.value"),
("p3.name", "p3.value"));