Як мені серіалізувати об’єкт у форматі рядка запитів?


76

Як мені серіалізувати об’єкт у форматі рядка запитів? Здається, я не можу знайти відповіді в google. Дякую.

Ось об’єкт, який я буду серіалізувати як приклад.

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

Чому б не створити власну функцію для серіалізації таким чином?
James Black

Ви хочете закінчити: Id = 1 & State = CA & Prefix = Mr ... щось подібне? Якщо так, я погоджуюсь з @James.
Боб Кауфман,

3
@James Wow, це єдиний спосіб? Я прикинув, що десь щось вбудоване у .NET. Я думаю, що це щось на зразок оберненого до в'яжучого моделі MVC. Для цього повинен бути метод?
Бенджамін,

Якщо вбудованої функції немає, можете підказати, як її написати?
Бенджамін,

3
Flurl - це побудова URL-адрес / клієнт HTTP, який широко використовує об’єкти для подібних до імен-значень речей (рядки запитів, заголовки, значення форми, закодовані за URL-адресою тощо). SetQueryParamsробить саме те, що ви шукаєте. Якщо вам потрібен лише конструктор URL-адрес, а не всі матеріали HTTP, він доступний тут . [застереження: я автор]
Тодд Меньє,

Відповіді:


111

Я впевнений на 99%, що для цього немає вбудованого методу утиліти. Це не дуже поширене завдання, оскільки веб-сервер, як правило, не відповідає за допомогою URLEncoded рядка ключа / значення.

Як ви ставитесь до змішування рефлексії та LINQ? Це працює:

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

Оновлення:

Щоб написати метод, який повертає подання QueryString будь-якого 1-глибокого об'єкта, ви можете зробити це:

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

Ви також можете зробити це методом розширення без особливої ​​додаткової роботи

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

Це добре. Я намагаюся зробити це функцією, яка приймає динамічний параметр, але я припускаю, що я псую динамічний синтаксис вибору linq. 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()); }Я також не можу зрозуміти, як робити блоки коду в коментарях ..
Бенджамін,

@Benjamin: Оновив свою відповідь, щоб допомогти в цьому.
Дейв Уорд,

1
Оскільки ви кажете, що це не дуже поширене завдання. Який альтернативний підхід передавати велику кількість значень форми без необхідності жорсткого кодування всіх значень RouteValueDictionary
Дуг Чемберлен,

2
Ви можете зробити це більш ефективним, отримавши значення кожного властивості лише один раз, призначивши його тимчасовій змінній, наприклад, використовуючи let value = p.GetValue(obj, null).
WhatIsHeDoing

Це працює лише для інформації про англійську культуру на сервері. Якщо ви встановите певний формат дати та часу в Windows, це не спрацює, оскільки вам слід встановити CultureInfo Invariant як параметр ToString. Така ж проблема з конкретним десятковим роздільником у float / double.
Томас Кубес,

18

Спираючись на хороші ідеї інших коментарів, я створив загальний метод розширення .ToQueryString (), який можна використовувати на будь-якому об’єкті.

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

Він також буде працювати для об'єктів, що мають властивості типу Array та загальні списки, якщо вони містять лише примітиви або рядки.

Спробуйте, вітаємо коментарі: Серіалізуйте об’єкт у рядок запиту за допомогою Reflection


8
Чому б вам не покласти код сюди. Це не так вже й багато.
asakura89

Тільки трохи синтаксису цукру, якщо (request == null) викине новий ArgumentNullException (nameof (request));
Ян Скала,

8

Виходячи з популярних відповідей, мені також потрібно було оновити код для підтримки масивів. Спільний доступ до реалізації:

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

7

Використовувати Json.Netйого було б набагато простіше, шляхом серіалізації, а потім десеріалізації до пар ключових значень.

Ось приклад коду:

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

Я використав це, оскільки це розробило поле для властивостей DateTime
Ешлі Кілгур

1
Мені подобається простота цього. Він чудово підходить для плоских об’єктів, але не підходить для вкладених об’єктів / списків
Клікер,

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

2

Це моє рішення:

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

Я використовую це розширення для мого тесту інтеграції, воно чудово працює :)


1

Просто черговий варіант вищезазначеного, але я хотів використати існуючі атрибути DataMember у своєму класі моделі, тому лише властивості, які я хочу серіалізувати, надсилаються на сервер за URL-адресою в запиті GET.

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

1

Можливо, цей загальний підхід буде корисним:

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

1

Це також буде корисно для вкладених об'єктів

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

0

Ось те, що я писав, робить те, що вам потрібно.

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

1
Це так само легко прийняти object.
ChaosPandion

@TheGeekYouNeed Дякую! Я спробую це. Я здивований, що нічого не вбудовано. Чи буде цей улов також успадкованими властивостями?
Бенджамін,

Бенджамін, я так думаю - я не зовсім пам’ятаю, як писав цей код деякий час тому, але пам’ятав, що мав, коли бачив ваше запитання.
TheGeekYouNeed

0

Я шукав рішення цього для програми Windows 10 (UWP). Взявши підхід Relection, запропонований Дейвом , і після додавання пакета Microsoft.AspNet.WebApi.Client Nuget я використав такий код, який обробляє Url Encoding значень властивостей:

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

0

Простий підхід, який підтримує властивості списку:

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

Швидше рішення, таке ж швидке, як написання коду для серіалізації кожного типу:

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

Приклад використання будь-якого рішення:

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;

Результат:
http://test.com/Days=Tuesday&Days=Wednesday&Time=14:30:00&Link=conferences.com%2Fapple%2Fstream%2F15
Жодне з рішень не обробляє екзотичні типи, індексовані параметри або вкладені параметри.

Коли ручна серіалізація простіша, цей підхід c # 7 / .net4.7 може допомогти:

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

Приклад використання:

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

Вихід:
http://test.com/?days=Tuesday&days=Wednesday&time=14:30:00&link=conferences.com%2Fapple%2Fstream%2F15


-2

Зіткнувшись із подібною ситуацією, що я зробив, XML серіалізує об'єкт і передає його як параметр рядка запиту. Складність такого підходу полягала в тому, що, незважаючи на кодування, форма отримання видає виняток із вимогою "потенційно небезпечний запит ...". Я отримав спосіб зашифрувати серіалізований об'єкт, а потім кодувати, щоб передавати його як параметр рядка запиту. Що, у свою чергу, зробило рядок запиту фальшивим (бонусне блукання на територію HMAC)!

FormA XML серіалізує об’єкт> шифрує серіалізований рядок> кодує> передає як рядок запиту до FormB FormB дешифрує значення параметра запиту (як декодує також request.querystring)> десеріалізує отриманий рядок XML для об’єкта за допомогою XmlSerializer.

Я можу надати свій код VB.NET за запитом до howIdidit-at-applecart-dot-net

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.