Flurl is a modern URL builder.Building Flurl.Url
- Implicitly create Flurl.Url objects using string extension methods
using Flurl;var url = "https://some-api.com" .AppendPathSegment("endpoint") .SetQueryParams(new { api_key = _config.GetValue<string>("MyApiKey"), max_results = 20, q = "I'll get encoded!" }) .SetFragment("after-hash");// result:// https://some-api.com/endpoint?api_key=xxx&max_results=20&q=I%27ll%20get%20encoded%21#after-hash
2. Create a Url object explicitly; all string extension methods can also be used with System.Uri
var url = new Url("http://www.some-api.com").AppendPathSegment(...
3. Set query parameters; you can use object notation as in example 1 or other methods
url.SetQueryParam("name", "value"); // one by oneurl.SetQueryParams(dictionary); // any IDictionary<string, object>url.SetQueryParams(kvPairs); // any IEnumerable<KeyValuePair>url.SetQueryParams(new[] {(name1, value1), (name2, value2), ...}); // any collection of Tuplesurl.SetQueryParams(new[] { new { name = "foo", value = 1 }, ...}); // virtually anything resembling name/value pairs
SetQueryParam(s) overrides any previously set values with the same name, and you can also set multiple values with 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 several steps without overriding, just use AppendQueryParam(s)
"https://some-api.com" .AppendQueryParam("x", 1); .AppendQueryParam("x", 2); .AppendQueryParams("x", new[] { 3, 4 }); // https://some-api.com?x=1&x=2&x=3&x=4
Parsing Flurl.Url
var url = new Url("https://user:[email protected]: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:[email protected]:1234", url.Authority);Assert.Equal("https://user:[email protected]:1234", url.Root);Assert.Equal("/with/path", url.Path);Assert.Equal("x=1&y=2", url.Query);Assert.Equal("foo", url.Fragment);
Url.QueryParams is a special collection type that maintains order and allows duplicate names, but is optimized for typical cases 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"));
Mutability of Flurl.UrlUrl is actually 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 create a new Url object by calling AppendPathSegment
public class MyServiceClass{ private readonly string _baseUrl; public Task CallServiceAsync(string endpoint, object data) { return _baseUrl .AppendPathSegment(endpoint) .PostAsync(data); // requires Flurl.Http package }}
Another method, when you need to bypass Url, is to use the Clone() method to get a new Url object
var url2 = url1.Clone().AppendPathSegment("next");
Encoding in Flurl.UrlFlurl is responsible for encoding characters in the URL, but it handles path segments differently than query string values.The following are the rules Flurl follows:
- Query string values are fully URL-encoded.
- For path segments,reserved characters like
<span>/</span>and<span>%</span>are not encoded. - For path segments,illegal characters (like spaces) are encoded.
- For path segments,
<span>?</span>characters are encoded because query strings are treated specially
In some cases, you may want to set known encoded query parameters. SetQueryParam has an optional isEncoded parameter:
url.SetQueryParam("x", "I%27m%20already%20encoded", true);
And the official URL encoding for space characters is %20, and the often seen + is used in query parameters, which can be implemented through the optional parameter encodeSpaceAsPlus in Url.ToString
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));
Other Utility Methods of Flurl.UrlUrl also includes some convenient static methods, such as Combine, ensuring only one separator between parts
var url = Url.Combine( "http://foo.com/", "/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 properly formatted absolute URL
if (!Url.IsValid(input)) throw new Exception("You entered an invalid URL!");
Also provides various URL encoding/decoding methods that adhere to good programming practices
Url.Encode(string s, bool encodeSpaceAsPlus); // includes reserved characters like / and ?Url.EncodeIllegalCharacters(string s, bool encodeSpaceAsPlus); // reserved characters aren't touchedUrl.Decode(string s, bool interpretPlusAsSpace);
Basic Usage of Flurl.Http
using Flurl;using Flurl.Http;var result = await "https://some-api.com" .AppendPathSegment("endpoint") .GetStringAsync();
Get results other than strings
T poco = await "http://api.foo.com".GetJsonAsync<T>();byte[] bytes = await "http://site.com/image.jpg".GetBytesAsync();Stream stream = await "http://site.com/music.mp3".GetStreamAsync();
Supports all other common verbs
var result = await "http://api.foo.com".PostJsonAsync(requestObj).ReceiveJson<T>();var resultStr = await "http://api.foo.com/1".PatchJsonAsync(requestObj).ReceiveString();var resultStr2 = await "http://api.foo.com/2".PutStringAsync("hello").ReceiveString();var resp = await "http://api.foo.com".OptionsAsync();await "http://api.foo.com".HeadAsync();
Response and Error Handling in Flurl.Http
try { var result = await url.PostJsonAsync(requestObj).ReceiveJson<T>();}catch (FlurlHttpException ex) { var err = await ex.GetResponseJsonAsync<TError>(); // or GetResponseStringAsync(), etc. logger.Write($"Error returned from {ex.Call.Request.Url}: {err.SomeDetails}");}
If you want to check the response status independently of error handling, or check other response properties, you can first return an IFlurlResponse
var resp1 = await "http://api.foo.com".GetAsync();var resp2 = await "http://api.foo.com".PostJsonAsync(requestObj);
Then read other information before consuming the response data
int status = resp.StatusCode;string headerVal = resp.Headers.FirstOrDefault("my-header");T body = await resp.GetJsonAsync<T>();
You can disable the exception throwing behavior for specific statuses or all statuses, where x,X, and * are valid wildcards, 2xx or 3xx status codes are considered successful by default and never throw exceptions.
var resp2 = await "http://api.foo.com".AllowHttpStatus(400, 401).GetAsync();var resp3 = await "http://api.foo.com".AllowHttpStatus("400-403,5xx").GetAsync();var resp1 = await "http://api.foo.com".AllowAnyHttpStatus().GetAsync();
Simulating a BrowserSimulate HTML form post
await "http://site.com/login".PostUrlEncodedAsync(new { user = "user", pass = "pass"});
Multipart form posting (usually related to file uploads)
var resp = await "http://api.com".PostMultipartAsync(mp => mp .AddString("name", "hello!") // individual string .AddStringParts(new {a = 1, b = 2}) // multiple strings .AddFile("file1", path1) // local file path .AddFile("file2", stream, "foo.txt") // file stream .AddJson("json", new { foo = "x" }) // json .AddUrlEncoded("urlEnc", new { bar = "y" }) // URL-encoded .Add(content)); // any HttpContent
Download files
// filename is optional here; it will default to the remote file namevar path = await "http://files.foo.com/image.jpg" .DownloadFileAsync("c:\downloads", filename);
Cookie ManagementSend requests with some cookies
var resp = await "https://cookies.com" .WithCookie("name", "value") .WithCookies(new { cookie1 = "foo", cookie2 = "bar" }) .GetAsync();
More commonly, get response cookies from the first request and let Flurl determine when to send them back
await "https://cookies.com/login".WithCookies(out var jar).PostUrlEncodedAsync(credentials);await "https://cookies.com/a".WithCookies(jar).GetAsync();await "https://cookies.com/b".WithCookies(jar).GetAsync();
You can avoid all WithCookies calls by using CookieSession, which allows you to manage cookies more easily
using var session = new CookieSession("https://cookies.com");// set any initial cookies on session.Cookiesawait session.Request("a").GetAsync();await session.Request("b").GetAsync();// read cookies at any point using session.Cookies
In the above example, jar and session.Cookies are instances of CookieJar, equivalent to Flurl’s CookieContainer, but with the advantage: it is not bound to HttpMessageHandler, so you can simulate multiple cookie sessions on a single HttpClient/Handler instance.
// string-based persistence:var saved = jar.ToString();var jar2 = CookieJar.LoadFromString(saved);// file-based persistence:using var writer = new StreamWriter("path/to/file");jar.WriteTo(writer);using var reader = new StreamReader("path/to/file");var jar2 = CookieJar.LoadFrom(reader);
Other Use Cases of Flurl.HttpSet request headers
// one:await url.WithHeader("Accept", "text/plain").GetJsonAsync();// multiple:await url.WithHeaders(new { Accept = "text/plain", User_Agent = "Flurl" }).GetJsonAsync();
Use Basic Authentication
await url.WithBasicAuth("username", "password").GetJsonAsync();
Use OAuth 2.0 tokens
await url.WithOAuthBearerToken("mytoken").GetJsonAsync();
Specify timeouts
await url.WithTimeout(10).GetAsync(); // 10 secondsawait url.WithTimeout(TimeSpan.FromMinutes(2)).GetAsync();
Handle timeout errors
try { var result = await url.GetStringAsync();}catch (FlurlHttpTimeoutException) { // handle timeouts}catch (FlurlHttpException) { // handle error responses}
Cancel requests
var cts = new CancellationTokenSource();var task = url.GetAsync(cts.Token);...cts.Cancel();
Clientless Usage
var result = await "https://some-api.com".GetJsonAsync<T>();
Flurl’s “clientless” mode does not mean there is no client; it just means you do not need to handle it explicitly, you can trust it is managed cleverly. For this, Flurl uses caching and reuses client instances FlurlHttp.Clients.This is a global singleton instance IFlurlClientCache, which can be pre-configured at startup
FlurlHttp.ConfigureClientForUrl("https://some-api.com") .WithSettings(settings => ...) .WithHeaders(...)
Explicit Client Usage
var client = new FlurlClient("https://some-api.com") // a base URL is optional .WithSettings(settings => ...) .WithHeaders(...);var result = await client.Request("path").GetJsonAsync<T>();
Using Dependency InjectionWhen we do not want to instantiate a client from a service class, the recommended approach is to register a singleton of IFlurlClientCache and inject it into the service. IFlurlClientCache supports named clients, which is very similar to using named clients with IHttpClientFactory
// at service registration:services.AddSingleton<IFlurlClientCache>(sp => new FlurlClientCache() .Add("MyCli", "https://some-api.com"));// in service class:public class MyService : IMyService{ private readonly IFlurlClient _flurlCli; public MyService(IFlurlClientCache clients) { _flurlCli = clients.Get("MyCli"); }}
In the above example, clients.Get will throw an exception if the named client has not been created yet. Clients can be pre-created at startup
_flurlCli = clients.GetOrAdd("MyCli", "https://some-api.com");
Both Add and GetOrAdd support an optional third parameter – Action<IFlurlClientBuilder>, to configure the client
.Add("MyCli", "https://some-api.com", builder => builder .WithSettings(settings => ...) .WithHeaders(...)
Configuration of Flurl.HttpFlurl can mainly be configured through the Settings property of IFlurlClient, IFlurlRequest, IFlurlClientBuilder, and HttpTest.Directly setting
// set default on the client:var client = new FlurlClient("https://some-api.com");client.Settings.Timeout = TimeSpan.FromSeconds(600);client.Settings.Redirects.Enabled = false;// override on the request:var request = client.Request("endpoint");request.Settings.Timeout = TimeSpan.FromSeconds(1200);request.Settings.Redirects.Enabled = true;
Fluent configuration
clientOrRequest.WithSettings(settings => { settings.Timeout = TimeSpan.FromSeconds(600); settings.AllowedHttpStatusRange = "*"; settings.Redirects.Enabled = false;})...
clientOrRequest .WithTimeout(600) .AllowAnyHttpStatus() .WithAutoRedirect(false) ...
Configuration only for the current request
await client .WithSettings(...) // configures the client, affects all subsequent requests .Request("endpoint") // creates and returns a request .WithSettings(...) // configures just this request .GetAsync();
var result = await "https://some-api.com/endpoint" .WithSettings(...) // configures just this request .WithTimeout(...) // ditto .GetJsonAsync<T>();
All the above settings and extension methods are also available on IFlurlClientBuilder
// clientless pattern, all clients:FlurlHttp.Clients.WithDefaults(builder => builder.WithSettings(...));// clientless pattern, for a specific site/service:FlurlHttp.ConfigureClientForUrl("https://some-api.com") .WithSettings(...);// DI pattern:services.AddSingleton<IFlurlClientCache>(_ => new FlurlClientCache() // all clients: .WithDefaults(builder => builder.WithSettings(...)) // specific named client: .Add("MyClient", "https://some-api.com", builder => builder.WithSettings(...))
Serialization in Flurl.Http1. Versions 3.x and earlier are based on Newtonsoft, version 4.0 replaces it withSystem.Text.Json2. You can use the default implementation or customize JsonSerializerOptions
clientOrRequest.Settings.JsonSerializer = new DefaultJsonSerializer(new JsonSerializerOptions { PropertyNameCaseInsensitive = true, IgnoreReadOnlyProperties = true});
Event Handling in Flurl.HttpSeparating cross-cutting concerns (like logging and error handling) from the normal logic flow allows for cleaner code.Flurl.Http defines four events: BeforeCall, AfterCall, OnError, and OnRedirect, and there is an EventHandlers property in IFlurlClient, IFlurlRequest, and IFlurlClientBuilder.
clientOrRequest .BeforeCall(call => DoSomething(call)) // attach a synchronous handler .OnError(call => LogErrorAsync(call)) // attach an async handler
The call in the above code example is an instance of FlurlCall, which contains a set of information related to the request and response:
IFlurlRequest RequestHttpRequestMessage HttpRequestMessagestring RequestBodyIFlurlResponse ResponseHttpResponseMessage HttpResponseMessageFlurlRedirect RedirectException Exceptionbool ExceptionHandledDateTime StartedUtcDateTime? EndedUtcTimeSpan? Durationbool Completedbool Succeeded
OnError can occur before AfterCall, allowing developers the opportunity to decide whether to allow exceptions to bubble up
clientOrRequest.OnError(async call => { await LogTheErrorAsync(call.Exception); call.ExceptionHandled = true; // otherwise, the exception will bubble up});
OnRedirect allows precise handling of 3xx responses
clientOrRequest.OnRedirect(call => { if (call.Redirect.Count > 5) { call.Redirect.Follow = false; } else { log.WriteInfo($"redirecting from {call.Request.Url} to {call.Redirect.Url}"); call.Redirect.ChangeVerbToGet = (call.Response.Status == 301); call.Redirect.Follow = true; }});
Event handlers implement IFlurlEventHandler, which defines two methods:
void Handle(FlurlEventType eventType, FlurlCall call);Task HandleAsync(FlurlEventType eventType, FlurlCall call);
Usually, you only need to implement one or the other, so Flurl provides a default implementation: FlurlEventHandler, as follows:
clientOrRequest.EventHandlers.Add((FlurlEventType.BeforeCall, new MyEventHandler()));
EventHandlers are of type Tuple<EventType, IFlurlEventHandler>. Keeping handlers separate from event types means a given handler can be reused for different event types
public interface IFlurlErrorLogger : IFlurlEventHandler { }public class FlurlErrorLogger : FlurlEventHandler, IFlurlErrorLogger{ private readonly ILogger _logger; public FlurlErrorLogger(ILogger logger) { _logger = logger; }}
// register ILogger:services.AddLogging();// register service that implements IFlurlEventHander and has dependency on ILoggerservices.AddSingleton<IFlurlErrorLogger, FlurlErrorLogger>();// register event handler with Flurl, using IServiceProvider to wire up dependencies:services.AddSingleton<IFlurlClientCache>(sp => new FlurlClientCache() .WithDefaults(builder => builder.EventHandlers.Add((FlurlEventType.OnError, sp.GetService<IFlurlErrorLogger>()))
Message Handling in Flurl.HttpFlurl.Http is built on HttpClient, which uses HttpClientHandler (by default) to do most of the heavy lifting. IFlurlClientBuilder exposes methods for configuring both
// clientless pattern:FlurlHttp.Clients.WithDefaults(builder => builder .ConfigureHttpClient(hc => ...) .ConfigureInnerHandler(hch => { hch.Proxy = new WebProxy("https://my-proxy.com"); hch.UseProxy = true; }));// DI pattern: services.AddSingleton<IFlurlClientCache>(_ => new FlurlClientCache() .WithDefaults(builder => builder. .ConfigureHttpClient(hc => ...) .ConfigureInnerHandler(hch => ...)));
One type of message handler supported by Flurl is DelegatingHandler, a type of linkable handler, commonly referred to as middleware, typically implemented by third-party libraries.
// clientless pattern:FlurlHttp.Clients.WithDefaults(builder => builder .AddMiddleware(new MyDelegatingHandler()));// DI pattern: services.AddSingleton<IFlurlClientCache>(sp => new FlurlClientCache() .WithDefaults(builder => builder .AddMiddleware(sp.GetService<IMyDelegatingHandler>())
The following code example uses Polly, a popular resilience library,which requires installingMicrosoft.Extensions.Http.Polly, and configuring Flurl in a DI scenario:
using Microsoft.Extensions.Http;var policy = Policy .Handle<HttpRequestException>() ...services.AddSingleton<IFlurlClientCache>(_ => new FlurlClientCache() .WithDefaults(builder => builder .AddMiddleware(() => new PolicyHttpMessageHandler(policy))));
Extensibility1. Extending URLsURL extension methods typically contain three overloads: one extending Flurl.Url, one extending System.Uri, and one extending String. All of these should return a modified Flurl.Url object
public static Url DoMyThing(this Url url) { // do something interesting with url return url;}// keep these overloads DRY by constructing a Url and deferring to the above methodpublic static Url DoMyThing(this Uri uri) => new Url(uri).DoMyThing(); public static Url DoMyThing(this string url) => new Url(url).DoMyThing();
2. Extending Flurl.HttpFlurl.Http extension methods can extend Flurl.Url, System.Uri, String, and IFlurlRequest. All should return the current IFlurlRequest allowing further chaining.
public static IFlurlRequest DoMyThing(this IFlurlRequest req) { // do something interesting with req.Settings, req.Headers, req.Url, etc. return req;}// keep these overloads DRY by constructing a Url and deferring to the above methodpublic static IFlurlRequest DoMyThing(this Url url) => new FlurlRequest(url).DoMyThing();public static IFlurlRequest DoMyThing(this Uri uri) => new FlurlRequest(uri).DoMyThing();public static IFlurlRequest DoMyThing(this string url) => new FlurlRequest(url).DoMyThing();
Thus achieving the following effect:
result = await "http://api.com" .DoMyThing() // string extension .GetAsync();result = "http://api.com" .AppendPathSegment("endpoint") .DoMyThing() // Url extension .GetAsync();result = "http://api.com" .AppendPathSegment("endpoint") .WithBasicAuth(u, p) .DoMyThing() // IFlurlRequest extension .GetAsync();
Flurl official website: https://flurl.dev/ (based on version 4.x)