Make NameValueCollection accessible to LINQ Query
Asked Answered
A

7

66

How to make NameValueCollection accessible to LINQ query operator such as where, join, groupby?

I tried the below:

private NameValueCollection RequestFields()
{
    NameValueCollection nvc = new NameValueCollection()
                                  {
                                      {"emailOption: blah Blah", "true"},
                                      {"emailOption: blah Blah2", "false"},
                                      {"nothing", "false"},
                                      {"nothinger", "true"}
                                  };
    return nvc;

}

public void GetSelectedEmail()
{
    NameValueCollection nvc = RequestFields();
    IQueryable queryable = nvc.AsQueryable();
}

But I got an ArgumentException telling me that the source is not IEnumerable<>.

Alben answered 24/12, 2008 at 8:28 Comment(1)
See also #5066428Staurolite
P
98

You need to "lift" the non-generic IEnumerable to an IEnumerable<string>. It has been suggested that you use OfType but that is a filtering method. What you're doing is the equivalent of a cast, for which there is the Cast operator:

var fields = RequestFields().Cast<string>();

As Frans pointed out, this only provides access to the keys. You would still need to index into the collection for the values. Here is an extension method to extract KeyValuePairs from the NameValueCollection:

public static IEnumerable<KeyValuePair<string, string>> ToPairs(this NameValueCollection collection)
{
    if(collection == null)
    {
        throw new ArgumentNullException("collection");
    }

    return collection.Cast<string>().Select(key => new KeyValuePair<string, string>(key, collection[key]));
}

Edit: In response to @Ruben Bartelink's request, here is how to access the full set of values for each key using ToLookup:

public static ILookup<string, string> ToLookup(this NameValueCollection collection)
{
    if(collection == null)
    {
        throw new ArgumentNullException("collection");
    }

    var pairs =
        from key in collection.Cast<String>()
        from value in collection.GetValues(key)
        select new { key, value };

    return pairs.ToLookup(pair => pair.key, pair => pair.value);
}

Alternatively, using C# 7.0 tuples:

public static IEnumerable<(String name, String value)> ToTuples(this NameValueCollection collection)
{
    if(collection == null)
    {
        throw new ArgumentNullException("collection");
    }

    return
        from key in collection.Cast<string>()
        from value in collection.GetValues(key)
        select (key, value);
}
Packston answered 28/12, 2008 at 18:38 Comment(7)
how can you use Where clause? and lambda?Monge
@user384080: RequestFields().ToPairs().Where(pair => pair.Key.Contains("a"))Packston
Warning. You can miss some values this way. One key can have multiple values assigned see method GetValues.Tael
@Kugel: Great distinction - I did not realize that NameValueCollection actually supports multiple values per key. The documentation for the Item property (msdn.microsoft.com/en-us/library/8d0bzeeb.aspx) says it will return the values in a comma-separated list. So, you wouldn't lose them entirely, but they would not be in the key/value format that you might expect.Packston
-1 Any chance of fixing the code to use .ToLookup or yielding arrays or adding an edit flagging the comma-separated list issue. Will remove when fixed and I see this again. (Just find it annoying that every answer here has bad advice as of this writing)Staurolite
@Ruben Bartelink: I updated the answer to include a .ToLookup extension to NameValueCollection in case you need access to the full set of values for each key. Let me know if that meets your needs.Packston
the Linq expression in ToLookup can be written as collection.Cast<string>().SelectMany(key => collection.GetValues(key), (key, value) => new {key, value}). If you want lambdas :)Mcarthur
I
12

I know I'm late to the party but just wanted to add my answer that doesn't involve the .Cast extension method but instead uses the AllKeys property:

var fields = RequestFields().AllKeys;

This would allow the following extension method:

public static IEnumerable<KeyValuePair<string, string>> ToPairs(this NameValueCollection collection)
{
    if(collection == null)
    {
        throw new ArgumentNullException("collection");
    }

    return collection.AllKeys.Select(key => new KeyValuePair<string, string>(key, collection[key]));
}

