Skip to content

Commit

Permalink
Replace generic dictionary with specialized collection type.
Browse files Browse the repository at this point in the history
  • Loading branch information
JimBobSquarePants committed Jan 21, 2022
1 parent 7c2b496 commit c84f4ba
Show file tree
Hide file tree
Showing 22 changed files with 221 additions and 99 deletions.
89 changes: 89 additions & 0 deletions src/ImageSharp.Web/Commands/CommandCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace SixLabors.ImageSharp.Web.Commands
{
/// <summary>
/// Represents an ordered collection of processing commands.
/// </summary>
public sealed class CommandCollection : KeyedCollection<string, KeyValuePair<string, string>>
{
private readonly List<string> keys = new();

/// <summary>
/// Initializes a new instance of the <see cref="CommandCollection"/> class.
/// </summary>
public CommandCollection()
: this(StringComparer.OrdinalIgnoreCase)
{
}

private CommandCollection(IEqualityComparer<string> comparer)
: base(comparer)
{
}

/// <summary>
/// Gets an <see cref="ICollection{T}"/> representing the keys of the collection.
/// </summary>
public ICollection<string> Keys => this.keys;

/// <summary>
/// Gets the command value with the specified key.
/// </summary>
/// <param name="key">The key of the element to get.</param>
/// <returns>
/// The command value with the specified key. If a value with the specified key is not
/// found, an exception is thrown.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="key"/> is null.</exception>
/// <exception cref="KeyNotFoundException">An element with the specified key does not exist in the collection.</exception>
public new string this[string key]
{
get
{
if (this.TryGetValue(key, out KeyValuePair<string, string> item))
{
return item.Key;
}

throw new KeyNotFoundException();
}
}

/// <inheritdoc/>
protected override void InsertItem(int index, KeyValuePair<string, string> item)
{
base.InsertItem(index, item);
this.keys.Insert(index, item.Key);
}

/// <inheritdoc/>
protected override void RemoveItem(int index)
{
base.RemoveItem(index);
this.keys.RemoveAt(index);
}

/// <inheritdoc/>
protected override void SetItem(int index, KeyValuePair<string, string> item)
{
base.SetItem(index, item);
this.keys[index] = item.Key;
}

/// <inheritdoc/>
protected override void ClearItems()
{
base.ClearItems();
this.keys.Clear();
}

/// <inheritdoc/>
protected override string GetKeyForItem(KeyValuePair<string, string> item) => item.Key;
}
}
25 changes: 25 additions & 0 deletions src/ImageSharp.Web/Commands/CommandCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System.Collections.Generic;

