Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace generic dictionary for commands with specialized collection type. #203

Merged
merged 9 commits into from
Jan 27, 2022

Conversation

JimBobSquarePants
Copy link
Member

@JimBobSquarePants JimBobSquarePants commented Jan 21, 2022

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

This breaking change is required in order to allow users to easily insert commands into the processing pipeline via the OnParseCommandsAsync.

It introduces a new type CommandCollection based upon KeyedCollection<string, KeyValuePair<string, string>> to replace the generic IDictionary<string, string> previously used to house parsed commands.

The requirement was brought to my attention in #191 where an issue was correctly identified when attempting to insert commands into the beginning of the command collection.

While the change is breaking and changes the signature of a fundamental component (IImageWebProcessor) I believe that it is an important change to make and we should do so now before it is too late.

Following the change, automatically inserting the command for a potential AutoOrientWebProcessor processor to manage EXIF rotation would be as simple as following.

c.Commands.Insert(0, new(AutoOrientWebProcessor.Key, null));

CC @ronaldbarendse as I want to know what affect this would have on Umbraco.
CC @deanmarcussen as I want to know what affect this would have on Orchard.

@JimBobSquarePants JimBobSquarePants added this to the v2.0.0 milestone Jan 21, 2022
@JimBobSquarePants JimBobSquarePants requested a review from a team January 21, 2022 04:00
@codecov
Copy link

codecov bot commented Jan 21, 2022

Codecov Report

Merging #203 (ec8ecee) into master (7c2b496) will decrease coverage by 0.01%.
The diff coverage is 88.23%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #203      +/-   ##
==========================================
- Coverage   85.56%   85.55%   -0.02%     
==========================================
  Files          54       55       +1     
  Lines        1552     1585      +33     
  Branches      231      237       +6     
==========================================
+ Hits         1328     1356      +28     
- Misses        167      171       +4     
- Partials       57       58       +1     
Flag Coverage Δ
unittests 85.55% <88.23%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
...harp.Web/Processors/BackgroundColorWebProcessor.cs 100.00% <ø> (ø)
...Sharp.Web/Commands/QueryCollectionRequestParser.cs 71.42% <66.66%> (ø)
src/ImageSharp.Web/Commands/CommandCollection.cs 82.35% <82.35%> (ø)
...rc/ImageSharp.Web/Processors/ResizeWebProcessor.cs 91.52% <91.66%> (ø)
...eSharp.Web/Commands/CommandCollectionExtensions.cs 100.00% <100.00%> (ø)
...Commands/PresetOnlyQueryCollectionRequestParser.cs 100.00% <100.00%> (ø)
...c/ImageSharp.Web/Middleware/ImageCommandContext.cs 100.00% <100.00%> (ø)
...mageSharp.Web/Middleware/ImageProcessingContext.cs 100.00% <100.00%> (ø)
.../ImageSharp.Web/Middleware/ImageSharpMiddleware.cs 84.54% <100.00%> (+0.40%) ⬆️
...rc/ImageSharp.Web/Processors/FormatWebProcessor.cs 100.00% <100.00%> (ø)
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7c2b496...ec8ecee. Read the comment docs.

@deanmarcussen
Copy link
Collaborator

It will be marginally breaking for us, only in the sense that the api has changed, so we can't easily release to our preview feed, and get some people trying the new version, as we don't like to push anything that isn't on NuGet.

We do quite a lot with commands, for DDOS prevention, and such, but it's mostly about removal. Either way the api still does what it used to, so all good from us.

@JimBobSquarePants
Copy link
Member Author

@deanmarcussen Thanks for getting back to me. Good to hear it's not an issue.

@ronaldbarendse
Copy link
Contributor

Thanks for bringing this to my attention 👍🏻 We'll only update the ImageSharp dependency to v2 in the next major Umbraco version (v10, planned Q2, 2022), so breaking this API shouldn't be a big issue for us.

However, I'm not aware of any other breaking API changes in ImageSharp/ImageSharp.Web and this would prevent users from upgrading Umbraco 9 sites to use v2, as the codebase uses the affected OnParseCommandsAsync and adds a CropWebProcessor (a custom IImageWebProcessor). It would be a shame if users can't add WebP support and improved memory management, either because Umbraco 10 isn't released yet or they can't upgrade their site (e.g. because of other dependencies).

I've quickly tested this on an Umbraco 9.2.0 project using the nightly NuGet feed (SixLabors.ImageSharp 2.0.0-alpha.0.159 and SixLabors.ImageSharp.Web 2.0.0-alpha.0.3) and both the command parsing (configurable max width/height) and the custom crops still work.

This is the only concern I currently have with these changes, but I also see the value in allowing commands to be inserted (adding auto-rotation, as you already showed).

Copy link
Contributor

@ronaldbarendse ronaldbarendse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of the breaking changes can be avoided by having CommandCollection implement IDictionary<string, string> and forward that to the KeyedCollection<>, right?

That would make accessing the context.Commands in the OnParseCommandsAsync work, as it can still use the members of the IDictionary<string, string> on the new collection. And IImageWebProcessor can keep the existing signature, as that doesn't alter the order of the commands and thus doesn't need the concrete implementation.

}

private CommandCollection(IEqualityComparer<string> comparer)
: base(comparer)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@JimBobSquarePants JimBobSquarePants Jan 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered implementing IDictionary<string, string> but it leads to a confusing API since the type inherits Collection<T>. Contains vs ContainsKey for example, I also didn't want to expose some unordered properties like Values. There's also virtualization issues passing around IDictionary<T> all the time. I also experimented with using a wrapper type with a private implementation of KeyedCollection<T,TItem> but this started to get messy quickly so I took the easy route.

