Skip to content

Commit 22b42d3

Browse files
Merge pull request #203 from SixLabors/js/command-control
Replace generic dictionary for commands with specialized collection type.
2 parents 7c2b496 + ec8ecee commit 22b42d3

24 files changed

Lines changed: 499 additions & 119 deletions
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.ObjectModel;
7+
using System.Runtime.CompilerServices;
8+
9+
namespace SixLabors.ImageSharp.Web.Commands
10+
{
11+
/// <summary>
12+
/// Represents an ordered collection of processing commands.
13+
/// </summary>
14+
public sealed class CommandCollection : KeyedCollection<string, KeyValuePair<string, string>>
15+
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="CommandCollection"/> class.
18+
/// </summary>
19+
public CommandCollection()
20+
: this(StringComparer.OrdinalIgnoreCase)
21+
{
22+
}
23+
24+
private CommandCollection(IEqualityComparer<string> comparer)
25+
: base(comparer)
26+
{
27+
}
28+
29+
/// <summary>
30+
/// Gets an <see cref="IEnumerable{String}"/> representing the keys of the collection.
31+
/// </summary>
32+
public IEnumerable<string> Keys
33+
{
34+
get
35+
{
36+
foreach (KeyValuePair<string, string> item in this)
37+
{
38+
yield return this.GetKeyForItem(item);
39+
}
40+
}
41+
}
42+
43+
/// <summary>
44+
/// Gets or sets the value associated with the specified key.
45+
/// </summary>
46+
/// <param name="key">The key of the value to get or set.</param>
47+
/// <returns>
48+
/// The value associated with the specified key. If the specified key is not found,
49+
/// a get operation throws a <see cref="KeyNotFoundException"/>, and
50+
/// a set operation creates a new element with the specified key.
51+
/// </returns>
52+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is null.</exception>
53+
/// <exception cref="KeyNotFoundException">An element with the specified key does not exist in the collection.</exception>
54+
public new string this[string key]
55+
{
56+
get
57+
{
58+
if (!this.TryGetValue(key, out string value))
59+
{
60+
ThrowKeyNotFound();
61+
}
62+
63+
return value;
64+
}
65+
66+
set
67+
{
68+
if (this.TryGetValue(key, out KeyValuePair<string, string> item))
69+
{
70+
this.SetItem(this.IndexOf(item), new(key, value));
71+
}
72+
else
73+
{
74+
this.Add(key, value);
75+
}
76+
}
77+
}
78+
79+
/// <summary>
80+
/// Adds an element with the provided key and value to the <see cref="CommandCollection"/>.
81+
/// </summary>
82+
/// <param name="key">The <see cref="string"/> to use as the key of the element to add.</param>
83+
/// <param name="value">The <see cref="string"/> to use as the value of the element to add.</param>
84+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is null.</exception>
85+
public void Add(string key, string value) => this.Add(new(key, value));
86+
87+
/// <summary>
88+
/// Inserts an element into the <see cref="CommandCollection"/> at the
89+
/// specified index.
90+
/// </summary>
91+
/// <param name="index">The zero-based index at which item should be inserted.</param>
92+
/// <param name="key">The <see cref="string"/> to use as the key of the element to insert.</param>
93+
/// <param name="value">The <see cref="string"/> to use as the value of the element to insert.</param>
94+
/// <exception cref="ArgumentOutOfRangeException">index is less than zero. -or- index is greater than <see cref="P:CommandCollection.Count"/>.</exception>
95+
public void Insert(int index, string key, string value) => this.Insert(index, new(key, value));
96+
97+
/// <summary>
98+
/// Gets the value associated with the specified key.
99+
/// </summary>
100+
/// <param name="key">The key whose value to get.</param>
101+
/// <param name="value">
102+
/// When this method returns, the value associated with the specified key, if the
103+
/// key is found; otherwise, the default value for the type of the value parameter.
104+
/// This parameter is passed uninitialized.
105+
/// </param>
106+
/// <returns>
107+
/// <see langword="true"/> if the object that implements <see cref="CommandCollection"/> contains
108+
/// an element with the specified key; otherwise, <see langword="false"/>.
109+
/// </returns>
110+
/// <exception cref="ArgumentNullException"><paramref name="key"/> is null.</exception>
111+
public bool TryGetValue(string key, out string value)
112+
{
113+
if (this.TryGetValue(key, out KeyValuePair<string, string> keyValue))
114+
{
115+
value = keyValue.Value;
116+
return true;
117+
}
118+
119+
value = default;
120+
return false;
121+
}
122+
123+
/// <summary>
124+
/// Searches for an element that matches the conditions defined by the specified
125+
/// predicate, and returns the zero-based index of the first occurrence within the
126+
/// entire <see cref="CommandCollection"/>.
127+
/// </summary>
128+
/// <param name="match">
129+
/// The <see cref="Predicate{T}"/> delegate that defines the conditions of the element to
130+
/// search for.
131+
/// </param>
132+
/// <returns>
133+
/// The zero-based index of the first occurrence of an element that matches the conditions
134+
/// defined by match, if found; otherwise, -1.
135+
/// </returns>
136+
/// <exception cref="ArgumentNullException"><paramref name="match"/> is null.</exception>
137+
public int FindIndex(Predicate<string> match)
138+
{
139+
Guard.NotNull(match, nameof(match));
140+
141+
int index = 0;
142+
foreach (KeyValuePair<string, string> item in this)
143+
{
144+
if (match(item.Key))
145+
{
146+
return index;
147+
}
148+
149+
index++;
150+
}
151+
152+
return -1;
153+
}
154+
155+
/// <summary>
156+
/// Searches for the specified key and returns the zero-based index of the first
157+
/// occurrence within the entire <see cref="CommandCollection"/>.
158+
/// </summary>
159+
/// <param name="key">The key to locate in the <see cref="CommandCollection"/>.</param>
160+
/// <returns>
161+
/// The zero-based index of the first occurrence of key within the entire <see cref="CommandCollection"/>,
162+
/// if found; otherwise, -1.
163+
/// </returns>
164+
public int IndexOf(string key)
165+
{
166+
if (this.TryGetValue(key, out KeyValuePair<string, string> item))
167+
{
168+
return this.IndexOf(item);
169+
}
170+
171+
return -1;
172+
}
173+
174+
/// <inheritdoc/>
175+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
176+
protected override string GetKeyForItem(KeyValuePair<string, string> item) => item.Key;
177+
178+
[MethodImpl(MethodImplOptions.NoInlining)]
179+
private static void ThrowKeyNotFound() => throw new KeyNotFoundException();
180+
}
181+
}

