Wie serialisiere ich ein Objekt in das Abfragezeichenfolgenformat?

76

Wie serialisiere ich ein Objekt in das Abfragezeichenfolgenformat? Ich kann auf Google anscheinend keine Antwort finden. Vielen Dank.

Hier ist das Objekt, das ich als Beispiel serialisieren werde.

public class EditListItemActionModel
{
    public int? Id { get; set; }
    public int State { get; set; }
    public string Prefix { get; set; }
    public string Index { get; set; }
    public int? ParentID { get; set; }
}
Benjamin
quelle
Warum erstellen Sie nicht Ihre eigene Funktion, um auf diese Weise zu serialisieren?
James Black
Sie möchten am Ende Folgendes haben: Id = 1 & State = CA & Prefix = Mr ... so etwas? Wenn ja, stimme ich @James zu.
Bob Kaufman
3
@ James Wow, ist das der einzige Weg? Ich dachte, irgendwo ist etwas in .NET eingebaut. Ich denke irgendwie wie die Umkehrung des MVC-Modellbinders. Es muss eine Methode dafür geben, oder?
Benjamin
Wenn es keine eingebaute Funktion gibt, können Sie mir einen Hinweis geben, wie man eine schreibt?
Benjamin
3
Flurl ist ein URL-Builder / HTTP-Client, der Objekte häufig für Namen-Wert-Paar-ähnliche Dinge verwendet (Abfragezeichenfolgen, Header, URL-codierte Formularwerte usw.). SetQueryParamsmacht genau das, wonach Sie suchen. Wenn Sie nur den URL-Builder und nicht alle HTTP-Inhalte benötigen, finden Sie ihn hier . [Haftungsausschluss: Ich bin der Autor]
Todd Menier

Antworten:

110

Ich bin mir zu 99% sicher, dass es dafür keine integrierte Dienstprogrammmethode gibt. Dies ist keine sehr häufige Aufgabe, da ein Webserver normalerweise nicht mit einer URLEncoded-Schlüssel- / Wertzeichenfolge antwortet .

Wie denkst du über das Mischen von Reflexion und LINQ? Das funktioniert:

var foo = new EditListItemActionModel() {
  Id = 1,
  State = 26,
  Prefix = "f",
  Index = "oo",
  ParentID = null
};

var properties = from p in foo.GetType().GetProperties()
                 where p.GetValue(foo, null) != null
                 select p.Name + "=" + HttpUtility.UrlEncode(p.GetValue(foo, null).ToString());

// queryString will be set to "Id=1&State=26&Prefix=f&Index=oo"                  
string queryString = String.Join("&", properties.ToArray());

Aktualisieren:

Gehen Sie folgendermaßen vor, um eine Methode zu schreiben, die die QueryString-Darstellung eines 1-tiefen Objekts zurückgibt:

public string GetQueryString(object obj) {
  var properties = from p in obj.GetType().GetProperties()
                   where p.GetValue(obj, null) != null
                   select p.Name + "=" + HttpUtility.UrlEncode(p.GetValue(obj, null).ToString());

  return String.Join("&", properties.ToArray());
}

// Usage:
string queryString = GetQueryString(foo);

Sie können es auch zu einer Erweiterungsmethode machen, ohne viel zusätzlichen Aufwand

public static class ExtensionMethods {
  public static string GetQueryString(this object obj) {
    var properties = from p in obj.GetType().GetProperties()
                     where p.GetValue(obj, null) != null
                     select p.Name + "=" + HttpUtility.UrlEncode(p.GetValue(obj, null).ToString());

    return String.Join("&", properties.ToArray());
  }
}

