private static string MakeLegalQueryString(Controller controller, CachedMethod method, QueryStringBehavior stripQueryStrings) { if (method.PreservedParameters != null) { var sb = new StringBuilder(); int i = 0; foreach (var preserved in method.PreservedParameters.Intersect(controller.Request.QueryString.AllKeys)) { sb.AppendFormat($"{(i++ == 0 ? '?' : '&')}{HttpUtility.UrlEncode(preserved)}={HttpUtility.UrlEncode(controller.Request.Params.Get(preserved))}"); } return(sb.ToString()); } return(string.Empty); }
private static bool DetermineSeoRedirect(Controller controller, CachedMethod method, out string destination) { bool redirect = false; string currentAction = (string)controller.RouteData.Values["action"]; string currentController = (string)controller.RouteData.Values["controller"]; //tentatively.... destination = HttpContext.Current.Request.Url.AbsolutePath; //Case-based redirect if (currentAction != method.Action) { redirect = true; destination = destination.Replace(currentAction, method.Action); } //Case-based redirect if (currentController != method.Controller) { redirect = true; destination = destination.Replace(currentController, method.Controller); } //Trailing-backslash configuration (this is the simplification of the NOT XNOR) if ((method.IsIndex != destination.EndsWith("/"))) { redirect = true; destination = string.Format("{0}{1}", destination.TrimEnd(new[] { '/' }), method.IsIndex ? "/" : ""); } //No Index in link if (method.IsIndex && destination.EndsWith("/Index/")) { redirect = true; const string search = "Index/"; destination = destination.Remove(destination.Length - search.Length); } return redirect; }
public static void SeoRedirect(Controller controller, [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0) { string key = string.Format("{0}:{1}", filePath, lineNumber); CachedMethod lastMethod; if (!MethodCache.TryGetValue(key, out lastMethod)) { var method = new StackFrame(1).GetMethod(); lastMethod = new CachedMethod { Action = method.Name, Controller = method.DeclaringType.Name.Remove(method.DeclaringType.Name.Length - "Controller".Length), IsIndex = method.Name == "Index" }; MethodCache.Add(key, lastMethod); } string destination; if (DetermineSeoRedirect(controller, lastMethod, out destination)) { controller.Response.RedirectPermanent(destination); } }
private static bool DetermineSeoRedirect(Controller controller, CachedMethod method, QueryStringBehavior stripQueryStrings, out string destination) { bool redirect = false; string currentAction = (string)controller.RouteData.Values["action"]; string currentController = (string)controller.RouteData.Values["controller"]; //tentatively.... //note: not using a string builder because the assumption is that most requests are correct destination = HttpContext.Current.Request.Url.AbsolutePath; //Case-based redirect if (currentAction != method.Action) { redirect = true; destination = destination.Replace(currentAction, method.Action); } //Case-based redirect if (currentController != method.Controller) { redirect = true; destination = destination.Replace(currentController, method.Controller); } //Trailing-backslash configuration (this is the simplification of the NOT XNOR) if ((method.IsIndex != destination.EndsWith("/"))) { redirect = true; destination = string.Format("{0}{1}", destination.TrimEnd('/'), method.IsIndex ? "/" : ""); } //No Index in link if (method.IsIndex && destination.EndsWith("/Index/")) { redirect = true; const string search = "Index/"; destination = destination.Remove(destination.Length - search.Length); } //Query strings if (stripQueryStrings == QueryStringBehavior.StripAll || method.PreservedParameters == null) { redirect = redirect || HttpContext.Current.Request.QueryString.Count > 0; } else if (stripQueryStrings == QueryStringBehavior.KeepActionParameters) { if (HttpContext.Current.Request.QueryString.AllKeys.Any(k => Array.BinarySearch(method.PreservedParameters, k) < 0)) { redirect = true; var i = 0; StringBuilder qsBuilder = null; foreach (var key in HttpContext.Current.Request.QueryString.AllKeys.Where(k => Array.BinarySearch(method.PreservedParameters, k) >= 0)) { var value = HttpContext.Current.Request.QueryString[key]; qsBuilder = qsBuilder ?? new StringBuilder(); qsBuilder.AppendFormat("{0}{1}{2}{3}", i == 0 ? '?' : '&', key, string.IsNullOrEmpty(value) ? "" : "=", string.IsNullOrEmpty(value) ? "" : HttpUtility.UrlEncode(value)); ++i; } if (qsBuilder != null) { destination += qsBuilder.ToString(); } } else { //Keep query string parameters as-is destination += HttpContext.Current.Request.Url.Query; } } else //QueryStringBehavior.KeepAll { destination += HttpContext.Current.Request.Url.Query; } return(redirect); }
private static void SeoRedirect(Controller controller, QueryStringBehavior stripQueryStrings, string[] additionalPreservedKeys, MethodBase callingMethod, ulong key) { var cachedMethod = new CachedMethod { Action = callingMethod.Name, Controller = callingMethod.DeclaringType.Name.Remove(callingMethod.DeclaringType.Name.Length - "Controller".Length), IsIndex = callingMethod.Name == "Index" }; if (stripQueryStrings == QueryStringBehavior.KeepActionParameters) { //Optimization: if no explicitly preserved query strings and no action parameters, avoid creating HashSet and strip all if ((additionalPreservedKeys == null || additionalPreservedKeys.Length == 0) && callingMethod.GetParameters().Length == 0) { //This won't actually persist anywhere because it's only set once during the reflection phase and not included in the cache entry //we're relying on checking if HashSet == null in DetermineSeoRedirect #if DEBUG //I'd leave it in here and rely on the compiler to strip it out, but... stripQueryStrings = QueryStringBehavior.StripAll; #endif } else { int i = 0; bool skippedId = false; bool routeHasId = controller.RouteData.Values.ContainsKey("id"); bool requestHasId = HttpContext.Current.Request.QueryString.Keys.Cast <string>().Any(queryKey => queryKey == "id"); cachedMethod.PreservedParameters = new string[(callingMethod.GetParameters().Length + (additionalPreservedKeys?.Length ?? 0))]; foreach (var preserved in callingMethod.GetParameters()) { //Optimization: remove the 'id' parameter if it's determined by the route, potentially saving on HashSet lookup entirely if (!skippedId && routeHasId && !requestHasId && preserved.Name == "id") { //The parameter id is part of the route and not obtained via query string parameters if (cachedMethod.PreservedParameters.Length == 1) { //Bypass everything, no need for parameter preservation //This won't actually persist anywhere because it's only set once during the reflection phase and not included in the cache entry //we're relying on checking if HashSet == null in DetermineSeoRedirect #if DEBUG //I'd leave it in here and rely on the compiler to strip it out, but... stripQueryStrings = QueryStringBehavior.StripAll; #endif cachedMethod.PreservedParameters = null; break; } skippedId = true; var newPreserved = new string[cachedMethod.PreservedParameters.Length - 1]; Array.Copy(cachedMethod.PreservedParameters, 0, newPreserved, 0, i); cachedMethod.PreservedParameters = newPreserved; continue; } cachedMethod.PreservedParameters[i++] = preserved.Name; } if (additionalPreservedKeys != null) { foreach (var preserved in additionalPreservedKeys) { cachedMethod.PreservedParameters[i++] = preserved; } } if (cachedMethod.PreservedParameters != null && cachedMethod.PreservedParameters.Length > 1) { Array.Sort(cachedMethod.PreservedParameters); } } } MethodCache.TryAdd(key, cachedMethod); string destination; if (DetermineSeoRedirect(controller, cachedMethod, stripQueryStrings, out destination)) { controller.Response.Headers.Add("X-Redirect-Reason", "NeoSmart SEO Rule"); controller.Response.RedirectPermanent(destination); } }