Skip to content

Conversation

@Krishanthaudayakumara
Copy link
Collaborator

@Krishanthaudayakumara Krishanthaudayakumara commented Jul 9, 2025

Description / Motivation

This PR implements responsive image support for the ImageTagHelper by adding srcset and sizes attribute functionality, enabling developers to output responsive images using Sitecore's built-in media resizing capabilities.

Key Changes:

  1. Added SrcSet and Sizes Properties: Extended the ImageTagHelper with new properties to support responsive image configuration for both <sc-img> and <img> tags.

  2. Multiple Input Format Support: The SrcSet property accepts two different input formats for maximum flexibility:

    • Anonymous objects: new object[] { new { w = 800 }, new { mw = 400 } }
    • Dictionary arrays: new Dictionary<string, object> { {"w", 800} }
  3. Content SDK Parameter Priority: Implements the same parameter precedence as Content SDK:

    • w (width) takes priority over mw (max width) for srcset descriptors
    • Extended support for legacy parameters: w > mw > width > maxWidth
    • Only entries with valid width parameters are included in srcset
    • Matches Content SDK's getSrcSet function behavior exactly
  4. Enhanced Parameter Merging with Preservation:

    • Base imageParams are merged with individual srcSet parameters
    • srcSet parameters override imageParams for each srcset entry
    • Critical Fix: Added GetSitecoreMediaUriWithPreservation method to preserve existing query parameters
    • Preserves essential Sitecore parameters (rev, db, la, vs, ts, hash, ttc, tt, etc.)
    • Follows Content SDK's { ...imageParams, ...params } merge pattern
  5. Dual URL Processing Methods:

    • Legacy GetSitecoreMediaUri: Maintains existing behavior (strips query parameters)
    • New GetSitecoreMediaUriWithPreservation: Preserves critical parameters for responsive images
    • GetMediaLinkForSrcSet: Specialized method for srcSet URL generation with parameter preservation
    • Ensures backward compatibility while fixing responsive image display issues

Content SDK Compatibility:

The implementation now produces identical output to the Content SDK's Image component:

Content SDK (React):

<Image 
  field={imageField} 
  imageParams={{quality: 80}} 
  srcSet={[{w: 800}, {mw: 400}]} 
  sizes="(min-width: 768px) 800px, 400px" 
/>

ASP.NET Core SDK (Razor):

<sc-img asp-for="@Model.ImageField" 
        image-params='new { quality = 80 }'
        src-set='new object[] { new { w = 800 }, new { mw = 400 } }'
        sizes="(min-width: 768px) 800px, 400px" />

Technical Implementation Details:

  1. Parameter Preservation Architecture:

    • MergeParameters(): Merges base and override parameters using reflection and dictionary handling
    • GetSitecoreMediaUriWithPreservation(): Preserves existing URL parameters while adding new ones
    • ConvertToStringDictionary(): Converts objects to string dictionaries for URL building
    • GetMediaLinkForSrcSet(): Specialized extension method for responsive image URL generation
  2. JsonElement Handling: Enhanced support for JSON deserialization scenarios to handle JsonElement types properly

  3. Helper Methods: Added AddResponsiveImageAttributes() overloads to eliminate code duplication across Process(), GenerateImage(), and editable markup methods

Usage Examples:

<!-- Basic responsive image -->
<sc-img asp-for="@Model.HeroImage" 
        src-set='new object[] { new { w = 1200 }, new { w = 800 }, new { w = 400 } }'
        sizes="(min-width: 1200px) 1200px, (min-width: 800px) 800px, 400px" />

<!-- With base parameters (preserves existing URL params) -->
<img asp-for="@Model.ProductImage" 
     image-params='new { quality = 80, format = "webp" }'
     src-set='new object[] { new { w = 800, h = 600 }, new { mw = 400, mh = 300 } }'
     sizes="(min-width: 768px) 800px, 400px" />

<!-- Legacy parameter support -->
<sc-img asp-for="@Model.LegacyImage" 
        src-set='new object[] { new { width = 800 }, new { maxWidth = 400 } }'
        sizes="(min-width: 768px) 800px, 400px" />

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

  • The Unit & Integration tests are passing.
  • I have added the necessary tests to cover my changes.

Terms

  • I agree to follow this project's Code of Conduct.

Fix #16

@robearlam robearlam requested review from Copilot and robearlam and removed request for robearlam July 9, 2025 22:56

This comment was marked as outdated.

@robearlam robearlam marked this pull request as draft July 11, 2025 00:48
…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
Copy link

Copilot AI left a 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 SrcSet and Sizes properties to ImageTagHelper with 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

…ng StringComparer.OrdinalIgnoreCase for the dictionary to ensure consistent parameter key handling
@robearlam robearlam marked this pull request as ready for review July 17, 2025 00:43
@Krishanthaudayakumara Krishanthaudayakumara changed the title Enable SrcSet support with three different input formats for Image tag helper Enable SrcSet support with two different input formats for Image tag helper Jul 31, 2025
Comment on lines 189 to 222
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;
Copy link
Member

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;

Copy link
Collaborator Author

@Krishanthaudayakumara Krishanthaudayakumara Aug 1, 2025

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;

b15b4c1

Copy link
Member

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;

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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 Uri properties (Scheme, Host, AbsolutePath, Query) and HttpUtility.ParseQueryString.
  • For relative URIs, since accessing Uri.Query throws InvalidOperationException, I'm falling back to string matching and using QueryHelpers.ParseQuery.

ab6e358

…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;
Copy link
Member

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.

Copy link
Collaborator Author

@Krishanthaudayakumara Krishanthaudayakumara Aug 19, 2025

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

Copy link
Member

Choose a reason for hiding this comment

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

Same adjustment to GetMediaLinkForSrcSet?

Copy link
Collaborator Author

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

{
if (parameters == null)
{
return;
Copy link
Member

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?

Copy link
Collaborator Author

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

Copy link
Member

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?

Copy link
Collaborator Author

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.

@sc-ivanlieckens sc-ivanlieckens added the enhancement New feature or request label Aug 18, 2025
@sc-ivanlieckens sc-ivanlieckens moved this to In review in ASP.NET Core SDK Aug 18, 2025
…ng the explicit type declarations with target-typed new expressions
…LINQ to eliminate the early return statement
…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;
Copy link
Member

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)
Copy link
Member

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.

Copy link
Collaborator Author

@Krishanthaudayakumara Krishanthaudayakumara Sep 8, 2025

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;
Copy link
Member

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.
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

Responsive image srcset support for Image tag helper

3 participants