Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 653
Yoann. B Avatar asked Dec 20 '12 15:12

Yoann. B


2 Answers

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.

like image 175
Steve Ruble Avatar answered Sep 30 '22 18:09

Steve Ruble


@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
}
like image 44
mono blaine Avatar answered Sep 30 '22 17:09

mono blaine