-
Notifications
You must be signed in to change notification settings - Fork 9
Enable SrcSet support with two different input formats for Image tag helper #46
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
base: main
Are you sure you want to change the base?
Enable SrcSet support with two different input formats for Image tag helper #46
Conversation
…er support, Specific exception handling - Only catch JsonException instead of all exceptions, Better ASP.NET Core compatibility - Replaced HttpUtility with QueryHelpers
…e tests - Keep GetSitecoreMediaUriWithPreservation - Add 14 new test cases covering srcset parameter merging, validation, and edge cases - Fix null handling and zero/negative width filtering in ImageTagHelper - Ensure development environment compatibility with CDN/edge URLs
…ty and comprehensive test cases added Updated - Enable SrcSet support with Content SDK approach compatibility and comprehensive test cases added
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements comprehensive responsive image support for the ImageTagHelper by adding srcset and sizes attribute functionality. The implementation enables developers to output responsive images using Sitecore's built-in media resizing capabilities while maintaining compatibility with the Content SDK behavior.
- Added
SrcSetandSizesproperties toImageTagHelperwith support for multiple input formats (anonymous objects, dictionary arrays, JSON strings) - Implemented parameter merging logic that preserves existing URL parameters while allowing srcSet parameters to override base imageParams
- Added extensive test coverage for various srcSet scenarios including parameter precedence, validation, and edge cases
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
ImageTagHelper.cs |
Core implementation of srcSet/sizes support with parameter parsing and URL generation logic |
SitecoreFieldExtensions.cs |
Added parameter preservation methods and URL building logic for responsive images |
ImageTagHelperFixture.cs |
Comprehensive test suite covering all srcSet functionality and edge cases |
src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs
Outdated
Show resolved
Hide resolved
…ng StringComparer.OrdinalIgnoreCase for the dictionary to ensure consistent parameter key handling
src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs
Outdated
Show resolved
Hide resolved
tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs
Outdated
Show resolved
Hide resolved
tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs
Outdated
Show resolved
Hide resolved
tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs
Outdated
Show resolved
Hide resolved
tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs
Outdated
Show resolved
Hide resolved
tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs
Outdated
Show resolved
Hide resolved
…directly creating the final mergedParams dictionary
…ethod - ApplyJssMediaUrlPrefix
…t to ParseJsonSrcSet, move the type checks to the calling method and simplifying ParseJsonSrcSet
…t to ParseJsonSrcSet, move the type checks to the calling method and simplifying ParseJsonSrcSet
…he test cases to validate the fix
| string urlWithoutQuery; | ||
| string? query = null; | ||
|
|
||
| if (uri.IsAbsoluteUri) | ||
| { | ||
| urlWithoutQuery = uri.GetLeftPart(UriPartial.Path); | ||
| query = uri.Query; | ||
| } | ||
| else | ||
| { | ||
| // For relative URIs, manually split on '?' | ||
| var original = uri.OriginalString; | ||
| int idx = original.IndexOf('?'); | ||
| if (idx >= 0) | ||
| { | ||
| urlWithoutQuery = original.Substring(0, idx); | ||
| query = original.Substring(idx); | ||
| } | ||
| else | ||
| { | ||
| urlWithoutQuery = original; | ||
| } | ||
| } | ||
|
|
||
| if (!string.IsNullOrEmpty(query)) | ||
| { | ||
| var parsedQuery = QueryHelpers.ParseQuery(query); | ||
| foreach (KeyValuePair<string, StringValues> kvp in parsedQuery) | ||
| { | ||
| parameters[kvp.Key] = kvp.Value.Count > 0 ? kvp.Value[0] : null; | ||
| } | ||
| } | ||
|
|
||
| return urlWithoutQuery; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like we could simplify this massively. Would the following work instead?
string url = $"${uri.Scheme}://${uri.Host}";
NameValueCollection queryParams = HttpUtility.ParseQueryString(uri.Query);
foreach (string? param in queryParams.AllKeys)
{
if (!string.IsNullOrEmpty(param))
{
parameters[param] = queryParams[param];
}
}
return url;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion Rob, I tested with this but couple of test cases get failed, since it requires to handle relative URL's as well. So I tried to simplified it in this way -
string original = uri.OriginalString;
int queryIndex = original.IndexOf('?');
if (queryIndex >= 0)
{
string query = original.Substring(queryIndex);
var parsedQuery = QueryHelpers.ParseQuery(query);
foreach (var kvp in parsedQuery)
{
parameters[kvp.Key] = kvp.Value.Count > 0 ? kvp.Value[0] : null;
}
return original.Substring(0, queryIndex);
}
return original;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need to do all of that string matching, that's what I'm trying to avoid. You should be able to get all of the information you need from the Uri class. Check out the docs here so you can see what I mean: https://learn.microsoft.com/en-us/dotnet/api/system.uri?view=net-8.0
Maybe something like:
string url = uri.IsAbsoluteUri ? $"${uri.Scheme}://{uri.Host}{uri.AbsolutePath}" : uri.AbsolutePath;
NameValueCollection queryParams = HttpUtility.ParseQueryString(uri.Query);
foreach (string? param in queryParams.AllKeys)
{
if (!string.IsNullOrEmpty(param))
{
parameters[param] = queryParams[param];
}
}
return url;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Rob,
Thanks for clarifying, I've tried and tested the approach you suggested in couple of different ways, using only the Uri class properties (like uri.AbsolutePath and uri.Query) to avoid manual string matching, as described in the .NET docs. This works for absolute URIs and root relative URIs (e.g., /media/image.png). However, in .NET, if I pass a simple relative URI such as media/image.png?foo=1, accessing uri.Query will throw an InvalidOperationException. That causes some of the tests get failed. Let me know what's your suggestions on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update : I've updated the ParseUrlParams method to handle absolute and relative URIs separately.
- For absolute URIs, I'm using
Uriproperties (Scheme,Host,AbsolutePath,Query) andHttpUtility.ParseQueryString. - For relative URIs, since accessing
Uri.QuerythrowsInvalidOperationException, I'm falling back to string matching and usingQueryHelpers.ParseQuery.
…ions to handle absolute and relative URIs separately
… absolute URIs separately - Replace string manipulation with Uri class properties for absolute URIs - Add proper handling for relative URIs that throw InvalidOperationException when accessing Uri.Query - Use HttpUtility.ParseQueryString for absolute URIs and QueryHelpers.ParseQuery for relative URIs - Add explanatory comments for the if/else branching logic - Simplify ParseUrlParams method while maintaining compatibility with all URI types
|
|
||
| if (urlStr == null) | ||
| { | ||
| return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please avoid multiple return statements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedbacks Ivan, I refactored GetMediaLink to use a single return statement - Krishanthaudayakumara#26
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same adjustment to GetMediaLinkForSrcSet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure Ivan, Did the same for GetMediaLinkForSrcSet here - https://github.com/Krishanthaudayakumara/ASP.NET-Core-SDK/pull/30/files
src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs
Outdated
Show resolved
Hide resolved
| { | ||
| if (parameters == null) | ||
| { | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the "escape hatch" here for performance reasons; but I hate return statements like these. My IDE turns this method into the following:
private static void AddParametersToResult(Dictionary<string, object?> result, object? parameters, bool skipNullValues = false)
{
switch (parameters)
{
case null:
break;
case Dictionary<string, object?> paramDict:
foreach (KeyValuePair<string, object?> kvp in paramDict.Where(kvp => !skipNullValues || kvp.Value != null))
{
result[kvp.Key] = kvp.Value;
}
break;
default:
RouteValueDictionary routeValues = new(parameters);
foreach (KeyValuePair<string, object?> kvp in routeValues.Where(kvp => !skipNullValues || kvp.Value != null))
{
result[kvp.Key] = kvp.Value;
}
break;
}
}Same result, different readability. Also read into the constructor of RouteValueDictionary, it'll show you a cool method of extracting values from a variety of valid parameter formats without locking yourself onto a specific one. Because seems we don't have overrides for Dictionary<string, object?> specifically anywhere making it less likely for that code branch to be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get the point Ivan, So refactored AddParametersToResult to use a switch pattern, eliminating the early return and improving readability - Krishanthaudayakumara#26
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And what did you think of the RouteValueDictionary constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the RouteValueDictionary constructor is a handy escape hatch , it accepts IDictionary, POCOs and anonymous objects , normalises keys and copies values so we don’t need lots of special cases.
src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/SitecoreFieldExtensions.cs
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs
Outdated
Show resolved
Hide resolved
src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs
Outdated
Show resolved
Hide resolved
…ng the explicit type declarations with target-typed new expressions
…LINQ to eliminate the early return statement
…atement in the ParseUrlParams method
…tAttribute - ImageTagHelper
…8 - Style, Readability, and Improvements - Refactored methods to avoid multiple return statements and redundant branches. - Replaced explicit type declarations with target-typed new expressions (IDE0090). - Eliminated early returns in favor of switch/case patterns for better readability. - Updated substring extraction and return statements to use C# range indexers. - Removed all var usages, replacing them with explicit types. - Removed unnecessary null checks and used collection initializer syntax for lists. - Improved overall code clarity, consistency, and compliance with project style guidelines.
|
|
||
| if (urlStr == null) | ||
| { | ||
| return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same adjustment to GetMediaLinkForSrcSet?
| /// <param name="uri">The Uri with potential query parameters.</param> | ||
| /// <param name="parameters">The dictionary to add parsed parameters to.</param> | ||
| /// <returns>The URL without query parameters.</returns> | ||
| private static string ParseUrlParams(Uri? uri, Dictionary<string, object?> parameters) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we squeeze 3 functions into 1? Seeing all these returns I would consider perhaps extracting them as a function on their own and isolating those returns allowing you to do the "branch selection" in this function. As I mentioned earlier in this same file, I dislike multiple return statements as it makes debugging harder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense Ivan, I refactored URL parsing to single return style with extracting ParseAbsoluteUriParams and ParseRelativeUriParams as separate methods - https://github.com/Krishanthaudayakumara/ASP.NET-Core-SDK/pull/30/files
| { | ||
| if (parameters == null) | ||
| { | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And what did you think of the RouteValueDictionary constructor?
- Extract ParseAbsoluteUriParams and ParseRelativeUriParams; make ParseUrlParams a single-return wrapper. - Convert ParseRelativeUriParams and GetSitecoreMediaUriWithPreservation to single-return style. - Preserve existing parsing behavior for absolute and relative URIs.
…ediaUriWithPreservation
…le - PR Review - 09/15 - Extract ParseAbsoluteUriParams and ParseRelativeUriParams; make ParseUrlParams a single-return wrapper. - Convert ParseRelativeUriParams and GetSitecoreMediaUriWithPreservation to single-return style. - Preserve existing parsing behavior for absolute and relative URIs. - Improve debuggability and reduce branching/early return
Description / Motivation
This PR implements responsive image support for the
ImageTagHelperby addingsrcsetandsizesattribute functionality, enabling developers to output responsive images using Sitecore's built-in media resizing capabilities.Key Changes:
Added
SrcSetandSizesProperties: Extended theImageTagHelperwith new properties to support responsive image configuration for both<sc-img>and<img>tags.Multiple Input Format Support: The
SrcSetproperty accepts two different input formats for maximum flexibility:new object[] { new { w = 800 }, new { mw = 400 } }new Dictionary<string, object> { {"w", 800} }Content SDK Parameter Priority: Implements the same parameter precedence as Content SDK:
w(width) takes priority overmw(max width) for srcset descriptorsw>mw>width>maxWidthgetSrcSetfunction behavior exactlyEnhanced Parameter Merging with Preservation:
imageParamsare merged with individualsrcSetparameterssrcSetparameters overrideimageParamsfor each srcset entryGetSitecoreMediaUriWithPreservationmethod to preserve existing query parameters{ ...imageParams, ...params }merge patternDual URL Processing Methods:
GetSitecoreMediaUri: Maintains existing behavior (strips query parameters)GetSitecoreMediaUriWithPreservation: Preserves critical parameters for responsive imagesGetMediaLinkForSrcSet: Specialized method for srcSet URL generation with parameter preservationContent SDK Compatibility:
The implementation now produces identical output to the Content SDK's
Imagecomponent:Content SDK (React):
ASP.NET Core SDK (Razor):
Technical Implementation Details:
Parameter Preservation Architecture:
MergeParameters(): Merges base and override parameters using reflection and dictionary handlingGetSitecoreMediaUriWithPreservation(): Preserves existing URL parameters while adding new onesConvertToStringDictionary(): Converts objects to string dictionaries for URL buildingGetMediaLinkForSrcSet(): Specialized extension method for responsive image URL generationJsonElement Handling: Enhanced support for JSON deserialization scenarios to handle
JsonElementtypes properlyHelper Methods: Added
AddResponsiveImageAttributes()overloads to eliminate code duplication acrossProcess(),GenerateImage(), and editable markup methodsUsage Examples:
This implementation ensures feature parity with the Content SDK while maintaining the familiar ASP.NET Core TagHelper syntax, full backward compatibility, and proper parameter preservation for functional image display.
Testing
Terms
Fix #16