Yoann. B
Yoann. B

Reputation: 11143

How-to generate querystring from model with asp.net mvc framework

I've a model, with some nested properties, lists ... and i want to get a querystring parameters from that model.

Is there any class/helper in asp.net mvc framework to do this ?

I know that with model binder we can bind a model from a querystring, but i want to do the inverse.

Thanks.

Upvotes: 6

Views: 3388

Answers (2)

mono blaine
mono blaine

Reputation: 999

@Steve's code had some minor bug when extra nesting and enumerables were the case.

Sample Model

public class BarClass {
    public String prop { get; set; }
}

public class FooClass {
    public List<BarClass> bar { get; set; } 
}

public class Model {
    public FooClass foo { get; set; }
}

Test Code

var model = new Model {
    foo = new FooClass {
        bar = new List<BarClass> {
            new BarClass { prop = "value1" },
            new BarClass { prop = "value2" }
        }
    }
};

var queryString = new ViewDataDictionary<Model>(model).ModelMetadata.ToQueryString();

The value of queryString should be:

"?foo.bar[0].prop=value1&foo.bar[1].prop=value2"

But @Steve's code produces the following output:

"?foobar[0].prop=value1&foobar[1].prop=value2"

Updated Code

Here is a slightly modified version of the @Steve's solution:

public static class QueryStringExtensions {
    #region inner types

    private struct PrefixedModelMetadata {

        public readonly String Prefix;
        public readonly ModelMetadata ModelMetadata;

        public PrefixedModelMetadata (String prefix, ModelMetadata modelMetadata) {
            Prefix = prefix;
            ModelMetadata = modelMetadata;
        }
    }

    #endregion
    #region fields

    private static readonly Type IEnumerableType = typeof(IEnumerable),
                                 IEnumerableGenericType = typeof(IEnumerable<>);

    #endregion
    #region methods

    public static String ToQueryString<ModelType> (this ModelType model) {
        return new ViewDataDictionary<ModelType>(model).ModelMetadata.ToQueryString();
    }

    public static String ToQueryString (this ModelMetadata modelMetadata) {
        if (modelMetadata.Model == null) {
            return String.Empty;
        }

        var keyValuePairs = modelMetadata.Properties.SelectMany(mm =>
            mm.SelectPropertiesAsQueryStringParameters(new List<String>())
        );

        return String.Join("&", keyValuePairs.Select(kvp => String.Format("{0}={1}", kvp.Key, kvp.Value)));
    }

    private static IEnumerable<KeyValuePair<String, String>> SelectPropertiesAsQueryStringParameters (this ModelMetadata modelMetadata, List<String> prefixChain) {
        if (modelMetadata.Model == null) {
            yield break;
        }

        if (modelMetadata.IsComplexType) {
            IEnumerable<KeyValuePair<String, String>> keyValuePairs;

            if (IEnumerableType.IsAssignableFrom(modelMetadata.ModelType)) {
                keyValuePairs = modelMetadata.GetItemMetadata().Select((mm, i) =>
                    new PrefixedModelMetadata(
                        modelMetadata: mm,
                        prefix: String.Format("{0}[{1}]", modelMetadata.PropertyName, i)
                    )
                ).SelectMany(prefixed => prefixed.ModelMetadata.SelectPropertiesAsQueryStringParameters(
                    prefixChain.ToList().AddChainable(prefixed.Prefix, addOnlyIf: IsNeitherNullNorWhitespace)
                ));
            }
            else {
                keyValuePairs = modelMetadata.Properties.SelectMany(mm =>
                    mm.SelectPropertiesAsQueryStringParameters(
                        prefixChain.ToList().AddChainable(
                            modelMetadata.PropertyName,
                            addOnlyIf: IsNeitherNullNorWhitespace
                        )
                    )
                );
            }

            foreach (var keyValuePair in keyValuePairs) {
                yield return keyValuePair;
            }
        }
        else {
            yield return new KeyValuePair<String, String>(
                key: AntiXssEncoder.HtmlFormUrlEncode(
                    String.Join(".",
                        prefixChain.AddChainable(
                            modelMetadata.PropertyName,
                            addOnlyIf: IsNeitherNullNorWhitespace
                        )
                    )
                ),
                value: AntiXssEncoder.HtmlFormUrlEncode(modelMetadata.Model.ToString()));
        }
    }

    // Returns the metadata for each item from a ModelMetadata.Model which is IEnumerable
    private static IEnumerable<ModelMetadata> GetItemMetadata (this ModelMetadata modelMetadata) {
        if (modelMetadata.Model == null) {
            yield break;
        }

        var genericType = modelMetadata.ModelType.GetInterfaces().FirstOrDefault(x =>
            x.IsGenericType && x.GetGenericTypeDefinition() == IEnumerableGenericType
        );

        if (genericType == null) {
            yield return modelMetadata;
        }

        var itemType = genericType.GetGenericArguments()[0];

        foreach (Object item in ((IEnumerable) modelMetadata.Model)) {
            yield return ModelMetadataProviders.Current.GetMetadataForType(() => item, itemType);
        }
    }

    private static List<T> AddChainable<T> (this List<T> list, T item, Func<T, Boolean> addOnlyIf = null) {
        if (addOnlyIf == null || addOnlyIf(item)) {
            list.Add(item);
        }

        return list;
    }

    private static Boolean IsNeitherNullNorWhitespace (String value) {
        return !String.IsNullOrWhiteSpace(value);
    }

    #endregion
}

Upvotes: 1

Steve Ruble
Steve Ruble

Reputation: 3905

I'm fairly certain there is no "serialize to query string" functionality in the framework, mostly because I don't think there's a standard way to represent nested values and nested collections in a query string.

I thought this would be pretty easy to do using the ModelMetadata infrastructure, but it turns out that there are some complications around getting the items from a collection-valued property using ModelMetadata. I've hacked together an extension method that works around that and built a ToQueryString extension you can call from any ModelMetadata object you have.

public static string ToQueryString(this ModelMetadata modelMetadata)
{
    if(modelMetadata.Model == null)
        return string.Empty;

    var parameters = modelMetadata.Properties.SelectMany (mm => mm.SelectPropertiesAsQueryStringParameters(null));
    var qs = string.Join("&",parameters);
    return "?" + qs;
}

private static IEnumerable<string> SelectPropertiesAsQueryStringParameters(this ModelMetadata modelMetadata, string prefix)
{
    if(modelMetadata.Model == null)
        yield break;

    if(modelMetadata.IsComplexType)
    {
        IEnumerable<string> parameters;
        if(typeof(IEnumerable).IsAssignableFrom(modelMetadata.ModelType))
        {
            parameters = modelMetadata.GetItemMetadata()
                                    .Select ((mm,i) => new {
                                        mm, 
                                        prefix = string.Format("{0}{1}[{2}]", prefix, modelMetadata.PropertyName, i)
                                    })
                                    .SelectMany (prefixed =>
                                        prefixed.mm.SelectPropertiesAsQueryStringParameters(prefixed.prefix)
                                    );          
        } 
        else 
        {
            parameters = modelMetadata.Properties
                        .SelectMany (mm => mm.SelectPropertiesAsQueryStringParameters(string.Format("{0}{1}", prefix, modelMetadata.PropertyName)));
        }

        foreach (var parameter in parameters)
        {
            yield return parameter;
        }
    } 
    else 
    {
        yield return string.Format("{0}{1}{2}={3}",
            prefix, 
            prefix != null && modelMetadata.PropertyName != null ? "." : string.Empty,
            modelMetadata.PropertyName, 
            modelMetadata.Model);
    }
}

// Returns the metadata for each item from a ModelMetadata.Model which is IEnumerable
private static IEnumerable<ModelMetadata> GetItemMetadata(this ModelMetadata modelMetadata)
{
    if(modelMetadata.Model == null)
        yield break;

    var genericType = modelMetadata.ModelType
                        .GetInterfaces()
                        .FirstOrDefault (x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>));

    if(genericType == null)
        yield return modelMetadata;

    var itemType = genericType.GetGenericArguments()[0];

    foreach (object item in ((IEnumerable)modelMetadata.Model))
    {
        yield return ModelMetadataProviders.Current.GetMetadataForType(() => item, itemType);
    }
}

Example usage:

var vd = new ViewDataDictionary<Model>(model); // in a Controller, ViewData.ModelMetadata
var queryString = vd.ModelMetadata.ToQueryString();

I haven't tested it very thoroughly, so there may be some null ref errors lurking in it, but it spits out the correct query string for the complex objects I've tried.

Upvotes: 4

Related Questions