namespace SixLabors.ImageSharp.Web.Commands
{
/// <summary>
/// Extension methods for <see cref="CommandCollectionExtensions"/>.
/// </summary>
public static class CommandCollectionExtensions
{
/// <summary>
/// Gets the value associated with the specified key or the default value.
/// </summary>
/// <param name="collection">The collection instance.</param>
/// <param name="key">The key of the value to get.</param>
/// <returns>The value associated with the specified key or the default value.</returns>
public static string GetValueOrDefault(this CommandCollection collection, string key)
{
collection.TryGetValue(key, out KeyValuePair<string, string> result);
return result.Value;
}
}
}
2 changes: 1 addition & 1 deletion src/ImageSharp.Web/Commands/IRequestParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ public interface IRequestParser
/// </summary>
/// <param name="context">Encapsulates all HTTP-specific information about an individual HTTP request.</param>
/// <returns>The <see cref="IDictionary{TKey,TValue}"/>.</returns>
IDictionary<string, string> ParseRequestCommands(HttpContext context);
CommandCollection ParseRequestCommands(HttpContext context);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
Expand All @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Web.Commands
/// </summary>
public class PresetOnlyQueryCollectionRequestParser : IRequestParser
{
private readonly IDictionary<string, IDictionary<string, string>> presets;
private readonly IDictionary<string, CommandCollection> presets;

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

/// <inheritdoc/>
public IDictionary<string, string> ParseRequestCommands(HttpContext context)
public CommandCollection ParseRequestCommands(HttpContext context)
{
if (context.Request.Query.Count == 0 || !context.Request.Query.ContainsKey(QueryKey))
{
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
return new();
}

var requestedPreset = context.Request.Query["preset"][0];
return this.presets.GetValueOrDefault(requestedPreset) ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string requestedPreset = context.Request.Query["preset"][0];
return this.presets.GetValueOrDefault(requestedPreset) ?? new();
}

private static IDictionary<string, IDictionary<string, string>> ParsePresets(
private static IDictionary<string, CommandCollection> ParsePresets(
IDictionary<string, string> unparsedPresets) =>
unparsedPresets
.Select(keyValue =>
new KeyValuePair<string, IDictionary<string, string>>(keyValue.Key, ParsePreset(keyValue.Value)))
new KeyValuePair<string, CommandCollection>(keyValue.Key, ParsePreset(keyValue.Value)))
.ToDictionary(keyValue => keyValue.Key, keyValue => keyValue.Value, StringComparer.OrdinalIgnoreCase);

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

return transformed;
Expand Down
11 changes: 6 additions & 5 deletions src/ImageSharp.Web/Commands/QueryCollectionRequestParser.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
Expand All @@ -15,18 +14,20 @@ namespace SixLabors.ImageSharp.Web.Commands
public sealed class QueryCollectionRequestParser : IRequestParser
{
/// <inheritdoc/>
public IDictionary<string, string> ParseRequestCommands(HttpContext context)
public CommandCollection ParseRequestCommands(HttpContext context)
{
if (context.Request.Query.Count == 0)
{
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
return new();
}

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

return transformed;
Expand Down
7 changes: 3 additions & 4 deletions src/ImageSharp.Web/Middleware/ImageCommandContext.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System.Collections.Generic;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using SixLabors.ImageSharp.Web.Commands;
Expand All @@ -22,7 +21,7 @@ public class ImageCommandContext
/// <param name="culture">The culture used to parse commands.</param>
public ImageCommandContext(
HttpContext context,
IDictionary<string, string> commands,
CommandCollection commands,
CommandParser parser,
CultureInfo culture)
{
Expand All @@ -38,9 +37,9 @@ public ImageCommandContext(
public HttpContext Context { get; }

/// <summary>
/// Gets the dictionary containing the collection of URI derived processing commands.
/// Gets the collection of URI derived processing commands.
/// </summary>
public IDictionary<string, string> Commands { get; }
public CommandCollection Commands { get; }

/// <summary>
/// Gets the command parser for parsing URI derived processing commands.
Expand Down
7 changes: 4 additions & 3 deletions src/ImageSharp.Web/Middleware/ImageProcessingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Http;
using SixLabors.ImageSharp.Web.Commands;

namespace SixLabors.ImageSharp.Web.Middleware
{
Expand All @@ -23,7 +24,7 @@ public class ImageProcessingContext
public ImageProcessingContext(
HttpContext context,
Stream stream,
IDictionary<string, string> commands,
CommandCollection commands,
string contentType,
string extension)
{
Expand All @@ -47,10 +48,10 @@ public ImageProcessingContext(
/// <summary>
/// Gets the parsed collection of processing commands.
/// </summary>
public IDictionary<string, string> Commands { get; }
public CommandCollection Commands { get; }

/// <summary>
/// Gets the content type for for the processed image.
/// Gets the content type for the processed image.
/// </summary>
public string ContentType { get; }

Expand Down
11 changes: 4 additions & 7 deletions src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,18 +172,15 @@ public ImageSharpMiddleware(
this.asyncKeyLock = asyncKeyLock;
}

#pragma warning disable IDE1006 // Naming Styles
/// <summary>
/// Performs operations upon the current request.
/// </summary>
/// <param name="context">The current HTTP request context.</param>
/// <returns>The <see cref="Task"/>.</returns>
public async Task Invoke(HttpContext context)
#pragma warning restore IDE1006 // Naming Styles
{
// We expect to get concrete collection type which removes virtual dispatch concerns and enumerator allocations
IDictionary<string, string> parsedCommands = this.requestParser.ParseRequestCommands(context);
Dictionary<string, string> commands = parsedCommands as Dictionary<string, string> ?? new Dictionary<string, string>(parsedCommands, StringComparer.OrdinalIgnoreCase);
CommandCollection commands = this.requestParser.ParseRequestCommands(context);

if (commands.Count > 0)
{
Expand Down Expand Up @@ -243,7 +240,7 @@ await this.ProcessRequestAsync(
commands);
}

private void StripUnknownCommands(Dictionary<string, string> commands, int startAtIndex)
private void StripUnknownCommands(CommandCollection commands, int startAtIndex)
{
var keys = new List<string>(commands.Keys);
for (int index = startAtIndex; index < keys.Count; index++)
Expand All @@ -260,7 +257,7 @@ private async Task ProcessRequestAsync(
HttpContext context,
IImageResolver sourceImageResolver,
ImageContext imageContext,
IDictionary<string, string> commands)
CommandCollection commands)
{
// Create a cache key based on all the components of the requested url
string uri = GetUri(context, commands);
Expand Down Expand Up @@ -511,7 +508,7 @@ private async Task SendResponseAsync(
}
}

private static string GetUri(HttpContext context, IDictionary<string, string> commands)
private static string GetUri(HttpContext context, CommandCollection commands)
{
var sb = new StringBuilder(context.Request.Host.ToString());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private static readonly IEnumerable<string> ColorCommands
public FormattedImage Process(
FormattedImage image,
ILogger logger,
IDictionary<string, string> commands,
CommandCollection commands,
CommandParser parser,
CultureInfo culture)
{
Expand Down
5 changes: 2 additions & 3 deletions src/ImageSharp.Web/Processors/FormatWebProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,15 @@ public FormatWebProcessor(IOptions<ImageSharpMiddlewareOptions> options)
public FormattedImage Process(
FormattedImage image,
ILogger logger,
IDictionary<string, string> commands,
CommandCollection commands,
CommandParser parser,
CultureInfo culture)
{
string extension = commands.GetValueOrDefault(Format);

if (!string.IsNullOrWhiteSpace(extension))
{
IImageFormat format = this.options.Configuration
.ImageFormatsManager.FindFormatByFileExtension(extension);
IImageFormat format = this.options.Configuration.ImageFormatsManager.FindFormatByFileExtension(extension);

if (format != null)
{
Expand Down
8 changes: 4 additions & 4 deletions src/ImageSharp.Web/Processors/IImageWebProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ namespace SixLabors.ImageSharp.Web.Processors
public interface IImageWebProcessor
{
/// <summary>
/// Gets the collection of recognized querystring commands.
/// Gets the collection of recognized command keys.
/// </summary>
IEnumerable<string> Commands { get; }

/// <summary>
/// Processes the image based on the querystring commands.
/// Processes the image based on the given commands.
/// </summary>
/// <param name="image">The image to process.</param>
/// <param name="logger">The type used for performing logging.</param>
/// <param name="commands">The querystring collection containing the processing commands.</param>
/// <param name="commands">The ordered collection containing the processing commands.</param>
/// <param name="parser">The command parser use for parting commands.</param>
/// <param name="culture">
/// The <see cref="CultureInfo"/> to use as the current parsing culture.
Expand All @@ -32,7 +32,7 @@ public interface IImageWebProcessor
FormattedImage Process(
FormattedImage image,
ILogger logger,
IDictionary<string, string> commands,
CommandCollection commands,
CommandParser parser,
CultureInfo culture);
}
Expand Down
4 changes: 2 additions & 2 deletions src/ImageSharp.Web/Processors/QualityWebProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ private static readonly IEnumerable<string> QualityCommands
public FormattedImage Process(
FormattedImage image,
ILogger logger,
IDictionary<string, string> commands,
CommandCollection commands,
CommandParser parser,
CultureInfo culture)
{
if (commands.ContainsKey(Quality))
if (commands.Contains(Quality))
{
// The encoders clamp any values so no validation is required.
int quality = parser.ParseValue<int>(commands.GetValueOrDefault(Quality), culture);
Expand Down
Loading

0 comments on commit c84f4ba

Please sign in to comment.