// Usage:
string queryString = foo.GetQueryString();
Dave Ward
quelle
Das ist nett. Ich versuche, daraus eine Funktion zu machen, die einen dynamischen Parameter akzeptiert, aber ich gehe davon aus, dass ich die dynamische Linq-Auswahlsyntax durcheinander bringe. public string SerializeWithDynamicLINQ(dynamic Thing) { var Properties = Thing.GetType().GetProperties().ToArray(); return "&" + Properties.select("Property.Name") + "=" + HttpUtility.UrlEncode( Properties.select("Property").GetValue(Thing, null).ToString()); }Ich kann auch nicht herausfinden, wie man Codeblöcke in Kommentaren macht.
Benjamin
@ Benjamin: Meine Antwort wurde aktualisiert, um dabei zu helfen.
Dave Ward
1
Da Sie sagen, es ist keine sehr häufige Aufgabe. Was ist der alternative Ansatz, um viele Formularwerte zu übergeben, ohne alle RouteValueDictionary-Werte hart codieren zu müssen
Doug Chamberlain
2
Sie können dies effizienter gestalten, indem Sie den Wert jeder Eigenschaft nur einmal abrufen, indem Sie ihn einer temporären Variablen zuweisen, z let value = p.GetValue(obj, null). B. mithilfe von .
WhatIsHeDoing
Dies funktioniert nur für englische Kulturinformationen auf dem Server. Wenn Sie unter Windows ein bestimmtes Datum / Uhrzeit-Format festlegen, funktioniert dies nicht, da Sie CultureInfo Invariant als ToString-Parameter festlegen sollten. Das gleiche Problem besteht bei einem bestimmten Dezimaltrennzeichen in float / double.
Tomas Kubes
18

Aufbauend auf den guten Ideen aus anderen Kommentaren habe ich eine generische Erweiterungsmethode .ToQueryString () erstellt, die für jedes Objekt verwendet werden kann.

public static class UrlHelpers
{
    public static string ToQueryString(this object request, string separator = ",")
    {
        if (request == null)
            throw new ArgumentNullException("request");

        // Get all properties on the object
        var properties = request.GetType().GetProperties()
            .Where(x => x.CanRead)
            .Where(x => x.GetValue(request, null) != null)
            .ToDictionary(x => x.Name, x => x.GetValue(request, null));

        // Get names for all IEnumerable properties (excl. string)
        var propertyNames = properties
            .Where(x => !(x.Value is string) && x.Value is IEnumerable)
            .Select(x => x.Key)
            .ToList();

        // Concat all IEnumerable properties into a comma separated string
        foreach (var key in propertyNames)
        {
            var valueType = properties[key].GetType();
            var valueElemType = valueType.IsGenericType
                                    ? valueType.GetGenericArguments()[0]
                                    : valueType.GetElementType();
            if (valueElemType.IsPrimitive || valueElemType == typeof (string))
            {
                var enumerable = properties[key] as IEnumerable;
                properties[key] = string.Join(separator, enumerable.Cast<object>());
            }
        }

        // Concat all key/value pairs into a string separated by ampersand
        return string.Join("&", properties
            .Select(x => string.Concat(
                Uri.EscapeDataString(x.Key), "=",
                Uri.EscapeDataString(x.Value.ToString()))));
    }
}

Es funktioniert auch für Objekte mit Eigenschaften vom Typ Array und generischen Listen, wenn sie nur Grundelemente oder Zeichenfolgen enthalten.

Probieren Sie es aus, Kommentare sind willkommen: Serialisieren Sie das Objekt mit Reflection in eine Abfragezeichenfolge

Ole
quelle
8
Warum geben Sie den Code nicht hier ein? Es ist nicht so viel.
Asakura89
Nur ein wenig Syntaxzucker, wenn (request == null) eine neue ArgumentNullException (nameof (request)) auslöst;
Jan Skála
8

Basierend auf den populären Antworten musste ich den Code aktualisieren, um auch Arrays zu unterstützen. Teilen der Implementierung:

public string GetQueryString(object obj)
{
    var result = new List<string>();
    var props = obj.GetType().GetProperties().Where(p => p.GetValue(obj, null) != null);
    foreach (var p in props)
    {
        var value = p.GetValue(obj, null);
        var enumerable = value as ICollection;
        if (enumerable != null)
        {
            result.AddRange(from object v in enumerable select string.Format("{0}={1}", p.Name, HttpUtility.UrlEncode(v.ToString())));
        }
        else
        {
            result.Add(string.Format("{0}={1}", p.Name, HttpUtility.UrlEncode(value.ToString())));
        }
    }

    return string.Join("&", result.ToArray());
}
Alvis
quelle
5

Die Verwendung Json.Netwäre viel einfacher, wenn die Schlüsselwertpaare serialisiert und dann deserialisiert würden.