Hope this helps any future visitors

Incardination answered 17/9, 2013 at 15:59 Comment(1)
This looks like a decent way of implementing it. +1 for usePestalozzi
E
11

AsQueryable must take an IEnumerable<T>, a generic. NameValueCollection implements IEnumerable, which is different.

Instead of this:

{
    NameValueCollection nvc = RequestFields();
    IQueryable queryable = nvc.AsQueryable();
}

Try OfType (it accepts the non-generic interface)

{
    NameValueCollection nvc = RequestFields();
    IEnumerable<string> canBeQueried = nvc.OfType<string>();
    IEnumerable<string> query =
       canBeQueried.Where(s => s.StartsWith("abc"));
}
Ent answered 24/12, 2008 at 15:15 Comment(6)
Good solution! I needed an easy way to search through the Request.Params NameValueCollection for a specific pattern, and this little tidbit of code helped me get there.Lydalyddite
-1 As covered in the other answers, should be Cast, not OfType, comment and downvote disappears if I see it again and the text doesnt have an OfType in it.Staurolite
@Ruben Bartelink , I stand by my use of OfType. Cast is not reliable, as it has been modified between different .net versions.Ent
Any citations? What else do you think could be in there? (Obv if this can be confirmed as real then this has real merit and my vote flips)Staurolite
social.msdn.microsoft.com/forums/en-US/linqprojectgeneral/… from Heljsberg: "All of this was fixed in SP1"Ent
@David B I very much disagree that changes in semantics on the supported value type conversions should prevent one from, in the case of a clear reference type casting requirement, switch from using the Cast conversion operator to a filtering predicate which a) conveys a completely different meaning b) could result in stuff silently being ignored - I want that to fail fast. But I very much welcome the response so will now execute vote flip by doing a null editStaurolite
R
8

A dictionary is probably actually closer to what you want to use since it will actually fill more of the roles that NameValueCollection fills. This is a variation of Bryan Watts' solution:

public static class CollectionExtensions
{
    public static IDictionary<string, string> ToDictionary(this NameValueCollection source)
    {
        return source.Cast<string>().Select(s => new { Key = s, Value = source[s] }).ToDictionary(p => p.Key, p => p.Value); 
    }
}
Rsfsr answered 7/6, 2010 at 15:21 Comment(2)
Still not great. A NameValueCollection is actually roughly IDictionary<string,IEnumerable<string>>. For some use cases that should be preserved. ILookup is what you really need. Something like souce.Cast<string>().SelectMany(s => source.GetValues(s).Select(t=> new {Key=s, Value=t}) ).ToLookup(p=>p.Key,p=>p.Value). That will play very nicely with Linq, since ILookup was created specifically for Linq.Octavia
-1 This is just a specific impl of @Frans Bouma's impl, with the noted side effect of compacting the values separated by commas. -1 goes when this uses ToLookupStaurolite
E
4

The problem is that the collection implements IEnumerable (as opposed to IEnumerable<T>) and enumerating the collection returns the keys, not the pairs.

If I were you, I'd use a Dictionary<string, string> which is enumerable and can be used with LINQ.

Eure answered 24/12, 2008 at 8:36 Comment(3)
Thanks, but I don't quite understand why despite that NameValueCollection implements IEnumerable, it still says that source is not IEnumerable.Alben
It implements IEnumerable (on the base class) but it doesn't implement IEnumerable<T> (the generic variant) so you can't use the extension methods which require the generic variant.Eure
+1 But as noted by @Kevin Cathcart on Orion Adrian's answer, if you actually flatten to a Dictionary<key,string>, multiple values get joined with a comma which makes the operation lossyStaurolite
S
3

For me, @Bryan Watts' (+1'd) answer's ToLookup variant represents by far the clearest approach for using it on a read-only basis.

For my use case, I'm manipulating a query string for use with Linq2Rest and also need to turn it all back into a NameValueCollection at the end, so I have a set of extension methods for NameValueCollection which offer more granular operations (to operate both per parameter name (AsEnumerable) and per argument (AsKeyValuePairs)) and also the inverse operation of converting it back ToNameValueCollection (from either representation)).

