Crear cadena de consulta para System.Net.HttpClient get

184

Si deseo enviar una solicitud de obtención de http utilizando System.Net.HttpClient parece que no hay una API para agregar parámetros, ¿es esto correcto?

¿Hay alguna api simple disponible para construir la cadena de consulta que no implique construir una colección de valores de nombre y url que los codifique y luego finalmente los concatene? Esperaba usar algo como la api de RestSharp (es decir, AddParameter (..))

NeoDarque
fuente
@Michael Perrenoud es posible que desee reconsiderar el uso de la respuesta aceptada con caracteres que necesitan codificación, consulte mi explicación a continuación
inmigrante ilegal

Respuestas:

309

Si deseo enviar una solicitud de obtención de http utilizando System.Net.HttpClient parece que no hay una API para agregar parámetros, ¿es esto correcto?

Si.

¿Hay alguna api simple disponible para construir la cadena de consulta que no implique construir una colección de valores de nombre y url que los codifique y luego finalmente los concatene?

Por supuesto:

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

te dará el resultado esperado:

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

También puede encontrar UriBuilderútil la clase:

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

te dará el resultado esperado:

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

que podría alimentar con más seguridad su HttpClient.GetAsyncmétodo.

Darin Dimitrov
fuente
9
Eso es lo mejor en términos de manejo de URL en .NET. No es necesario codificar url manualmente y realizar concatenaciones de cadenas o constructores de cadenas o lo que sea. La clase UriBuilder incluso manejará las URL con fragmentos ( #) para usted usando la propiedad Fragment. He visto a muchas personas cometer el error de manejar manualmente las URL en lugar de usar las herramientas integradas.
Darin Dimitrov
66
NameValueCollection.ToString()normalmente no realiza cadenas de consulta, y no hay documentación que indique que hacer un ToStringresultado ParseQueryStringdará como resultado una nueva cadena de consulta, por lo que podría romperse en cualquier momento ya que no hay garantía en esa funcionalidad.
Matthew
11
HttpUtility está en System.Web que no está disponible en tiempo de ejecución portátil. Parece extraño que esta funcionalidad no esté disponible de manera más general en las bibliotecas de clases.
Chris Eldredge
82
Esta solución es despreciable. .Net debería tener un generador de cadena de consulta adecuado.
Kugel
8
El hecho de que la mejor solución esté oculta en la clase interna a la que solo se puede acceder llamando a un método de utilidad que pasa una cadena vacía no se puede llamar exactamente una solución elegante.
Kugel
79

Para aquellos que no quieren incluir System.Weben los proyectos que aún no lo utilizan, se puede utilizar FormUrlEncodedContentdesde System.Net.Httpy hacer algo como lo siguiente:

versión 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;
}

versión del diccionario

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;
}
Rostov
fuente
¿Por qué usas una declaración de uso?
Ian Warburton
Es probable que libere recursos, pero esto es exagerado. No hagas esto.
Kody
55
Esto puede ser más conciso usando Dictionary <string, string> en lugar de la matriz KVP. Luego, usando la sintaxis de inicializador de: {"ham", "Glazed?" }
Sean B
@SeanB Esa es una buena idea, especialmente cuando se usa algo para agregar una lista dinámica / desconocida de parámetros. Para este ejemplo, dado que es una lista "fija", no sentí que la sobrecarga de un diccionario valiera la pena.
Rostov el
66
@Kody ¿Por qué dices que no uses dispose? Siempre dispongo a menos que tenga una buena razón para no hacerlo, como reutilizar HttpClient.
Dan Friedman
41

TL; DR: no use la versión aceptada ya que está completamente rota en relación con el manejo de caracteres Unicode, y nunca use API interna

De hecho, he encontrado un extraño problema de doble codificación con la solución aceptada:

Entonces, si se trata de caracteres que necesitan ser codificados, la solución aceptada conduce a una doble codificación:

  • los parámetros de consulta se codifican automáticamente mediante el uso de NameValueCollectionindexador ( y esto utiliza UrlEncodeUnicode, no se espera regularmente UrlEncode(!) )
  • Luego, cuando lo llama uriBuilder.Uri, crea un nuevo Uriconstructor usando que codifica una vez más (codificación de URL normal)
  • Eso no se puede evitar haciendouriBuilder.ToString() (a pesar de que esto devuelve correcto Uriqué IMO es al menos inconsistencia, tal vez un error, pero esa es otra pregunta) y luego usando el HttpClientmétodo de aceptación de cadena: el cliente aún crea a Uripartir de su cadena pasada de esta manera:new Uri(uri, UriKind.RelativeOrAbsolute)