Hier ist ein Codebeispiel:

using Newtonsoft.Json;
using System.Web;

string ObjToQueryString(object obj)
{
     var step1 = JsonConvert.SerializeObject(obj);

     var step2 = JsonConvert.DeserializeObject<IDictionary<string, string>>(step1);

     var step3 = step2.Select(x => HttpUtility.UrlEncode(x.Key) + "=" + HttpUtility.UrlEncode(x.Value));

     return string.Join("&", step3);
}
yoel halb
quelle
Ich habe dies verwendet, als es die Box für DateTime-Eigenschaften herausgearbeitet hat
Ashley Kilgour
1
Ich mag die Einfachheit davon. Es ist großartig für flache Objekte, aber nicht gut für verschachtelte Objekte / Listen
Klicker
2
public static class UrlHelper
{
    public static string ToUrl(this Object instance)
    {
        var urlBuilder = new StringBuilder();
        var properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
        for (int i = 0; i < properties.Length; i++)
        {
            urlBuilder.AppendFormat("{0}={1}&", properties[i].Name, properties[i].GetValue(instance, null));
        }
        if (urlBuilder.Length > 1)
        {
            urlBuilder.Remove(urlBuilder.Length - 1, 1);
        }
        return urlBuilder.ToString();
    }
}
Alexander
quelle
2

Das ist meine Lösung:

public static class ObjectExtensions
{
    public static string ToQueryString(this object obj)
    {
        if (!obj.GetType().IsComplex())
        {
            return obj.ToString();
        }

        var values = obj
            .GetType()
            .GetProperties()
            .Where(o => o.GetValue(obj, null) != null);

        var result = new QueryString();

        foreach (var value in values)
        {
            if (!typeof(string).IsAssignableFrom(value.PropertyType) 
                && typeof(IEnumerable).IsAssignableFrom(value.PropertyType))
            {
                var items = value.GetValue(obj) as IList;
                if (items.Count > 0)
                {
                    for (int i = 0; i < items.Count; i++)
                    {
                        result = result.Add(value.Name, ToQueryString(items[i]));
                    }
                }
            }
            else if (value.PropertyType.IsComplex())
            {
                result = result.Add(value.Name, ToQueryString(value));
            }
            else
            {
                result = result.Add(value.Name, value.GetValue(obj).ToString());
            }
        }

        return result.Value;
    }

    private static bool IsComplex(this Type type)
    {
        var typeInfo = type.GetTypeInfo();
        if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            // nullable type, check if the nested type is simple.
            return IsComplex(typeInfo.GetGenericArguments()[0]);
        }
        return !(typeInfo.IsPrimitive
          || typeInfo.IsEnum
          || type.Equals(typeof(Guid))
          || type.Equals(typeof(string))
          || type.Equals(typeof(decimal)));
    }
}

Ich benutze diese Erweiterung für meinen Integrationstest, sie funktioniert perfekt :)

Cedric Arnould
quelle
1

Nur eine weitere Variante des oben Gesagten, aber ich wollte die vorhandenen DataMember-Attribute in meiner Modellklasse verwenden, sodass nur die Eigenschaften, die ich serialisieren möchte, in der URL in der GET-Anforderung an den Server gesendet werden.

    public string ToQueryString(object obj)
    {
        if (obj == null) return "";

        return "?" + string.Join("&", obj.GetType()
                                   .GetProperties()
                                   .Where(p => Attribute.IsDefined(p, typeof(DataMemberAttribute)) && p.GetValue(obj, null) != null)
                                   .Select(p => $"{p.Name}={Uri.EscapeDataString(p.GetValue(obj).ToString())}"));
    }
Peter Kerr
quelle
1

Vielleicht ist dieser generische Ansatz nützlich:

    public static string ConvertToQueryString<T>(T entity) where T: class
    {
        var props = typeof(T).GetProperties();

        return $"?{string.Join('&', props.Where(r=> r.GetValue(entity) != null).Select(r => $"{HttpUtility.UrlEncode(r.Name)}={HttpUtility.UrlEncode(r.GetValue(entity).ToString())}"))}";
    }
Schleppen
quelle
1