Example consumption:

public static NameValueCollection WithoutPagingOperators( this NameValueCollection that )
{
    return that.AsEnumerable()
        .Where( @param => @param.Key != OdataParameters.Skip 
          && @param.Key != OdataParameters.Top )
        .ToNameValueCollection();
}

Code:

using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;

public static class NamedValueCollectionExtensions
{
    public static IEnumerable<KeyValuePair<string, string[]>> AsEnumerable( this NameValueCollection that )
    {
        return that
            .Cast<string>() // doesn't implement IEnumerable<T>, but does implement IEnumerable
            .Select( ( item, index ) => // enable indexing by integer rather than string
                new KeyValuePair<string, string[]>( item, that.GetValues( index ) ) ); // if you use the indexer or GetValue it flattens multiple values for a key, Joining them with a ',' which we don't want
    }

    public static IEnumerable<KeyValuePair<string, string>> AsKeyValuePairs( this IEnumerable<KeyValuePair<string, string[]>> that )
    {
        return that
            .SelectMany( item =>
                item.Value.Select( value =>
                    new KeyValuePair<string, string>( item.Key, value ) ) );
    }

    public static NameValueCollection ToNameValueCollection( this IEnumerable<KeyValuePair<string, string[]>> that )
    {
        return that.AsKeyValuePairs().ToNameValueCollection();
    }

    public static NameValueCollection ToNameValueCollection( this IEnumerable<KeyValuePair<string, string>> that )
    {
        var result = new NameValueCollection();
        foreach ( KeyValuePair<string, string> item in that )
            result.Add( item.Key, item.Value );
        return result;
    }
}
Staurolite answered 3/10, 2012 at 13:1 Comment(0)
P
2

I don't really see why anyone would need to add an extension method.
Here's some different ways to do it in VB.NET. It includes 4 different intermediate forms of IEnumerable: Array, Tuple, Anonymous, and KeyValuePair. For the C# equivalent go to converter.telerik dot com and convert it.

Dim nvc As New NameValueCollection() From {{"E", "55"}, {"A", "11"}, {"D", "44"}, {"C", "33"}, {"G", "66"}, {"B", "22"}}

Dim dictStrings As Dictionary(Of String, String) = nvc.Cast(Of String).ToDictionary(Function(key) key, Function(key) nvc(key))
Dim Ints2Chars__ As Dictionary(Of Integer, Char) = nvc.Cast(Of Object).ToDictionary(Function(key) CInt(nvc(CStr(key))), Function(key) CChar(key))

Dim arrEnumerable__ = From x In nvc.Cast(Of String) Select {x, nvc(x)}
Dim tupleEnumerable = From x In nvc.Cast(Of String) Select Tuple.Create(x, nvc(x))
Dim anonEnumerable_ = From X In nvc.Cast(Of String) Select New With {X, .Y = nvc(X)}
Dim kvpEnumerable__ = From x In nvc.Cast(Of String) Select New KeyValuePair(Of String, String)(x, nvc(x))

Dim anonQuery = From anon In anonEnumerable_ Let n = CInt(anon.Y) Order By n Where n > 30 Select New With {.num = n, .val = anon.X}
Dim dictQuery = anonQuery.ToDictionary(Of Integer, String)(Function(o) o.num, Function(o) o.val)


Dim dictArray_ = arrEnumerable__.ToDictionary(Function(x) x(0), Function(x) x(1))
Dim dictTuples = tupleEnumerable.ToDictionary(Function(tuple) tuple.Item1, Function(tuple) tuple.Item2)
Dim dictAnon__ = anonEnumerable_.ToDictionary(Function(anon) anon.X, Function(anon) anon.Y)
Dim dictKVPrs_ = kvpEnumerable__.ToDictionary(Function(kvp) kvp.Key, Function(kvp) kvp.Value)
Phoney answered 13/1, 2016 at 4:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.