src/ImageSharp.Web/Commands/DictionaryExtensions.cs renamed to src/ImageSharp.Web/Commands/CommandCollectionExtensions.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,20 @@
66
namespace SixLabors.ImageSharp.Web.Commands
77
{
88
/// <summary>
9-
/// Extension methods for <see cref="IDictionary{TKey, TValue}"/>.
9+
/// Extension methods for <see cref="CommandCollectionExtensions"/>.
1010
/// </summary>
11-
public static class DictionaryExtensions
11+
public static class CommandCollectionExtensions
1212
{
1313
/// <summary>
1414
/// Gets the value associated with the specified key or the default value.
1515
/// </summary>
16-
/// <param name="dictionary">The dictionary instance.</param>
16+
/// <param name="collection">The collection instance.</param>
1717
/// <param name="key">The key of the value to get.</param>
18-
/// <typeparam name="TValue">The value type.</typeparam>
19-
/// <typeparam name="TKey">The key type.</typeparam>
2018
/// <returns>The value associated with the specified key or the default value.</returns>
21-
public static TValue GetValueOrDefault<TValue, TKey>(this IDictionary<TKey, TValue> dictionary, TKey key)
19+
public static string GetValueOrDefault(this CommandCollection collection, string key)
2220
{
23-
dictionary.TryGetValue(key, out TValue result);
24-
return result;
21+
collection.TryGetValue(key, out KeyValuePair<string, string> result);
22+
return result.Value;
2523
}
2624
}
2725
}

src/ImageSharp.Web/Commands/IRequestParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ public interface IRequestParser
1616
/// </summary>
1717
/// <param name="context">Encapsulates all HTTP-specific information about an individual HTTP request.</param>
1818
/// <returns>The <see cref="IDictionary{TKey,TValue}"/>.</returns>
19-
IDictionary<string, string> ParseRequestCommands(HttpContext context);
19+
CommandCollection ParseRequestCommands(HttpContext context);
2020
}
2121
}