Dies ist auch für verschachtelte Objekte hilfreich

string queryString = new
{
    myClass = new MyClass
    {
        FirstName = "john",
        LastName = "doe"
    },
    myArray = new int[] { 1, 2, 3, 4 },
}.ToQueryString();

public static class HttpQueryStrings
{
    public static string ToQueryString<T>(this T @this) where T : class
    {
        StringBuilder query = @this.ToQueryString("");

        if (query.Length > 0)
            query[0] = '?';

        return query.ToString();
    }

    private static StringBuilder ToQueryString<T>(this T obj, string prefix = "") where T : class
    {
        StringBuilder gatherer = new StringBuilder();

        foreach (var p in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            if (p.GetValue(obj, new object[0]) != null)
            {
                var value = p.GetValue(obj, new object[0]);

                if (p.PropertyType.IsArray && value.GetType() == typeof(DateTime[]))
                    foreach (var item in value as DateTime[])
                        gatherer.Append($"&{prefix}{p.Name}={item.ToString("yyyy-MM-dd")}");

                else if (p.PropertyType.IsArray)
                    foreach (var item in value as Array)
                        gatherer.Append($"&{prefix}{p.Name}={item}");

                else if (p.PropertyType == typeof(string))
                    gatherer.Append($"&{prefix}{p.Name}={value}");

                else if (p.PropertyType == typeof(DateTime) && !value.Equals(Activator.CreateInstance(p.PropertyType))) // is not default 
                    gatherer.Append($"&{prefix}{p.Name}={((DateTime)value).ToString("yyyy-MM-dd")}");

                else if (p.PropertyType.IsValueType && !value.Equals(Activator.CreateInstance(p.PropertyType))) // is not default 
                    gatherer.Append($"&{prefix}{p.Name}={value}");


                else if (p.PropertyType.IsClass)
                    gatherer.Append(value.ToQueryString($"{prefix}{p.Name}."));
            }
        }

        return gatherer;
    }
}
giorgi02
quelle
0

Hier ist etwas, das ich geschrieben habe und das tut, was Sie brauchen.

    public string CreateAsQueryString(PageVariables pv) //Pass in your EditListItemActionModel instead
    {
        int i = 0;
        StringBuilder sb = new StringBuilder();

        foreach (var prop in typeof(PageVariables).GetProperties())
        {
            if (i != 0)
            {
                sb.Append("&");
            }

            var x = prop.GetValue(pv, null).ToString();

            if (x != null)
            {
                sb.Append(prop.Name);
                sb.Append("=");
                sb.Append(x.ToString());
            }

            i++;
        }

        Formating encoding = new Formating();
        // I am encoding my query string - but you don''t have to
        return "?" + HttpUtility.UrlEncode(encoding.RC2Encrypt(sb.ToString()));  
    }
TheGeekYouNeed
quelle
1
Das könnte genauso gut dauern object.
ChaosPandion
@ TheGeekYouNeed Danke! Ich werde es ausprobieren. Ich bin überrascht, dass nichts eingebaut ist. Wird dies auch geerbte Eigenschaften fangen?
Benjamin
Benjamin, ich denke schon - ich erinnere mich nicht ganz, wie ich diesen Code vor einiger Zeit geschrieben habe, aber ich erinnere mich, dass ich ihn hatte, als ich Ihre Frage sah.
TheGeekYouNeed
0

Ich suchte nach einer Lösung für eine Windows 10 (UWP) App. Unter Verwendung des von Dave vorgeschlagenen Relection-Ansatzes und nach dem Hinzufügen des Microsoft.AspNet.WebApi.Client Nuget-Pakets habe ich den folgenden Code verwendet, der die URL-Codierung der Eigenschaftswerte behandelt:

 private void AddContentAsQueryString(ref Uri uri, object content)
    {            
        if ((uri != null) && (content != null))
        {
            UriBuilder builder = new UriBuilder(uri);

            HttpValueCollection query = uri.ParseQueryString();

            IEnumerable<PropertyInfo> propInfos = content.GetType().GetRuntimeProperties();

            foreach (var propInfo in propInfos)
            {
                object value = propInfo.GetValue(content, null);
                query.Add(propInfo.Name, String.Format("{0}", value));
            }

            builder.Query = query.ToString();
            uri = builder.Uri;                
        }
    }
