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

Fix case insensitive dictionary problem (Lombiq Technologies: OCORE-197) #16623

Merged
merged 16 commits into from
Aug 28, 2024

Conversation

sarahelsaig
Copy link
Contributor

@sarahelsaig sarahelsaig commented Aug 26, 2024

Fixes #16585

Easiest way to test:

  1. Create a new site with Blog recipe
  2. Go to Admin > Design > Templates
  3. Click "Add Template"
  4. Name it "Menu", "MENU", or "mEnU", set some custom content.
  5. Load the frontend and observe the overridden menu.

@sarahelsaig sarahelsaig mentioned this pull request Aug 26, 2024
30 tasks
Copy link
Member

@MikeAlhayek MikeAlhayek left a comment

Choose a reason for hiding this comment

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

@sarahelsaig what line fixed the referenced issue?

@@ -352,13 +352,17 @@ public async Task<ActionResult> ListPost(ContentOptions options, IEnumerable<str
{
return Forbid();
}

// Prevent multiple enumeration.
var itemIdList = itemIds as IList<string> ?? itemIds?.ToList() ?? new List<string>();
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure this is needed. Can you explain how this prevents multiple enumerations?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here you had itemIds.Contains(x.Key). This means itemIds was enumerated templatesDocument.Templates.Count times, which is wasteful if the object is not already IList<string>.

Copy link
Member

Choose a reason for hiding this comment

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

How about this instead?
The following code is more readable, does reduce enumeration, does not use as much memory and does not waste resources when Remove is not being request.

        if (itemIds?.Any() == true)
        {
            switch (options.BulkAction)
            {
                case ContentsBulkAction.None:
                    break;
                case ContentsBulkAction.Remove:

                    var templatesDocument = options.AdminTemplates
                        ? await _adminTemplatesManager.LoadTemplatesDocumentAsync()
                        : await _templatesManager.LoadTemplatesDocumentAsync();

                    foreach (var itemId in itemIds)
                    {
                        if (!templatesDocument.Templates.ContainsKey(itemId))
                        {
                            continue;
                        }

                        if (options.AdminTemplates)
                        {
                            await _adminTemplatesManager.RemoveTemplateAsync(itemId);

                            continue;
                        }

                        await _templatesManager.RemoveTemplateAsync(itemId);
                    }
                    await _notifier.SuccessAsync(H["Templates successfully removed."]);
                    break;
                default:
                    return BadRequest();
            }
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The if (itemIds?.Any() == true) also does as an enumeration on its own. I guess it can be replaced with if (itemIds != null) without too much performance hit (probably even a perf gain on average as it should be very rare that an empty ID list is posted.

Also it occurred to me that this code compares keys. If we use emplatesDocument.Templates.Keys, then we are just looking for the union of two string sets. Linq already has an extension method for that. see commit.

Copy link
Member

Choose a reason for hiding this comment

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

yes .Any() iterates over the IEnumerable checking if there is at least one item. This is okay because it's fast since it does not iterate over every item. May be better to use itemIds != null && itemIds.Any() this way you don't bother loading the document or applying the union logic where there is no work to be done.

@sarahelsaig
Copy link
Contributor Author

sarahelsaig what line fixed the referenced issue?

The changes in the file src/OrchardCore.Modules/OrchardCore.Templates/Models/TemplatesDocument.cs fixed the issue.

I have made the same changes for all other dictionary properties with a StringComparer.OrdinalIgnoreCase type default value, because the problem I have described here is applicable to any such property.

@gvkries
Copy link
Contributor

gvkries commented Aug 27, 2024

Instead of hard-coding this into the documents, shouldn't we use a JsonConverter instead if this applies to all dictonaries?

@sarahelsaig
Copy link
Contributor Author

Instead of hard-coding this into the documents, shouldn't we use a JsonConverter instead if this applies to all dictonaries?

That would be a converter that checks every object for dictionary properties with a particular default value. So some kind of a partial converter. Is that actually possible in STJ?

@gvkries
Copy link
Contributor

gvkries commented Aug 27, 2024

IIRC, you can apply a converter to a property, via the JsonConverterAttribute. The converter would be something like public class CaseInsensitiveDictionaryConverter<TKey, TValue> : JsonConverter<Dictionary<TKey, TValue>> and returns a case-insensitive dictionary instead of a default one.

@gvkries
Copy link
Contributor

gvkries commented Aug 27, 2024

@sarahelsaig
Copy link
Contributor Author

IIRC, you can apply a converter to a property, via the JsonConverterAttribute. The converter would be something like public class CaseInsensitiveDictionaryConverter<TKey, TValue> : JsonConverter<Dictionary<TKey, TValue>> and returns a case-insensitive dictionary instead of a default one.

Ok, I did that. I was confused, because this still involves some code you have to add to each dictionary prop.


namespace OrchardCore.BackgroundTasks.Models;

public class BackgroundTaskDocument : Document
{
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<BackgroundTaskSettings>))]
Copy link
Member

Choose a reason for hiding this comment

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

You see how these properties already had a dictionary with the correct casing? I believed that's a setting on the JSON options to prefer using the existing instance instead of creating a new one (which is what the attribute does).

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@MikeAlhayek Nice find. Unfortunately it's only for Dictionary<string, string> instead of Dictionary<string, TValue>.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's already set in JOptions.Base. So looks like it's not enough by itself.

@MikeAlhayek
Copy link
Member

MikeAlhayek commented Aug 27, 2024

@sarahelsaig Please remove EnumerableExtensions since it is no longer used.

@sarahelsaig
Copy link
Contributor Author

sarahelsaig Please remove EnumerableExtensions since it is no longer used.

Ok, removed.

Comment on lines -15 to +18
public Processing.ResizeMode Mode { get; set; }
public Processing.Format Format { get; set; }
public ResizeMode Mode { get; set; }
public Format Format { get; set; }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I called Rider's "Organize imports" feature to get rid of dead using namespaces and it did this. I think it looks better, but comment here if I should revert them.

Copy link
Contributor

@gvkries gvkries left a comment

Choose a reason for hiding this comment

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

LGTM, but let's see what @sebastienros and @MikeAlhayek have to say.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Breaking shape name resolution regarding menu shape
4 participants