Always seem to forget that StringExtensions.WithTrailingSlash(...)
is for file paths and not url paths. When using for url paths it’ll put a /
at the end no matter what, which is incorrect.
Not sure if there is a Extension method that already does this or not. If there is let me know.
In the ServiceStack.Text ReadMe.md under the URL Extensions:
heading you indicate
string UrlFormat() / AppendPath() / AppendPaths() / WithTrailingSlash()
However remember WithTrailingSlash()
works on file paths or urls without querystring or #
fragments.
If you want, here is an StringExtension
I came up with that you can add to ServiceStack.Text
or wherever you deem appropriate. Feel free to modify it in whole or in part as you see fit. There’s probably a regex way too. Not sure which way is more performant. There is probably something more performant than the below also.
Warning: The below doesn’t accommodate a url for a file. E.g.
domain.com/pic.png
public static string WithUrlTrailingSlash(this string url)
{
if (string.IsNullOrWhiteSpace(url)) return "/";
var qsIdx = url.IndexOf('?');
var hIdx = url.IndexOf('#');
// no qs and no #
if (qsIdx == -1 && hIdx == -1) return url[url.Length - 1] != '/' ? (url + '/') : url;
// qs comes before # fragment RFC 3986 http://tools.ietf.org/html/rfc3986#section-4.1
var idx = qsIdx != -1 ? qsIdx : hIdx;
// if ? or # is the first char
if (idx == 0) return "/" + url;
// check char before idx isn't already a slash, if so return
if (url[idx - 1] == '/') return url;
return url.Insert(idx, "/");
}
Here is the associated NUnit test.
[TestCase("", ExpectedResult = "/")]
[TestCase("/", ExpectedResult = "/")]
[TestCase("?p1=asdf", ExpectedResult = "/?p1=asdf")]
[TestCase("/page", ExpectedResult = "/page/")]
[TestCase("/page/", ExpectedResult = "/page/")]
[TestCase("/page?p1=asdf", ExpectedResult = "/page/?p1=asdf")]
[TestCase("/page?p1=asdf&p2=asdf", ExpectedResult = "/page/?p1=asdf&p2=asdf")]
[TestCase("/page/?p1=asdf&p2=asdf", ExpectedResult = "/page/?p1=asdf&p2=asdf")]
[TestCase("#here", ExpectedResult = "/#here")]
[TestCase("?p1=asdf#here", ExpectedResult = "/?p1=asdf#here")]
[TestCase("/page#here", ExpectedResult = "/page/#here")]
[TestCase("/page/#here", ExpectedResult = "/page/#here")]
[TestCase("/page?p1=asdf#here", ExpectedResult = "/page/?p1=asdf#here")]
[TestCase("/page?p1=asdf&p2=asdf#here", ExpectedResult = "/page/?p1=asdf&p2=asdf#here")]
[TestCase("/page/?p1=asdf&p2=asdf#here", ExpectedResult = "/page/?p1=asdf&p2=asdf#here")]
[TestCase("domain.com", ExpectedResult = "domain.com/")]
[TestCase("domain.com/", ExpectedResult = "domain.com/")]
[TestCase("domain.com?p1=asdf", ExpectedResult = "domain.com/?p1=asdf")]
[TestCase("domain.com/page?p1=asdf", ExpectedResult = "domain.com/page/?p1=asdf")]
[TestCase("domain.com/page?p1=asdf&p2=asdf", ExpectedResult = "domain.com/page/?p1=asdf&p2=asdf")]
[TestCase("domain.com/page/?p1=asdf&p2=asdf", ExpectedResult = "domain.com/page/?p1=asdf&p2=asdf")]
[TestCase("domain.com#here", ExpectedResult = "domain.com/#here")]
[TestCase("domain.com/#here", ExpectedResult = "domain.com/#here")]
[TestCase("domain.com?p1=asdf#here", ExpectedResult = "domain.com/?p1=asdf#here")]
[TestCase("domain.com/page?p1=asdf#here", ExpectedResult = "domain.com/page/?p1=asdf#here")]
[TestCase("domain.com/page?p1=asdf&p2=asdf#here", ExpectedResult = "domain.com/page/?p1=asdf&p2=asdf#here")]
[TestCase("domain.com/page/?p1=asdf&p2=asdf#here", ExpectedResult = "domain.com/page/?p1=asdf&p2=asdf#here")]
public string with_url_trailing_slash(string url)
{
return url.WithUrlTrailingSlash();
}