Howard
quelle
0

Ein einfacher Ansatz, der Listeneigenschaften unterstützt:

public static class UriBuilderExtensions
{
    public static UriBuilder SetQuery<T>(this UriBuilder builder, T parameters)
    {
        var fragments = typeof(T).GetProperties()
            .Where(property => property.CanRead)
            .Select(property => new
            {
                property.Name,
                Value = property.GetMethod.Invoke(parameters, null)
            })
            .Select(pair => new
            {
                pair.Name,
                List = (!(pair.Value is string) && pair.Value is IEnumerable list ? list.Cast<object>() : new[] { pair.Value })
                    .Select(element => element?.ToString())
                    .Where(element => !string.IsNullOrEmpty(element))
            })
            .Where(pair => pair.List.Any())
            .SelectMany(pair => pair.List.Select(value => Uri.EscapeDataString(pair.Name) + '=' + Uri.EscapeDataString(value)));

        builder.Query = string.Join("&", fragments);
        return builder;
    }
}

Eine schnellere Lösung, die so schnell ist wie das Ausschreiben des Codes zum Serialisieren der einzelnen Typen:

public static class UriBuilderExtensions
{
    public static UriBuilder SetQuery<TSource>(this UriBuilder builder, TSource parameters)
    {
        var fragments = Cache<TSource>.Properties
            .Select(property => new
            {
                property.Name,
                List = property.FetchValue(parameters)?.Where(item => !string.IsNullOrEmpty(item))
            })
            .Where(parameter => parameter.List?.Any() ?? false)
            .SelectMany(pair => pair.List.Select(item => Uri.EscapeDataString(pair.Name) + '=' + Uri.EscapeDataString(item)));

        builder.Query = string.Join("&", fragments);
        return builder;
    }

    /// <summary>
    /// Caches dynamically emitted code which converts a types getter property values to a list of strings.
    /// </summary>
    /// <typeparam name="TSource">The type of the object being serialized</typeparam>
    private static class Cache<TSource>
    {
        public static readonly IEnumerable<IProperty> Properties =
            typeof(TSource).GetProperties()
            .Where(propertyInfo => propertyInfo.CanRead)
            .Select(propertyInfo =>
            {
                var source = Expression.Parameter(typeof(TSource));
                var getter = Expression.Property(source, propertyInfo);
                var cast = Expression.Convert(getter, typeof(object));
                var expression = Expression.Lambda<Func<TSource, object>>(cast, source).Compile();
                return new Property
                {
                    Name = propertyInfo.Name,
                    FetchValue = typeof(IEnumerable).IsAssignableFrom(propertyInfo.PropertyType) && propertyInfo.PropertyType != typeof(string) ?
                        CreateListFetcher(expression) :
                        CreateValueFetcher(expression)
                };
            })
            .OrderBy(propery => propery.Name)
            .ToArray();

        /// <summary>
        /// Creates a function which serializes a <see cref="IEnumerable"/> property value to a list of strings.
        /// </summary>
        /// <param name="get">A lambda function which retrieves the property value from a given source object.</param>
        private static Func<TSource, IEnumerable<string>> CreateListFetcher(Func<TSource, object> get)
           => obj => ((IEnumerable)get(obj))?.Cast<object>().Select(item => item?.ToString());

        /// <summary>
        /// Creates a function which serializes a <see cref="object"/> property value to a list of strings.
        /// </summary>
        /// <param name="get">A lambda function which retrieves the property value from a given source object.</param>
        private static Func<TSource, IEnumerable<string>> CreateValueFetcher(Func<TSource, object> get)
            => obj => new[] { get(obj)?.ToString() };

        public interface IProperty
        {
            string Name { get; }
            Func<TSource, IEnumerable<string>> FetchValue { get; }
        }

        private class Property : IProperty
        {
            public string Name { get; set; }
            public Func<TSource, IEnumerable<string>> FetchValue { get; set; }
        }
    }
}

Ein Beispiel für die Verwendung einer der beiden Lösungen:

