November Happy Hour will be moved to Thursday December 5th.

IUrlResolver.GetUrl pages with custom action cache issue

Vote:
 

It seems that together with https://world.episerver.com/documentation/Release-Notes/ReleaseNote/?releaseNoteId=CMS-15789 was introduced a bug.

In IUrlResolver there is a method 

    string GetUrl(
      ContentReference contentLink,
      string language,
      UrlResolverArguments urlResolverArguments);

UrlResolverArguments doesn't have a dedicated field for action in the page controller. So, action could be passed as a part of RouteValues dictionary.

In the implementation for UrlResolver UrlResolverArguments are converted to VirtualPathArguments.

And this VirtualPathArguments class already has a dedicated property for Action and that property is used in GetHashCode() method which is used in cache key generation.

However action from RouteValues dictionary is not copied to Action property and during hash code calculation there is a logic which excludes node, action and language route parametwers from hash code calculation.

So, my question is if it's a kown behaviour and custom actions are no longer supported or it's a bug and I just need to wait for a fix?

Meanwhile, I created an overloaded implementation for UrlResolver

public class CustomUrlResolver : DefaultUrlResolver
	{
		public CustomUrlResolver(
			RouteCollection routes,
			IContentLoader contentLoader,
			ISiteDefinitionRepository siteDefinitionRepository,
			TemplateResolver templateResolver,
			IPermanentLinkMapper permanentLinkMapper,
			IContentLanguageSettingsHandler contentLanguageSettingsHandler,
			IContentUrlCache contentUrlCache,
			IContextModeResolver contextModeResolver,
			IRequestHostResolver requestHostResolver)
			: base(
				routes,
				contentLoader,
				siteDefinitionRepository,
				templateResolver,
				permanentLinkMapper,
				contentLanguageSettingsHandler,
				contentUrlCache,
				contextModeResolver,
				requestHostResolver
			)
		{
		}

		/// <summary>
		///EPiServer.Web.Routing.Internal.ContentUrlCacheContext doesn't take into account action if it's a part
		///of RouteValues dictionary. Remove that implementation as soon as it's fixed.
		/// </summary>
		/// <param name="contentLink"></param>
		/// <param name="language"></param>
		/// <param name="virtualPathArguments"></param>
		/// <returns></returns>
		public override string GetUrl(
			ContentReference contentLink,
			string language,
			VirtualPathArguments virtualPathArguments)
		{
			if (virtualPathArguments.RouteValues != null &&
			    virtualPathArguments.RouteValues.ContainsKey(RoutingConstants.ActionKey))
			{
				virtualPathArguments.Action = virtualPathArguments.RouteValues[RoutingConstants.ActionKey] as string;
			}

			return base.GetUrl(
				contentLink,
				language,
				virtualPathArguments
			);
		}
	}

But such customizations is not something which I'd like to see and maintain during EPiServer upgrades in my project:)

#236655
Edited, Dec 08, 2020 12:36
Vote:
 

Thank you for reporting, we looking into it soon.

#245536
Dec 14, 2020 7:48
Vote:
 

Hi

When I'm looking into the code we have explicity taken care of VirtualPathArguments.RouteValues in hash code calculation, would you like to test with latest version (11.20.2) or add a test procedure that can help us more. Thanks in advanced

#245537
Dec 14, 2020 8:34
Vote:
 

Hi Shahram,

Thank you for checking that issue and let me try to explain how to reproduce it.

In my application I use IUrlResolver interface which uses UrlResolverArguments as one of the parameters.

Action name is passed in RouteValues property of UrlResolverArguments .

UrlResolverArguments parameter is converted to VirtualPathArguments in EPiServer.Web.Routing.UrlResolver

    string IUrlResolver.GetUrl(
      ContentReference contentLink,
      string language,
      UrlResolverArguments urlResolverArguments)
    {
      return this.GetUrl(contentLink, language, new VirtualPathArguments(urlResolverArguments));
    }

    string IUrlResolver.GetUrl(
      UrlBuilder urlBuilderWithInternalUrl,
      UrlResolverArguments arguments)
    {
      return this.GetUrl(urlBuilderWithInternalUrl, new VirtualPathArguments(arguments));
    }

And VirtualPathArguments's constructor looks like

    internal VirtualPathArguments(UrlResolverArguments urlArguments)
      : this()
    {
      if (urlArguments == null)
        return;
      this.ContextMode = urlArguments.ContextMode;
      this.ForceCanonical = urlArguments.ForceCanonical;
      this.RouteValues = urlArguments.RouteValues != null ? new RouteValueDictionary(urlArguments.RouteValues) : new RouteValueDictionary();
      this.ForceAbsolute = urlArguments.ForceAbsolute;
    }

Action name is still in RouteValues property and Action property is empty by default.

And hash code is calculated using the following code:

    private static readonly HashSet<string> _defaultRouteValues = new HashSet<string>((IEnumerable<string>) new string[3]
    {
      "node",
      "action",
      "language"
    }, (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);  

    public override int GetHashCode()
    {
      HashCodeCombiner hashCodeCombiner = HashCodeCombiner.Start();
      hashCodeCombiner.Add<string>(this.Action, (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
      hashCodeCombiner.Add((object) this.ForceAbsolute);
      hashCodeCombiner.Add((object) this.ForceCanonical);
      hashCodeCombiner.Add((object) this.ValidateTemplate);
      hashCodeCombiner.Add<string>(this.GetRouteValue(), (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
      return hashCodeCombiner.CombinedHash;
    }

    private string GetRouteValue()
    {
      StringBuilder stringBuilder = new StringBuilder();
      foreach (KeyValuePair<string, object> keyValuePair in (IEnumerable<KeyValuePair<string, object>>) this.RouteValues ?? Enumerable.Empty<KeyValuePair<string, object>>())
      {
        if (!VirtualPathArguments._defaultRouteValues.Contains(keyValuePair.Key))
          stringBuilder.Append(keyValuePair.Key).Append(keyValuePair.Value);
      }
      return stringBuilder.ToString();
    }

You can see that action name in RouteValues property is excluded by "if (!VirtualPathArguments._defaultRouteValues.Contains(keyValuePair.Key))" condition and action in Action property is empty.

So, if I call IUrlResolver.GetUrl to generate urls to the same page with action and without action in 2nd call I receive cached value with the results of the call which was the 1st one due to action name is not taken into account as a part of cache key.

#245539
Edited, Dec 14, 2020 14:06
Vote:
 

Thanks for a very comprehensive report Borys Denysenko, this was very useful for troubleshooting this issue on our site.

#248057
Feb 02, 2021 23:22
Vote:
 

Hi!

We have also encountered this bug. Can't see that it has been fixed yet? Are you planning on releasing a fix soon?

#249967
Mar 12, 2021 13:50
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.