src/ImageSharp.Web/Commands/PresetOnlyQueryCollectionRequestParser.cs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
@@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Web.Commands
1616
/// </summary>
1717
public class PresetOnlyQueryCollectionRequestParser : IRequestParser
1818
{
19-
private readonly IDictionary<string, IDictionary<string, string>> presets;
19+
private readonly IDictionary<string, CommandCollection> presets;
2020

2121
/// <summary>
2222
/// The command constant for the preset query parameter.
@@ -31,31 +31,39 @@ public PresetOnlyQueryCollectionRequestParser(IOptions<PresetOnlyQueryCollection
3131
this.presets = ParsePresets(presetOptions.Value.Presets);
3232

3333
/// <inheritdoc/>
34-
public IDictionary<string, string> ParseRequestCommands(HttpContext context)
34+
public CommandCollection ParseRequestCommands(HttpContext context)
3535
{
3636
if (context.Request.Query.Count == 0 || !context.Request.Query.ContainsKey(QueryKey))
3737
{
38-
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
38+
// We return new here and below to ensure the collection is still mutable via events.
39+
return new();
3940
}
4041

41-
var requestedPreset = context.Request.Query["preset"][0];
42-
return this.presets.GetValueOrDefault(requestedPreset) ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
42+
string requestedPreset = context.Request.Query[QueryKey][0];
43+
if (this.presets.TryGetValue(requestedPreset, out CommandCollection collection))
44+
{
45+
return collection;
46+
}
47+
48+
return new();
4349
}
4450

45-
private static IDictionary<string, IDictionary<string, string>> ParsePresets(
51+
private static IDictionary<string, CommandCollection> ParsePresets(
4652
IDictionary<string, string> unparsedPresets) =>
4753
unparsedPresets
4854
.Select(keyValue =>
49-
new KeyValuePair<string, IDictionary<string, string>>(keyValue.Key, ParsePreset(keyValue.Value)))
55+
new KeyValuePair<string, CommandCollection>(keyValue.Key, ParsePreset(keyValue.Value)))
5056
.ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value, StringComparer.OrdinalIgnoreCase);
5157

52-
private static IDictionary<string, string> ParsePreset(string unparsedPresetValue)
58+
private static CommandCollection ParsePreset(string unparsedPresetValue)
5359
{
60+
// TODO: Investigate skipping the double allocation here.
61+
// In .NET 6 we can directly use the QueryStringEnumerable type and enumerate stright to our command collection
5462
Dictionary<string, StringValues> parsed = QueryHelpers.ParseQuery(unparsedPresetValue);
55-
var transformed = new Dictionary<string, string>(parsed.Count, StringComparer.OrdinalIgnoreCase);
56-
foreach (KeyValuePair<string, StringValues> keyValue in parsed)
63+
CommandCollection transformed = new();
64+
foreach (KeyValuePair<string, StringValues> pair in parsed)
5765
{
58-
transformed[keyValue.Key] = keyValue.Value.ToString();
66+
transformed.Add(new(pair.Key, pair.Value.ToString()));
5967
}
6068

6169
return transformed;

src/ImageSharp.Web/Commands/QueryCollectionRequestParser.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

4-
using System;
54
using System.Collections.Generic;
65
using Microsoft.AspNetCore.Http;
76
using Microsoft.AspNetCore.WebUtilities;
@@ -15,18 +14,21 @@ namespace SixLabors.ImageSharp.Web.Commands
1514
public sealed class QueryCollectionRequestParser : IRequestParser
1615
{
1716
/// <inheritdoc/>
18-
public IDictionary<string, string> ParseRequestCommands(HttpContext context)
17+
public CommandCollection ParseRequestCommands(HttpContext context)
1918
{
2019
if (context.Request.Query.Count == 0)
2120
{
22-
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
21+
// We return new to ensure the collection is still mutable via events.
22+
return new();
2323
}
2424

25+
// TODO: Investigate skipping the double allocation here.
26+
// In .NET 6 we can directly use the QueryStringEnumerable type and enumerate stright to our command collection
2527
Dictionary<string, StringValues> parsed = QueryHelpers.ParseQuery(context.Request.QueryString.ToUriComponent());
26-
var transformed = new Dictionary<string, string>(parsed.Count, StringComparer.OrdinalIgnoreCase);
28+
CommandCollection transformed = new();
2729
foreach (KeyValuePair<string, StringValues> pair in parsed)
2830
{
29-
transformed[pair.Key] = pair.Value.ToString();
31+
transformed.Add(new(pair.Key, pair.Value.ToString()));
3032
}
3133

3234
return transformed;

src/ImageSharp.Web/Middleware/ImageCommandContext.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

4-
using System.Collections.Generic;
54
using System.Globalization;
65
using Microsoft.AspNetCore.Http;
76
using SixLabors.ImageSharp.Web.Commands;
@@ -22,7 +21,7 @@ public class ImageCommandContext
2221
/// <param name="culture">The culture used to parse commands.</param>
2322
public ImageCommandContext(
2423
HttpContext context,
25-
IDictionary<string, string> commands,
24+
CommandCollection commands,
2625
CommandParser parser,
2726
CultureInfo culture)
2827
{
@@ -38,9 +37,9 @@ public ImageCommandContext(
3837
public HttpContext Context { get; }
3938

4039
/// <summary>
41-
/// Gets the dictionary containing the collection of URI derived processing commands.
40+
/// Gets the collection of URI derived processing commands.
4241
/// </summary>
43-
public IDictionary<string, string> Commands { get; }
42+
public CommandCollection Commands { get; }
4443

4544
/// <summary>
4645
/// Gets the command parser for parsing URI derived processing commands.

src/ImageSharp.Web/Middleware/ImageProcessingContext.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using System.IO;
66
using Microsoft.AspNetCore.Http;
7+
using SixLabors.ImageSharp.Web.Commands;
78

89
namespace SixLabors.ImageSharp.Web.Middleware
910
{
@@ -23,7 +24,7 @@ public class ImageProcessingContext
2324
public ImageProcessingContext(
2425
HttpContext context,
2526
Stream stream,
26-
IDictionary<string, string> commands,
27+
CommandCollection commands,
2728
string contentType,
2829
string extension)
2930
{
@@ -47,10 +48,10 @@ public ImageProcessingContext(
4748
/// <summary>
4849
/// Gets the parsed collection of processing commands.
4950
/// </summary>
50-
public IDictionary<string, string> Commands { get; }
51+
public CommandCollection Commands { get; }
5152

5253
/// <summary>
53-
/// Gets the content type for for the processed image.
54+
/// Gets the content type for the processed image.
5455
/// </summary>
5556
public string ContentType { get; }
5657

0 commit comments

Comments
 (0)