{"id":19586417,"url":"https://github.com/halforbit/api-client","last_synced_at":"2026-02-27T13:39:54.603Z","repository":{"id":38011173,"uuid":"174934912","full_name":"halforbit/api-client","owner":"halforbit","description":"Easily define and execute web requests with strong request and response types, and a simple fluent interface.","archived":false,"fork":false,"pushed_at":"2023-08-29T08:37:00.000Z","size":87,"stargazers_count":2,"open_issues_count":5,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-09-13T12:54:18.762Z","etag":null,"topics":["http-client","web-request"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/halforbit.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-03-11T05:58:07.000Z","updated_at":"2023-08-29T08:38:36.000Z","dependencies_parsed_at":"2023-01-24T04:20:18.248Z","dependency_job_id":null,"html_url":"https://github.com/halforbit/api-client","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halforbit%2Fapi-client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halforbit%2Fapi-client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halforbit%2Fapi-client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/halforbit%2Fapi-client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/halforbit","download_url":"https://codeload.github.com/halforbit/api-client/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224069488,"owners_count":17250456,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http-client","web-request"],"created_at":"2024-11-11T07:59:39.805Z","updated_at":"2026-02-27T13:39:49.565Z","avatar_url":"https://github.com/halforbit.png","language":"C#","readme":"﻿# Halforbit API Client\r\n\r\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) \u0026nbsp;[![Build status](https://ci.appveyor.com/api/projects/status/r96ru6eh9rk9s7n4?svg=true)](https://ci.appveyor.com/project/halforbit/api-client) \u0026nbsp;[![Nuget Package](https://img.shields.io/nuget/v/Halforbit.ApiClient.svg)](#nuget-packages)\r\n\r\nEasily define and execute web requests with strong request and response types, and a simple fluent interface.\r\n\r\n## Features\r\n\r\n- Simple, fluent interface\r\n- Natively `async`, easily parallelizable\r\n- Templated routes\r\n- Built-in support for authorization (basic, bearer token, and cookie)\r\n- Built-in support for failure retry\r\n- Dependency injection and unit testing friendly\r\n- Cross platform (.NET Standard 2.0) compatability\r\n\r\n## Getting Started\r\n\r\nInstall the `Halforbit.ApiClient` NuGet package:\r\n\r\n```powershell\r\nInstall-Package Halforbit.ApiClient\r\n```\r\n   \r\n## Simple Examples\r\n\r\n```csharp\r\n// Create a base request\r\nvar request = Request.Default.BaseUrl(\"https://alfa.bravo\");\r\n\r\n// GET some Users from a JSON array\r\nvar response = (await request.GetAsync(\"users\")).Content\u003cIReadOnlyList\u003cUser\u003e\u003e();\r\n\r\n// POST a new person\r\nvar response = await request\r\n    .Body(new\r\n    {\r\n        Name = \"John Doe\",\r\n        Job = \"Farmer\"\r\n    })\r\n    .PostAsync(\"users\");\r\n\r\n// GET an image\r\nvar response = await request.GetAsync(\"charlie/delta.jpg\");\r\nvar imageBytes = response.ByteArrayContent(); // image data\r\nvar imageType = response.ContentType.MediaType; // e.g. `image/jpeg`\r\n```\r\n\r\n## Creating Requests\r\n\r\nWe 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:\r\n\r\n```csharp\r\n// Here both requests will have the base URL and header from baseRequest.\r\n\r\nvar baseRequest = Request.Default\r\n    .BaseUrl(\"https://alfa.bravo\")\r\n    .Header(\"x-alfa\", \"bravo\");\r\n\r\nvar responseA = await baseRequest.GetAsync(\"people\");\r\n\r\nvar responseB = await baseRequest\r\n    .FormBody((\"first_name\", \"John\"), (\"last_name\", \"Doe\"))\r\n    .PostAsync(\"people\");\r\n```\r\n\r\n### Route Templating\r\n\r\nRequest 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:\r\n\r\n```csharp\r\nvar request = Request.Create(\"https://alfa.bravo/{AccountId}\");\r\n\r\nvar response = request\r\n    .RouteValues(new\r\n    {\r\n        AccountId = 1234,\r\n        Category = \"vehicles\",\r\n        VehicleId = 2345\r\n    })\r\n    .GetAsync(\"categories/{Category}/images/{VehicleId}\");\r\n```\r\n\r\nYou can also just use literal string values, or use string interpolation:\r\n\r\n```csharp\r\nvar request = Request.Default.BaseUrl($\"https://alfa.bravo/{accountId}\");\r\n```\r\n\r\n### Route Values, Query Values, and Headers\r\n\r\nRoute values, query string values, and headers can be specified using a single value, tuple values, or a dictionary of values:\r\n\r\n```csharp\r\n// This will have a query string of: \r\n//   ?alfa=bravo\u0026charlie=delta\u0026echo=foxtrot\u0026golf=hotel\r\nrequest\r\n    .QueryValue(\"alfa\", \"bravo\")\r\n    .QueryValues((\"charlie\", \"delta\"), (\"echo\", \"foxtrot\"))\r\n    .QueryValues(new Dictionary\u003cstring, string\u003e { [\"golf\"] = \"hotel\" })\r\n```\r\n\r\n### Request Bodies\r\n\r\nSeveral methods are provided to easily specify request bodies of various kinds:\r\n\r\n```csharp\r\n// Plain text body.\r\nrequest.TextBody(\"hello, world!\");\r\n\r\n// Form body. This can be tuple values or a dictionary of values.\r\nrequest.FormBody((\"first_name\", \"John\"), (\"last_name\", \"Doe\"));\r\n\r\n// Object body. This can be any serializable object, including classes, \r\n// anonymous objects, JObject, etc. The default object serialization \r\n// technique is JSON.\r\nrequest.Body(new { Name = \"John Doe\" });\r\n\r\n// Byte array body.\r\nrequest.Body(new byte[] { 1, 2, 3 });\r\n\r\n// Stream body. Be sure to close / dispose of your stream properly.\r\nusing(var stream = File.OpenRead(\"body.txt\"))\r\n{\r\n    await request\r\n        .Body(stream, contentType: \"text/plain; charset: utf-8\")\r\n        .PostAsync();\r\n}\r\n```\r\n\r\n## Handling Responses\r\n\r\n```csharp\r\nvar plainText = response.TextContent();\r\n\r\nvar bytes = response.ByteContent();\r\n\r\nvar deserializedAsClass = response.Content\u003cPerson\u003e();\r\n\r\nvar deserializedAsJToken = response.Content\u003cJToken\u003e();\r\n\r\n// Map a JToken to a type.\r\nvar mappedJToken = response.MapContent(c =\u003e new Person(c[\"name\"]));\r\n\r\n// Map an array of JTokens to an IReadOnlyList\u003c\u003e of your favorite type.\r\nvar mappedJArray = response.MapContentArray(e =\u003e new Person(e[\"name\"]));\r\n```\r\n\r\n## Automatic Retry\r\n\r\nYou can opt in to automatic retry by specifying the maximum number of times a transient failure should be retried:\r\n\r\n```csharp\r\n// Default is 5 retries\r\nrequest.Retry();\r\n\r\n// Specify a retry count\r\nrequest.Retry(retryCount: 10);\r\n```\r\n\r\nThe first retry will be immediate, and the interval between subsequent retries is exponential, e.g. 1 sec, 2 sec, 4 sec, 8 sec, etc.\r\n\r\n## Dependency Injection and Testing\r\n\r\nBehind the scenes, requests use an `IRequestClient` to execute and retrieve a response. When you create a request, you can optionally specify an `IRequestClient`:\r\n\r\n```csharp\r\n// Use the IRequestClient instance we're providing\r\nvar request = Request.Default\r\n    .RequestClient(requestClient)\r\n    .BaseUrl(\"https://alfa.bravo\");\r\n```\r\n\r\nIf you do not provide a request client, a static instance, `RequestClient.Instance`, will be used automatically:\r\n\r\n```csharp\r\n// Use RequestClient.Instance automatically\r\nvar request = Request.Default.BaseUrl(\"https://alfa.bravo\");\r\n```\r\n\r\n### Dependency Injection\r\n\r\nIf you wish to use constructor dependency injection and unit testing, you should register a singleton instance of `RequestClient`:\r\n\r\n```csharp\r\n// Using Microsoft.Exensions.DependencyInjection:\r\nservices.AddSingleton\u003cIRequestClient, RequestClient\u003e();\r\n\r\n// Using Autofac:\r\nbuilder.RegisterType\u003cRequestClient\u003e().AsImplementedInterfaces().InstancePerLifetimeScope();\r\n```\r\n\r\nYou can then receive an `IRequestClient` in your constructors and use it when creating requests:\r\n\r\n```csharp\r\nclass MyClient\r\n{\r\n    readonly Request _request;\r\n\r\n    public MyClient(IRequestClient requestClient)\r\n    {\r\n        _request = Request.Default\r\n            .RequestClient(requestClient)\r\n            .BaseUrl(\"https://alfa.bravo\");\r\n    }\r\n\r\n    public async Task\u003cstring\u003e GetAThing()\r\n    {\r\n        return await _request.GetAsync(\"things/123\").TextContent();\r\n    }\r\n}\r\n```\r\nHere we make a base request and store it in a private field for use by member methods.\r\n\r\n### Unit Testing\r\n\r\n`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.\r\n\r\n## Authorization\r\n\r\nSeveral authorization strategies are supported.\r\n\r\n### Basic Authorization\r\n\r\n```csharp\r\nrequest.BasicAuthorization(\r\n    username: \"probably_dont\",\r\n    password: \"hardcode_this\");\r\n```\r\n\r\n### Bearer Token Authorization\r\n\r\nYou can specify a lambda for retrieving a bearer token. This lambda should produce an `IAuthorizationToken`:\r\n\r\n```csharp\r\nrequest.BearerTokenAuthorization(\r\n    async () =\u003e await _myAuthorizationClient.Authorize());\r\n```\r\n\r\nAfter 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.\r\n\r\nHow you get a bearer token will vary, but here is an example of how you might do so:\r\n\r\n```csharp\r\npublic class MyAuthorizationClient\r\n{\r\n    readonly Request _request;\r\n\r\n    public MyAuthorizationClient(IRequestClient requestClient)\r\n    {\r\n        _request = Request.Default\r\n            .RequestClient(requestClient)\r\n            .BaseUrl(\"https://alfa.bravo\");\r\n    }\r\n\r\n    public async Task\u003cIAuthorizationToken\u003e Authorize()\r\n    {\r\n        return (await _request\r\n            .FormBody(\r\n                (\"username\", \"probably_dont\"),\r\n                (\"password\", \"hardcode_this\"))\r\n            .PostAsync(\"token\"))\r\n            .MapContent(c =\u003e new AuthorizationToken(\r\n                content: (string)c[\"access_token\"],\r\n                expireTime: DateTime.UtcNow.AddSeconds((int)c[\"expires_in\"])));\r\n    }\r\n}\r\n```\r\n\r\nIf the service you are authorizing against includes the base URL authorized requests should use in its response, you can use `.BearerTokenAuthorizationWithBaseUrl()`:\r\n\r\n```csharp\r\nrequest.BearerTokenAuthorizationWithBaseUrl(async () =\u003e \r\n{\r\n    var authResponse = await _myAuthorizationClient.Authorize();\r\n\r\n    return (authResponse.BearerToken, authResponse.BaseUrl);\r\n});\r\n```\r\n\r\n### Cookie Authorization\r\n\r\nCookie authorization is similar to bearer token authorization. Just provide a lambda to retrieve the cookie when it is needed:\r\n\r\n```csharp\r\nrequest.CookieAuthorization(\r\n    async () =\u003e await _myAuthorizationClient.Authorize());\r\n```\r\n\r\n## Roadmap\r\n\r\nSome features that are planned for implementation:\r\n- `Content-Encoding`, gzip/deflate support for compressed requests and responses.\r\n- Support multipart requests.\r\n- More robust support for e.g. `Accept`, `Accept-Charset`, `Range`, `206 Partial Content`, `X-Content-Type-Options`.\r\n- Follow redirects, allow distinction of requested vs redirected url\r\n\r\n\u003ca name=\"nuget-packages\"\u003e\u003c/a\u003e\r\n## NuGet Packages\r\n\r\nThe following NuGet package is provided:\r\n\r\n[`Halforbit.ApiClient`](https://www.nuget.org/packages/Halforbit.ApiClient)\r\n\r\n## License \r\n\r\nData Stores is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\r\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhalforbit%2Fapi-client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhalforbit%2Fapi-client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhalforbit%2Fapi-client/lists"}