var url = new UriBuilder("test.com").SetQuerySlow(new
{
    Days = new[] { WeekDay.Tuesday, WeekDay.Wednesday },
    Time = TimeSpan.FromHours(14.5),
    Link = "conferences.com/apple/stream/15",
    Pizzas = default(int?)
}).Uri;

Ausgabe:
http://test.com/Days=Tuesday&Days=Wednesday&Time=14:30:00&Link=conferences.com%2Fapple%2Fstream%2F15
Keine der Lösungen verarbeitet exotische Typen, indizierte Parameter oder verschachtelte Parameter.

Wenn die manuelle Serialisierung einfacher ist, kann dieser c # 7 / .net4.7-Ansatz helfen:

public static class QueryParameterExtensions
{
    public static UriBuilder SetQuery(this UriBuilder builder, params (string Name, object Obj)[] parameters)
    {
        var list = parameters
            .Select(parameter => new
            {
                parameter.Name,
                Values = SerializeToList(parameter.Obj).Where(value => !string.IsNullOrEmpty(value))
            })
            .Where(parameter => parameter.Values.Any())
            .SelectMany(parameter => parameter.Values.Select(item => Uri.EscapeDataString(parameter.Name) + '=' + Uri.EscapeDataString(item)));
        builder.Query = string.Join("&", list);
        return builder;
    }

    private static IEnumerable<string> SerializeToList(object obj)
    {
        switch (obj)
        {
            case string text:
                yield return text;
                break;
            case IEnumerable list:
                foreach (var item in list)
                {
                    yield return SerializeToValue(item);
                }
                break;
            default:
                yield return SerializeToValue(obj);
                break;
        }
    }

    private static string SerializeToValue(object obj)
    {
        switch (obj)
        {
            case bool flag:
                return flag ? "true" : null;
            case byte number:
                return number == default(byte) ? null : number.ToString();
            case short number:
                return number == default(short) ? null : number.ToString();
            case ushort number:
                return number == default(ushort) ? null : number.ToString();
            case int number:
                return number == default(int) ? null : number.ToString();
            case uint number:
                return number == default(uint) ? null : number.ToString();
            case long number:
                return number == default(long) ? null : number.ToString();
            case ulong number:
                return number == default(ulong) ? null : number.ToString();
            case float number:
                return number == default(float) ? null : number.ToString();
            case double number:
                return number == default(double) ? null : number.ToString();
            case DateTime date:
                return date == default(DateTime) ? null : date.ToString("s");
            case TimeSpan span:
                return span == default(TimeSpan) ? null : span.ToString();
            case Guid guid:
                return guid == default(Guid) ? null : guid.ToString();
            default:
                return obj?.ToString();
        }
    }
}

Anwendungsbeispiel:

var uri = new UriBuilder("test.com")
    .SetQuery(("days", standup.Days), ("time", standup.Time), ("link", standup.Link), ("pizzas", standup.Pizzas))
    .Uri;

Ausgabe:
http://test.com/?days=Tuesday&days=Wednesday&time=14:30:00&link=conferences.com%2Fapple%2Fstream%2F15

Rjz
quelle
-2

In einer ähnlichen Situation habe ich das Objekt in XML serialisiert und als Abfragezeichenfolgenparameter weitergegeben. Die Schwierigkeit bei diesem Ansatz bestand darin, dass das empfangende Formular trotz Codierung eine Ausnahme mit der Aufschrift "potenziell gefährliche Anforderung ..." auslöst. Ich habe mich darum gekümmert, das serialisierte Objekt zu verschlüsseln und dann zu codieren, um es als Abfragezeichenfolgenparameter weiterzugeben. Was wiederum die Abfragezeichenfolge manipulationssicher machte (Bonus wandert in das HMAC-Gebiet)!

FormA XML serialisiert ein Objekt> verschlüsselt die serialisierte Zeichenfolge> codiert> als Abfragezeichenfolge an FormB übergeben FormB entschlüsselt den Abfrageparameterwert (wie request.querystring auch decodiert)> deserialisiert die resultierende XML-Zeichenfolge mit XmlSerializer in ein Objekt.

Ich kann meinen VB.NET-Code auf Anfrage an howIdidit-at-applecart-dot-net weitergeben

Denny Jacob
quelle