Pequeño pero completo repro:

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!

Salida:

?cyrillic=%u043a%u0438%u0440%u0438%u043b%u0438%u0446%u044f

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

Como puede ver, no importa si hace uribuilder.ToString()+ httpClient.GetStringAsync(string)o uriBuilder.Uri+ httpClient.GetStringAsync(Uri), termina enviando un parámetro codificado doble

Ejemplo fijo podría ser:

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

Pero esto usa un constructor obsoleto Uri

PS en mi último .NET en Windows Server, el Uriconstructor con el comentario de bool doc dice "obsoleto, dontEscape siempre es falso", pero en realidad funciona como se espera (se escapa)

Entonces parece otro error ...

E incluso esto es simplemente incorrecto: envía UrlEncodedUnicode al servidor, no solo UrlEncoded lo que el servidor espera

Actualización: una cosa más es que NameValueCollection realmente hace UrlEncodeUnicode, que se supone que ya no se debe usar y es incompatible con url.encode / decode (¿Ver NameValueCollection to URL Query? ).

Entonces, la conclusión es: nunca use este truco,NameValueCollection query = HttpUtility.ParseQueryString(builder.Query); ya que alterará sus parámetros de consulta Unicode. Simplemente construya la consulta manualmente y asígnele lo UriBuilder.Queryque hará la codificación necesaria y luego use Uri UriBuilder.Uri.

Primer ejemplo de lastimarse a sí mismo usando un código que no se debe usar de esta manera

inmigrante ilegal
fuente
16
¿Podría agregar una función de utilidad completa a esta respuesta que funciona?
mafu
8
Respaldo mafu en esto: leí la respuesta pero no tengo una conclusión. ¿Hay una respuesta definitiva a esto?
Richard Griffiths
3
También me gustaría ver la respuesta definitiva para este problema
Pones
La respuesta definitiva a este problema es usar var namedValues = HttpUtility.ParseQueryString(builder.Query), pero luego, en lugar de usar el NameValueCollection devuelto, inmediatamente conviértalo en un diccionario de la siguiente manera: var dic = values.ToDictionary(x => x, x => values[x]); agregue nuevos valores al diccionario, luego páselo al constructor de FormUrlEncodedContenty solicítelo ReadAsStringAsync().Result. Eso le proporciona una cadena de consulta codificada correctamente, que puede asignar de nuevo al UriBuilder.
Triynko
En realidad se puede utilizar sólo NamedValueCollection.ToString en lugar de todo eso, pero sólo si se cambia un app.config / web.config configuración que evita que ASP.NET de utilizar el '% uXXXX' codificación: <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />. No dependería de este comportamiento, por lo que es mejor usar la clase FormUrlEncodedContent, como lo demostró una respuesta anterior: stackoverflow.com/a/26744471/88409
Triynko
41

En un proyecto ASP.NET Core puede usar la clase QueryHelpers.

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

var response = await client.GetAsync(QueryHelpers.AddQueryString("/api/", query));
Magu
fuente
2
Es molesto que, aunque con este proceso, aún no pueda enviar valores múltiples para la misma clave. Si desea enviar "bar" y "bar2" como parte de solo foo, no es posible.
m0g
2
Esta es una gran respuesta para aplicaciones modernas, funciona en mi escenario, simple y limpio. Sin embargo, no necesito ningún mecanismo de escape, no probado.
Patrick Stalph
Este paquete NuGet está dirigido a .NET standard 2.0, lo que significa que puede usarlo en el .NET framework completo 4.6.1+
eddiewould
24

Es posible que desee consultar Flurl [divulgación: soy el autor], un creador de URL fluido con lib complementario opcional que lo extiende en un cliente REST completo.

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

Echa un vistazo a los documentos para más detalles. El paquete completo está disponible en NuGet:

PM> Install-Package Flurl.Http

