Fluent URL Building

Flurl was born as a modest URL builder. While most of the capabilities described on this site require Flurl.Http, the builder is still available as a stand-alone package if it's all you need.

Here's the builder in action:

using Flurl;

var url = "https://some-api.com"
    .SetQueryParams(new {
        api_key = _config.GetValue<string>("MyApiKey"),
        max_results = 20,
        q = "I'll get encoded!"

// result:
// https://some-api.com/endpoint?api_key=xxx&max_results=20&q=I%27ll%20get%20encoded%21#after-hash

This example (and most on this site) uses a string extension method to implicitly create a Flurl.Url object. This object converts back to a string implicitly, meaning you can pass a Url object to any method that takes a string without explictly invoking ToString(). Combined, these features allow you to manipulate URL strings in a structured manner without ever enlisting the builder object explicitly.

Of course you may still create a Url object explicitly if you prefer:

var url = new Url("http://www.some-api.com").AppendPathSegment(...

All string extension methods are also available on System.Uri.

The first example demonstrated setting query parameters using object notation, where property names are map to parameter names. There are other approaches available:

url.SetQueryParam("name", "value"); // one by one
url.SetQueryParams(dictionary); // any IDictionary<string, object>
url.SetQueryParams(kvPairs); // any IEnumerable<KeyValuePair>
url.SetQueryParams(new[] {(name1, value1), (name2, value2), ...}); // any collection of Tuples
url.SetQueryParams(new[] {
    new { name = "foo", value = 1 }, ...}); // virtually anything resembling name/value pairs

These alternatives are particularly useful when parameter names are variable or not valid C# identifiers.

SetQueryParam(s) overwrites any previously set values of the same name, but you can set multiple values of the same name by passing a collection:

"https://some-api.com".SetQueryParam("x", new[] { 1, 2, 3 }); // https://some-api.com?x=1&x=2&x=3

To add multiple values in multiple steps without overwriting, just use AppendQueryParam(s) instead:

    .AppendQueryParam("x", 1);
    .AppendQueryParam("x", 2);
    .AppendQueryParams("x", new[] { 3, 4 }); // https://some-api.com?x=1&x=2&x=3&x=4

Builder methods and their overloads are highly discoverable, intuitive, and always chainable. A few destructive methods are also included, such as RemoveQueryParam, RemovePathSegment, and ResetToRoot.


In addition to building URLs, Flurl.Url is effective at decomposing an existing one:

var url = new Url("https://user:pass@www.mysite.com:1234/with/path?x=1&y=2#foo");
Assert.Equal("https", url.Scheme);
Assert.Equal("user:pass", url.UserInfo);
Assert.Equal("www.mysite.com", url.Host);
Assert.Equal(1234, url.Port);
Assert.Equal("user:pass@www.mysite.com:1234", url.Authority);
Assert.Equal("https://user:pass@www.mysite.com:1234", url.Root);
Assert.Equal("/with/path", url.Path);
Assert.Equal("x=1&y=2", url.Query);
Assert.Equal("foo", url.Fragment);

In addition, Url.QueryParams is a special collection type that maintains order and allows duplicate names, but is optimized for the typical case of unique names:

var url = new Url("https://www.mysite.com?x=1&y=2&y=3");
Assert.Equal("1", url.QueryParams.FirstOrDefault("x"));
Assert.Equal(new[] { "2", "3" }, url.QueryParams.GetAll("y"));

Although its parsing capabilities are similar to those of of System.Uri, Flurl aims to be more compliant with RFC 3986, and more true to the actual string provided, and therefore differs in the following ways:

  • Uri.Query includes the ? character; Url.Query does not.
  • Uri.Fragment includes the # character; Url.Fragment does not.
  • Uri.AbsolutePath always includes a leading / character; Url.Path includes it only if it's actually present in the original string, e.g. for "http://foo.com", Url.Path is an empty string.
  • Uri.Authority does not include user info (i.e. user:pass@); Url.Authority does.
  • Uri.Port has a default value if not present; Url.Port is nullable and is not defaulted.
  • Uri will make no attempt to parse a relative URL; Url assumes that if the string doesn't start with {scheme}://, then it starts with a path and parses it accordingly.


A Url is effectively a mutable builder object that implicitly converts to a string. If you need an immutable URL, such as a base URL as a member variable of a class, a common pattern is to type it as a String:

public class MyServiceClass
    private readonly string _baseUrl;

    public Task CallServiceAsync(string endpoint, object data) {
        return _baseUrl
            .PostAsync(data); // requires Flurl.Http package

Here the call to AppendPathSegment creates a new Url object. The result is that _baseUrl remains unmodified, and you've added no additional "noise" compared to if you had declared it as a Url.

Another way to get around the mutable nature of Url when needed is to use the Clone() method:

var url2 = url1.Clone().AppendPathSegment("next");

Here you get a new Url object based on another, so you can modify it without changing the original.


Flurl takes care of encoding characters in URLs, but it takes a different approach with path segments than it does with query string values. The assumption is that query string values are highly variable (such as from user input), whereas path segments tend to be more "fixed" and may already be encoded, in which case you don't want to double-encode. Here are the rules Flurl follows:

  • Query string values are fully URL-encoded.
  • For path segments, reserved characters such as / and % are not encoded.
  • For path segments, illegal characters such as spaces are encoded.
  • For path segments, the ? character is encoded, since query strings get special treatment.

In some cases, you might want to set a query parameter that you know to be already encoded. SetQueryParam has optional isEncoded argument:

url.SetQueryParam("x", "I%27m%20already%20encoded", true);

While the official URL encoding for the space character is %20, it's very common to see + used in query parameters. You can tell Url.ToString to do this with its optional encodeSpaceAsPlus argument:

var url = "http://foo.com".SetQueryParam("x", "hi there");
Assert.Equal("http://foo.com?x=hi%20there", url.ToString());
Assert.Equal("http://foo.com?x=hi+there", url.ToString(true));

Utility Methods

Url also contains some handy static methods, such as Combine, which is basically a Path.Combine for URLs, ensuring one and only one separator character between parts:

var url = Url.Combine(
    "/too/", "/many/", "/slashes/",
    "too", "few?",
    "x=1", "y=2");
// result: "http://www.foo.com/too/many/slashes/too/few?x=1&y=2"

Check if a given string is a well-formed absolute URL:

if (!Url.IsValid(input))
    throw new Exception("You entered an invalid URL!");

And to help you avoid some of the notorious quirks associated with the various URL encoding/decoding methods in .NET, Flurl provides some "quirk-free" alternatives:

Url.Encode(string s, bool encodeSpaceAsPlus); // includes reserved characters like / and ?
Url.EncodeIllegalCharacters(string s, bool encodeSpaceAsPlus); // reserved characters aren't touched
Url.Decode(string s, bool interpretPlusAsSpace);

Spot an error or omission? Edit this page.