Is there any way to rewrite this query to be more "LINQ-ier"?

The following parses a /-delimited URL path into a dictionary of key-value pairs:

    private Dictionary<string, string> ParsePathParameters(string path)
    {
        var parameters = new Dictionary<string, string>();
        if (string.IsNullOrEmpty(path))
        {
            return parameters;
        }

        var pathSegments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

        for (var i = pathSegments.Length - 1; i >= pathSegments.Length % 2; i -= 2)
        {
            parameters.Add(pathSegments[i - 1], pathSegments[i]);
        }

        return parameters;
    }

The input format is [/preamble][/key1/value1][/key2/value2]...[/keyN/valueN] so for example, given the input "/foo/1/bar/Thing" or "/slug/foo/1/bar/Thing", the output would be:

Dictionary<string, string>
{
    { "foo", "1" },
    { "bar", "Thing" },
}

This code is good code; simple, self-explanatory, and fast. But, because I like a challenge, I decided to rewrite it in LINQ:

    private Dictionary<string, string> ParsePathParameters(string path)
    {
        if (string.IsNullOrEmpty(path))
        {
            return new Dictionary<string, string>();
        }

        var pathSegments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
        var skip = pathSegments.Length % 2;

        return pathSegments.Skip(skip)
            .Where((_, i) => i % 2 == 0)
            .Select((_, i) => i * 2)
            .ToDictionary(i => pathSegments[i + skip], i => pathSegments[i + skip + 1]);
    }

This works, but it definitely doesn't feel optimal, probably because it also doesn't feel like the "right" way to achieve this using LINQ. Can anyone suggest if it's possible to write this code in a more "LINQ-like" manner, and if so give me some pointers in that regard?

Upvotes: 2

Views: 137

Answers (1)

Jeff Mercado
Jeff Mercado

Reputation: 134521

I would write it like so:

private Dictionary<string, string> ParsePathParameters(string path)
{
    return GetSegmentPairs().ToDictionary(x => x.k, x => x.v);
    IEnumerable<(string k, string v)> GetSegmentPairs()
    {
        var segments = path?.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
                ?? new string[0];
        for (int i = 0, l = segments.Length; i < l; i += 2)
            yield return (segments[i+0], segments[i+1]);
    }
}

Don't underestimate the power of local functions and generators. Generators are great when you need to create sequences that are awkward to write as a straight linq query. Then these generators may be used within linq queries. For this particuar case, it might not even be necessary for a fairly trivial query, but for more complex queries, it's invaluable. But it's a pattern you should learn to utilize more often.

If using C# 8, I'd get in the habit of using span/memory and slices where appropriate.

private Dictionary<string, string> ParsePathParameters(string path)
{
    return GetSegments().ToDictionary(x => x.Span[0], x => x.Span[1]);
    IEnumerable<System.Memory<string>> GetSegments()
    {
        var segments = path?.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0];
        for (int i = 0, l = segments.Length; i < l; i += 2)
            yield return segments[^i..i+1];
    }
}

Otherwise if you're using MoreLINQ, you could use the Pairwise() along with TakeEvery() methods to effectively do the same thing as the GetSegmentPairs() method above.

private Dictionary<string, string> ParsePathParameters(string path) =>
    (path?.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
            ?? Enumerable.Empty<string>())
        .Pairwise(ValueTuple.Create)
        .TakeEvery(2) // pairwise produces overlapping pairs so take every other
        .ToDictionary(x => x[0], x => x[1]);

Upvotes: 6

Related Questions