Easily define and execute web requests with strong request and response types, and a simple fluent interface.
- Simple, fluent interface
- Natively
async, easily parallelizable - Templated routes
- Built-in support for authorization (basic, bearer token, and cookie)
- Built-in support for failure retry
- Dependency injection and unit testing friendly
- Cross platform (.NET Standard 2.0) compatability
Install the Halforbit.ApiClient NuGet package:
Install-Package Halforbit.ApiClient// Create a base requestvarrequest=Request.Default.BaseUrl("https://alfa.bravo");// GET some Users from a JSON arrayvarresponse=(awaitrequest.GetAsync("users")).Content<IReadOnlyList<User>>();// POST a new personvarresponse=awaitrequest.Body(new{Name="John Doe",Job="Farmer"}).PostAsync("users");// GET an imagevarresponse=awaitrequest.GetAsync("charlie/delta.jpg");varimageBytes=response.ByteArrayContent();// image datavarimageType=response.ContentType.MediaType;// e.g. `image/jpeg`We provide a chained, fluent interface to construct Request objects. Each instance of Request is immutable and can be safely reused and built upon. You can create partial requests and compose them together in a way that is thread-safe:
// Here both requests will have the base URL and header from baseRequest.varbaseRequest=Request.Default.BaseUrl("https://alfa.bravo").Header("x-alfa","bravo");varresponseA=awaitbaseRequest.GetAsync("people");varresponseB=awaitbaseRequest.FormBody(("first_name","John"),("last_name","Doe")).PostAsync("people");Request routes often have route values to be filled in. Optionally, you can specify named route values with placeholders in the resource path, as well as in the base URL:
varrequest=Request.Create("https://alfa.bravo/{AccountId}");varresponse=request.RouteValues(new{AccountId=1234,Category="vehicles",VehicleId=2345}).GetAsync("categories/{Category}/images/{VehicleId}");You can also just use literal string values, or use string interpolation:
varrequest=Request.Default.BaseUrl($"https://alfa.bravo/{accountId}");Route values, query string values, and headers can be specified using a single value, tuple values, or a dictionary of values:
// This will have a query string of: // ?alfa=bravo&charlie=delta&echo=foxtrot&golf=hotelrequest.QueryValue("alfa","bravo").QueryValues(("charlie","delta"),("echo","foxtrot")).QueryValues(newDictionary<string,string>{["golf"]="hotel"})Several methods are provided to easily specify request bodies of various kinds:
// Plain text body.request.TextBody("hello, world!");// Form body. This can be tuple values or a dictionary of values.request.FormBody(("first_name","John"),("last_name","Doe"));// Object body. This can be any serializable object, including classes, // anonymous objects, JObject, etc. The default object serialization // technique is JSON.request.Body(new{Name="John Doe"});// Byte array body.request.Body(newbyte[]{1,2,3});// Stream body. Be sure to close / dispose of your stream properly.using(varstream=File.OpenRead("body.txt")){awaitrequest.Body(stream,contentType:"text/plain; charset: utf-8").PostAsync();}varplainText=response.TextContent();varbytes=response.ByteContent();vardeserializedAsClass=response.Content<Person>();vardeserializedAsJToken=response.Content<JToken>();// Map a JToken to a type.varmappedJToken=response.MapContent(c =>newPerson(c["name"]));// Map an array of JTokens to an IReadOnlyList<> of your favorite type.varmappedJArray=response.MapContentArray(e =>newPerson(e["name"]));You can opt in to automatic retry by specifying the maximum number of times a transient failure should be retried:
// Default is 5 retriesrequest.Retry();// Specify a retry countrequest.Retry(retryCount:10);The first retry will be immediate, and the interval between subsequent retries is exponential, e.g. 1 sec, 2 sec, 4 sec, 8 sec, etc.
Behind the scenes, requests use an IRequestClient to execute and retrieve a response. When you create a request, you can optionally specify an IRequestClient:
// Use the IRequestClient instance we're providingvarrequest=Request.Default.RequestClient(requestClient).BaseUrl("https://alfa.bravo");If you do not provide a request client, a static instance, RequestClient.Instance, will be used automatically:
// Use RequestClient.Instance automaticallyvarrequest=Request.Default.BaseUrl("https://alfa.bravo");If you wish to use constructor dependency injection and unit testing, you should register a singleton instance of RequestClient:
// Using Microsoft.Exensions.DependencyInjection:services.AddSingleton<IRequestClient,RequestClient>();// Using Autofac:builder.RegisterType<RequestClient>().AsImplementedInterfaces().InstancePerLifetimeScope();You can then receive an IRequestClient in your constructors and use it when creating requests:
classMyClient{readonlyRequest_request;publicMyClient(IRequestClientrequestClient){_request=Request.Default.RequestClient(requestClient).BaseUrl("https://alfa.bravo");}publicasyncTask<string>GetAThing(){returnawait_request.GetAsync("things/123").TextContent();}}Here we make a base request and store it in a private field for use by member methods.
IRequestClient contains only one method, Execute(Request), to mock for a unit test. You can use the mocking framework of your choice to simulate responses from this method and verify the correctness of calls.
Several authorization strategies are supported.
request.BasicAuthorization(username:"probably_dont",password:"hardcode_this");You can specify a lambda for retrieving a bearer token. This lambda should produce an IAuthorizationToken:
request.BearerTokenAuthorization(async()=>await_myAuthorizationClient.Authorize());After the bearer token is retrieved, it is cached for subsequent requests to use. If the token expires, or a request returns 401 Unauthorized, a new bearer token will be retrieved, and the request will be repeated.
How you get a bearer token will vary, but here is an example of how you might do so:
publicclassMyAuthorizationClient{readonlyRequest_request;publicMyAuthorizationClient(IRequestClientrequestClient){_request=Request.Default.RequestClient(requestClient).BaseUrl("https://alfa.bravo");}publicasyncTask<IAuthorizationToken>Authorize(){return(await_request.FormBody(("username","probably_dont"),("password","hardcode_this")).PostAsync("token")).MapContent(c =>newAuthorizationToken(content:(string)c["access_token"],expireTime:DateTime.UtcNow.AddSeconds((int)c["expires_in"])));}}If the service you are authorizing against includes the base URL authorized requests should use in its response, you can use .BearerTokenAuthorizationWithBaseUrl():
request.BearerTokenAuthorizationWithBaseUrl(async()=>{varauthResponse=await_myAuthorizationClient.Authorize();return(authResponse.BearerToken,authResponse.BaseUrl);});Cookie authorization is similar to bearer token authorization. Just provide a lambda to retrieve the cookie when it is needed:
request.CookieAuthorization(async()=>await_myAuthorizationClient.Authorize());Some features that are planned for implementation:
Content-Encoding, gzip/deflate support for compressed requests and responses.- Support multipart requests.
- More robust support for e.g.
Accept,Accept-Charset,Range,206 Partial Content,X-Content-Type-Options. - Follow redirects, allow distinction of requested vs redirected url
The following NuGet package is provided:
Data Stores is licensed under the MIT License. See the LICENSE file for details.