I'm not quite sure what you're pointing out re the constructor. Are you suggesting using the internal dictionary to implement the interface or for something else?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusion can be avoided by using an explicit interface implementation (which might already be required for some conflicting members)...

The point I wanted to make, is that this implementation stores the items both in the collection and internal lookup dictionary, in addition to keeping a sorted list of keys.

Too bad OrderedDictionary isn't using generics, although I see there is an internal MS implementation.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also use the internal lookup dictionary (which might be null, which might need a fallback to an empty list):

Suggested change
public ICollection<string> Keys => this.keys;
public ICollection<string> Keys => this.Dictionary?.Keys;

Copy link
Member Author

@JimBobSquarePants JimBobSquarePants Jan 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The internal lookup Dictionary.Keys property isn't actually in order. There's a private List<T> but I can't touch it which is frustrating so I've duplicated it. Memory usage should be fairly minimal but if it's a problem I'll reimplement the entire type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, of course, I already thought I was missing something! 😖

I see this is only used in ImageSharpMiddleware.StripUnknownCommands(), so might be worthwhile to convert this into something like this:

Suggested change
public ICollection<string> Keys => this.keys;
public IEnumerable<string> Keys
{
get
{
foreach (var item in this)
{
yield return this.GetKeyForItem(item);
}
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a neat suggestion! The enumerator should still be fast here.

I'll not commit this directly as it means I can remove a bunch of overrides at the same time.

@skttl
Copy link

skttl commented Jan 25, 2022

However, I'm not aware of any other breaking API changes in ImageSharp/ImageSharp.Web and this would prevent users from upgrading Umbraco 9 sites to use v2, as the codebase uses the affected OnParseCommandsAsync and adds a CropWebProcessor (a custom IImageWebProcessor). It would be a shame if users can't add WebP support and improved memory management, either because Umbraco 10 isn't released yet or they can't upgrade their site (e.g. because of other dependencies).

I thought it was possible to swap out the implementation in Umbraco, so potentially a package could bridge Umbraco 9 to ImageSharp 2?

@ronaldbarendse
Copy link
Contributor

I thought it was possible to swap out the implementation in Umbraco, so potentially a package could bridge Umbraco 9 to ImageSharp 2?

@skttl Umbraco does indeed use abstractions (for image URL generation and getting image dimensions), but Umbraco.Cms.Infrastructure and Umbraco.Cms.Web.Common still have a tight integration with ImageSharp and ImageSharp.Web, respectively. This means you can't install a version that isn't API compatible (as that would throw a MissingMethodException), although you can use a different package/service.

@JimBobSquarePants I've done some light hacking to get this PR compatible with Umbraco 9: ronaldbarendse@66f843d. Having CommandCollection implement IDictionary<string, string> was quite trivial and removes the need to change the IImageWebProcessor.Process() signature. The only thing that's a bit nasty, is the additional ImageCommandContext.OrderedCommands property, so the existing Commands property can be kept unchanged as well. Let me know what you think of this!

@JimBobSquarePants
Copy link
Member Author

@ronaldbarendse I hard a look at your branch and while the refactor is impressive I still believe the dedicated type is the right way to go.

  • Exposing IDictionary in this manner exposes the less efficient Keys/Values properties where we allocate a new list each time each of these properties are called. If people call them in from an IImageWebProcessor that's gonna suck. We are also now stuck again with the virtual dispatch performance issues.
  • The obsolete code that you've already identified as nasty 😄 is most definitely that. I wouldn't introduce an obsolete attribute for a major version.

Oh and re upgrading a major... There's precedence - Maybe we can convince the powers that be to let it happen. The memory usage improvements alone are well worth it and there's not many libraries that unit test as much as we do.

@ronaldbarendse
Copy link
Contributor

@JimBobSquarePants My attempt was mainly to see whether it was at least possible to minimize the breaking changes so it would still work on an existing Umbraco 9 project. You make valid points about the property/virtual dispatch performance issues and the nastiness it requires, so lets break some (more) stuff! To fix things and make it better of course! 😉

Regarding the precedence though: that major version bump in LightInject got reverted, same as with bumping the NPoco dependency... Umbraco 10 will be an LTS release, so I'd rather see a clean and performing v2 dependency included in the end. Lets have a chat if we need to align our release plans to make that possible.


I do have some suggestions on adding a couple of convenience methods to the CommandCollection (not sure if they all make sense, but I'll leave you to be the judge of that):

public void Add(string key, string value) => this.Add(new(key, value));

public void Insert(int index, string key, string value) => this.Insert(index, new(key, value));

public bool TryGetValue(string key, out string value)
{
    if (this.TryGetValue(key, out KeyValuePair<string, string> keyValue))
    {
        value = keyValue.Value;
        return true;
    }

    value = default;
    return false;
}

@JimBobSquarePants
Copy link
Member Author

Just added the comments for future us. Once this builds I think we're ready to merge.

Copy link
Collaborator

@deanmarcussen deanmarcussen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note, not sure if you're planning on an RC of v2 for ImageSharp and ImageSharp.Web, to NuGet, but if you are we'll take it in OC straight away.

Plenty of people real keen for webp, and I'm looking forward to the new memory allocator

@JimBobSquarePants JimBobSquarePants merged commit 22b42d3 into master Jan 27, 2022
@JimBobSquarePants JimBobSquarePants deleted the js/command-control branch January 27, 2022 10:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants