WithUrlTrailingSlash

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();
        }

I’ve added string.UrlWithTrailingSlash() ext method in this commit. Thx for the unit tests!

This change is available from v5.7.1 that’s now available on MyGet.

1 Like