如何将对象序列化为查询字符串格式?

如何将一个对象序列化为查询字符串格式? 我似乎找不到答案在谷歌。谢谢。

下面是我将序列化为示例的对象。

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; }
}
96275 次浏览

Here is something I wrote that does what you need.

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

I'm 99% sure there's no built-in utility method for this. It's not a very common task, since a web server doesn't typically respond with a URLEncoded key/value string.

How do you feel about mixing reflection and LINQ? This works:

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

Update:

To write a method that returns the QueryString representation of any 1-deep object, you could do this:

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

You could also make it an extension method without much additional work

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

Building on the good ideas from other comments, I have made a generic extension method .ToQueryString(), which can be used on any object.

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

It will also work for objects that have properties of the type Array and generic Lists if they only contain primitives or strings.

Try it out, comments are welcome: Serialize object into a query string with Reflection

Faced with a similar situation what I did, is to XML serialize the object and pass it around as query string parameter. The difficulty with this approach was that despite encoding, the receiving form throws exception saying "potentially dangerous request...". The way I got around was to encrypt the serialized object and then encode to pass it around as query string parameter. Which in turn made the query string tamper proof (bonus wandering into the HMAC territory)!

FormA XML serializes an object > encrypts the serialized string > encode > pass as query string to FormB FormB decrypts the query parameter value (as request.querystring decodes also) > deserialize the resulting XML string to object using XmlSerializer.

I can share my VB.NET code upon request to howIdidit-at-applecart-dot-net

I was looking for a solution to this for a Windows 10 (UWP) App. Taking the Relection approach suggested by Dave, and after adding the Microsoft.AspNet.WebApi.Client Nuget package, I used the following code, which handles Url Encoding of the property values:

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

Based on the the popular answers, I needed to update the code to support arrays as well. Sharing the implementation:

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

Just another variation of the above, but I wanted to utilize the existing DataMember attributes in my model class, so only the properties I want to serialize are sent to the server in the url in the GET request.

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

This my solution:

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

I use this extension for my integration test, it works perfectly :)

A simple approach that supports list properties:

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

A faster solution which is as fast as spelling out the code to serialize each type:

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

An example of using either solution:

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;

Output:
http://test.com/Days=Tuesday&Days=Wednesday&Time=14:30:00&Link=conferences.com%2Fapple%2Fstream%2F15
Neither of the solutions handle exotic types, indexed parameters, or nested parameters.

When manual serialization is simpler, this c#7/.net4.7 approach can help:

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

Example usage:

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

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

Perhaps this Generic approach will be useful:

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

Using Json.Net it would be much easier, by serializing and then deserializing to key value pairs.

Here is a code example:

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

It will also be useful for nested objects

public static class HttpQueryStrings
{
private static readonly StringBuilder _query = new();


public static string ToQueryString<T>(this T @this) where T : class
{
_query.Clear();


BuildQueryString(@this, "");


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


return _query.ToString();
}


private static void BuildQueryString<T>(T? obj, string prefix = "") where T : class
{
if (obj == null) return;


foreach (var p in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (p.GetValue(obj, Array.Empty<object>()) != null)
{
var value = p.GetValue(obj, Array.Empty<object>());




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


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


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


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


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




else if (p.PropertyType.IsClass)
BuildQueryString(value, $"{prefix}{p.Name}.");
}
}
}
}

An example of using the solution:

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

In addition to existing answers

public static string ToQueryString<T>(this T input)
{
if (input == null)
{
return string.Empty;
}
                

var queryStringBuilder = new StringBuilder("?");


var properties = input.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);


foreach (var property in properties)
{
var value = property.GetValue(input);
if (value is null || property.HasIgnoreDataMember())
continue;


queryStringBuilder.AppendFormat("{0}={1}&", property.GetName(), HttpUtility.UrlEncode(value.ToString()));
}
queryStringBuilder.Length--;


return queryStringBuilder.ToString();
}


private static bool HasIgnoreDataMember(this PropertyInfo propertyInfo)
{
return propertyInfo.GetCustomAttribute(typeof(IgnoreDataMemberAttribute), true) is not null;
}


private static DataMemberAttribute GetDataMemberAttribute(this PropertyInfo propertyInfo)
{
return propertyInfo.GetCustomAttribute<DataMemberAttribute>();
}


private static T GetCustomAttribute<T>(this PropertyInfo propertyInfo) where T : class
{
return propertyInfo.GetCustomAttribute(typeof(T), true) as T;
}


private static string GetName(this PropertyInfo propertyInfo)
{
return propertyInfo.GetDataMemberAttribute()?.Name ?? propertyInfo.Name;
}
}

Usage: var queryString = object.ToQueryString()