How do I serialize an object into query-string format?

asp.netSerializationObjectQuery String

asp.net Problem Overview


How do I serialize an object into query-string format? I can't seem to find an answer on google. Thanks.

Here is the object I will serialize as an example.

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

asp.net Solutions


Solution 1 - asp.net

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

Solution 2 - asp.net

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

Solution 3 - asp.net

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

Solution 4 - asp.net

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

Solution 5 - asp.net

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

Solution 6 - asp.net

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

Solution 7 - asp.net

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

Solution 8 - asp.net

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

Solution 9 - asp.net

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

Solution 10 - asp.net

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

Solution 11 - asp.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;                
        }
    }

Solution 12 - asp.net

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<br> 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<br>

Solution 13 - asp.net

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBenjaminView Question on Stackoverflow
Solution 1 - asp.netDave WardView Answer on Stackoverflow
Solution 2 - asp.netOleView Answer on Stackoverflow
Solution 3 - asp.netyoel halbView Answer on Stackoverflow
Solution 4 - asp.netAlvisView Answer on Stackoverflow
Solution 5 - asp.netgiorgi02View Answer on Stackoverflow
Solution 6 - asp.netAlexanderView Answer on Stackoverflow
Solution 7 - asp.netCedric ArnouldView Answer on Stackoverflow
Solution 8 - asp.netPeter KerrView Answer on Stackoverflow
Solution 9 - asp.netLugView Answer on Stackoverflow
Solution 10 - asp.netTheGeekYouNeedView Answer on Stackoverflow
Solution 11 - asp.netHowardView Answer on Stackoverflow
Solution 12 - asp.netRjzView Answer on Stackoverflow
Solution 13 - asp.netDenny JacobView Answer on Stackoverflow