o solo el creador de URL independiente:

PM> Install-Package Flurl

Todd Menier
fuente
2
¿Por qué no ampliar Urio comenzar con su propia clase en lugar de string?
mpen
2
Técnicamente comencé con mi propia Urlclase. Lo anterior es equivalente a new Url("https://api.com").AppendPathSegment...Personalmente. Prefiero las extensiones de cadena debido a menos pulsaciones de teclas y estandarizadas en los documentos, pero puede hacerlo de cualquier manera.
Todd Menier
Fuera de tema, pero muy buena lib, lo estoy usando después de ver esto. Gracias por usar IHttpClientFactory también.
Ed S.
4

A lo largo de las mismas líneas que el post de Rostov, si no se desea incluir una referencia a System.Websu proyecto, puede utilizar FormDataCollectiondesde System.Net.Http.Formattingy hacer algo como lo siguiente:

Utilizando 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();
cwills
fuente
3

Darin ofreció una solución interesante e inteligente, y aquí hay algo que puede ser otra opción:

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

y así, al usarlo, puede hacer esto:

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

var url = ...
url += "?" + parms;
Mike Perrenoud
fuente
55
Desea codificar kvp.Keyy por kvp.Valueseparado dentro del bucle for, no en la cadena de consulta completa (por lo tanto, no codifica los caracteres &y =).
Matthew
Gracias Mike Las otras soluciones propuestas (que implican NameValueCollection) no funcionaron para mí porque estoy en un proyecto PCL, por lo que esta fue una alternativa perfecta. Para otras personas que trabajan en el lado del cliente, server.UrlEncodese puede reemplazar conWebUtility.UrlEncode
BCA
2

O simplemente usando mi extensión Uri

Código

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

Uso

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

Resultado

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

Ratskey romano
fuente
27
¿No olvidaste la codificación de URL?
Kugel
1
Este es un gran ejemplo del uso de extensiones para crear ayudantes claros y útiles. Si se combina esto con la respuesta aceptada que está en su camino a la construcción de una sólida RESTClient
emran
2

La biblioteca de plantillas RFC 6570 URI que estoy desarrollando es capaz de realizar esta operación. Toda la codificación se maneja para usted de acuerdo con ese RFC. En el momento de escribir este artículo, hay disponible una versión beta y la única razón por la que no se considera una versión 1.0 estable es que la documentación no cumple completamente mis expectativas (consulte los problemas # 17 , # 18 , # 32 , # 43 ).

Puede construir una cadena de consulta solo:

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

O podrías construir un URI completo:

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);
Sam Harwell
fuente
1

Dado que tengo que reutilizar estas pocas veces, se me ocurrió esta clase que simplemente ayuda a abstraer cómo se compone la cadena de consulta.

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

}

El uso se simplificará a algo como esto:

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

eso devolverá la uri: http://example.com/?foo=bar%3c%3e%26-baz&bar=second

Jaider
fuente
1

Para evitar el problema de doble codificación descrito en la respuesta de taras.roshko y mantener la posibilidad de trabajar fácilmente con parámetros de consulta, puede usar en uriBuilder.Uri.ParseQueryString()lugar de HttpUtility.ParseQueryString().

Valeriy Lyuchyn
fuente
1

Buena parte de la respuesta aceptada, modificada para usar UriBuilder.Uri.ParseQueryString () en lugar de 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();
Jpsy
fuente
FYI: Esto requiere una referencia a System.Net.Http ya que el ParseQueryString()método de extensión no está dentro System.
Sunny Patel
0

Gracias a "Darin Dimitrov", este es el método de extensión.

 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();
    }
}
Waleed AK
fuente
-1

No pude encontrar una mejor solución que crear un método de extensión para convertir un Diccionario a QueryStringFormat. La solución propuesta por Waleed AK también es buena.

Sigue mi solución:

Crea el método de extensión:

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

Y ellos:

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"
Diego Mendes
fuente
1
A esta solución le falta la codificación correcta de los parámetros de la URL y no funcionará con valores que contengan caracteres 'inválidos'
Xavier Poinas
Siéntase libre de actualizar la respuesta y agregar la línea de codificación que falta, ¡es solo una línea de código!
Diego Mendes el