{"id":27155504,"url":"https://github.com/masinger/gossip4net","last_synced_at":"2025-04-08T19:41:24.878Z","repository":{"id":60787659,"uuid":"545303354","full_name":"masinger/Gossip4Net","owner":"masinger","description":"Gossip4Net is an extensible http client middleware similar to Spring Feign. It allows developers to easily consume APIs using an interface contract, which gets automatically implemented.","archived":false,"fork":false,"pushed_at":"2022-10-06T15:25:15.000Z","size":121,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"develop","last_synced_at":"2024-04-27T09:36:05.929Z","etag":null,"topics":["csharp","dotnet","feign","http","http-client","rest-api","rest-client"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/masinger.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-10-04T06:04:34.000Z","updated_at":"2022-10-07T06:17:10.000Z","dependencies_parsed_at":"2022-10-04T20:03:39.647Z","dependency_job_id":null,"html_url":"https://github.com/masinger/Gossip4Net","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/masinger%2FGossip4Net","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/masinger%2FGossip4Net/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/masinger%2FGossip4Net/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/masinger%2FGossip4Net/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/masinger","download_url":"https://codeload.github.com/masinger/Gossip4Net/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247913985,"owners_count":21017234,"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":["csharp","dotnet","feign","http","http-client","rest-api","rest-client"],"created_at":"2025-04-08T19:41:24.264Z","updated_at":"2025-04-08T19:41:24.868Z","avatar_url":"https://github.com/masinger.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Gossip4Net\nGossip4Net is an extensible http client middleware similar to Spring Feign. It allows developers to easily consume APIs using an interface contract, which gets automatically implemented.\n\n\u003e **Note**\n\u003e Please read the [Not invented here](#not-invented-here) section, to learn if this is the right library for your use case.\n\n1. [Getting started](#getting-started)\n    1. [Get Gossip4Net](#0-get-gossip4net)\n    1. [Define your API contract and models](#1-define-your-api-contract-and-models)\n    1. [Obtain and configure a HttpGossipBuilder](#2-obtain-and-configure-a-httpgossipbuilder)\n    1. [Let Gossip4Net implement your API interface](#3-let-gossip4net-implement-your-api-interface)\n    1. [Use your API](#4-use-your-api)\n1. [Not invented here](#not-invented-here)\n1. [Feaures](#features)\n    - [Url mapping](#url-mapping)\n    - [Url mapping from code](#url-mapping-from-code)\n    - [Path variables](#path-variables)\n    - [Query variables](#query-variables)\n    - [Static query values](#static-query-values)\n    - [Header variables](#header-variables)\n    - [Static header values](#static-header-values)\n    - [Raw http response](#raw-http-response)\n    - [Async and synchronous requests](#async-and-synchronous-requests)\n    - [Void methods](#void-methods)\n    - [Request body](#request-body)\n    - [Response body](#response-body)\n    - [Json (de)serialization](#json-deserialization)\n    - [Xml (de)serialization](#xml-deserialization)\n1. [Authentication](#authentication)\n    - [Basic auth](#basic-auth)\n    - [OpenID with client secret](#openid-with-client-secret)\n1. [Extensibility](#extensibility)\n    1. [The internals](#the-internals)\n        1. [Big picture](#big-picture)\n        1. [Registrations](#registrations)\n        1. [Request modifiers](#request-modifiers)\n        1. [Response modifiers](#response-modifiers)\n        1. [Response constructors](#response-constructors)\n    1. [Example (adding XML support) ](#example-adding-xml-support)\n1. [Testing](#testing)\n\n## Getting started\n\n### 0. Get Gossip4Net\nFor standalone usage:\n```ps\nInstall-Package Gossip4Net.Http\n```\n\nFor usage within ASP.NET core:\n```ps\nInstall-Package Gossip4Net.Http.DependencyInjection\n```\n\nFor XML support\n```ps\nInstall-Package Gossip4Net.Http.Xml\n```\n\n### 1. Define your API contract and models\n```csharp\nnamespace MyDemo {\n    public record HttpBinResponseDto(IDictionary\u003cstring, string\u003e Headers);\n    public record PersonRequestDto(string Fistname, string Lastname);\n\n    [HttpApi(\"https://httpbin.org\")]\n    public interface HttpBinApi {\n\n        [GetMapping(\"/get\")]\n        Task\u003cHttpBinResponseDto\u003e GetAsync();\n\n        [GetMapping(\"/get\")]\n        HttpBinResponseDto GetSync();\n\n        [PostMapping(\"/post\")]\n        Task PostAsync(Person person);\n    }\n}\n```\n\n### 2. Obtain and configure a HttpGossipBuilder\n```csharp\nusing  Gossip4Net.Http;\n\nnamespace MyDemo {\n    public class Demo {\n        public async Task Startup() {\n            IHttpGossipBuilder\u003cHttpBinApi\u003e builder = new HttpGossipBuilder\u003cHttpBinApi\u003e();\n            builder.AddDefaultBehavior();\n        }\n    }\n}\n```\n\n\nFor ASP.NET core, dependency injection can be used:\n```csharp\nusing  Microsoft.Extensions.DependencyInjection;\n\nnamespace AspNetDemo {\n\n    public class Startup {\n\n        public void ConfigureServices(IServiceCollection services)\n        {\n            services.AddGossipHttpClient\u003cHttpBinApi\u003e();\n        }\n    }\n}\n```\n\n### 3. Let Gossip4Net implement your API interface\n```csharp\nnamespace MyDemo {\n    public class Demo {\n        public async Task Startup() {\n            IHttpGossipBuilder\u003cHttpBinApi\u003e builder = new HttpGossipBuilder\u003cHttpBinApi\u003e();\n            builder.AddDefaultBehavior();\n\n            HttpBinApi api = builder.Build();\n        }\n    }\n}\n```\n\n### 4. Use your API\n```csharp\nusing  Gossip4Net.Http;\n\nnamespace MyDemo {\n    public class Demo {\n        public async Task Startup() {\n            IHttpGossipBuilder\u003cHttpBinApi\u003e builder = new HttpGossipBuilder\u003cHttpBinApi\u003e();\n            builder.AddDefaultBehavior();\n\n            HttpBinApi api = builder.Build();\n            HttpBinResponseDto response = await api.GetAync();\n        }\n    }\n}\n```\n\n## Not invented here\nThis is (by far) not the first project enabling developers to consume an API by automatically implementing an interface. \nIn fact, there is an awesome library called [Refit](https://github.com/reactiveui/refit), doing just that.\n\n\u003e **Note**\n\u003e For the sake of completeness, it has to be mentioned that there is also [DHaven.Faux](https://github.com/D-Haven/DHaven.Faux) and [feign.net](https://github.com/daixinkai/feign.net) heading in the same general direction (although I don't have firsthand experience with them).\n\n### Why you should use Refit\n[Refit](https://github.com/reactiveui/refit) is a widespread, stable and fast library, providing similar functionality. If you only intend to use \"standard\" features, it might well be the better choice for your use case - so go check it out!\n\n### Why you should use Gossip4Net\nThe \"issue\" with auto-generating libraries is (IMHO), that you are somewhat limited to the functionalities intended by the project's maintainers.\nMost of the time they will do just fine, but you are probably going to run into trouble the first you need to do something \"special\". \n\nWhat do you do now? Getting rid of the library and implementing everything manually (even the \"standard\" cases)?\n\nIn order to avoid having to ask these questions, Gossip4Net is focused on [extensibility](#extensibility). Even though this library is also providing some [standard features](#features), all of them are added dynamically and in the same way new behaviors could be added by any depending project.\n\n## Features\n\n### Url mapping\nThe API-Url can be specified using the `[HttpApi]` attribute\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi {}\n```\n\nand/or using the method mapping attributes (e.g. `[GetMapping]`, `[PostMapping]`, `[PatchMapping]`):\n\n```csharp\n[HttpApi]\npublic interface MyApi {\n    [GetMapping(\"https://httpbin.org/get\")]\n    Task\u003cHttpResponseMessage\u003e GetResponseAync();\n}\n```\n\nIf both are present and the url specified on the method mapping is relative, it will be combined with the url given in `[HttpApi]`.\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi {\n    [GetMapping(\"/get\")]\n    Task\u003cHttpResponseMessage\u003e GetResponseAync();\n}\n```\n\nThe above example will result in a call to `https://httpbin.org/get`.\n\n*Example path combinations*\n| Base path                 | Extension | Result                            |\n| ---------                 | --------- | ------                            |\n| `https://localhost`       | `/api`    | `https://localhost/api`           |\n| `https://localhost/api`   | `/person` | `https://localhost/api/person`    |\n| `https://localhost/api`   | `person`  | `https://localhost/person`        |\n| `https://localhost/api/`  | `person`  | `https://localhost/api/person`    |\n| `https://localhost/api/`  | `/person` | `https://localhost/person`        | \n\n### Url mapping from code\nYou can also modify/specify urls directly from your code.\nThis can come in handy, if you for example need to use a different API version depending on the current environment (e.g test, staging, prod).\n\nAll following examples will refer to this API definition.\n```csharp\n[HttpApi(\"/get\")]\npublic interface IRelativeHttpBinClient\n{\n    [GetMapping]\n    Task\u003cHttpBinResponse\u003e Get();\n}\n```\n\nTo archive this, you can choose one of the following methods. \n#### By providing a custom http client\nYou can specify a custom `HttpClient` provider by setting the builder's `.ClientProvider` property.\nThis provider could then set an appropriate `BaseAddress`.\n\n```csharp\nIHttpGossipBuilder\u003cIRelativeHttpBinClient\u003e builder = new HttpGossipBuilder\u003cIRelativeHttpBinClient\u003e().AddDefaultBehavior(new JsonSerializerOptions\n{\n    PropertyNamingPolicy = JsonNamingPolicy.CamelCase\n});\nbuilder.ClientProvider = () =\u003e new HttpClient { BaseAddress = new Uri(\"https://httpbin.org\") };\n\nIRelativeHttpBinClient client = builder.Build();\n```\n\n#### By adding a custom behavior \nGossip4Net focuses on extensibility, which enables you to freely add and remove different behaviors. To get and in-depth insight, refer to [Extensibility](#extensibility).\n\nRequest urls are handled by the `RequestUriModifier`, which takes an url and appends it to the currently processed http request.\nBy injecting a custom instance into the request chain, you can modify the url as you like.\n\n*Adding a global request modifier using the helper method* \n```csharp\nIRelativeHttpBinClient client = builder\n    .WithRegistrations(r =\u003e r.With(new RequestUriModifier(\"https://httpbin.org\")))\n    .AddDefaultBehavior(new JsonSerializerOptions\n    {\n        PropertyNamingPolicy = JsonNamingPolicy.CamelCase\n    })\n    .Build();\n```\n\n\u003e **Note**\n\u003e The order of operation (adding the `RequestUriModifier` first, then applying the default behavior) is essential. \nRequest modifiers are executed sequentially in order of their registration. If the `RequestUriModifier` was to be registered last,\nthe url `https://httpbin.org` would be appended to `/get` (set by the `[HttpApi]` attribute).\n\n### Path variables\nParameter values can be interpolated into the request path using the `[PathVariable]` attribute.\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi {\n    [GetMapping(\"/{method}\")]\n    Task\u003cHttpResponseMessage\u003e GetResponseAync([PathVariable] string method);\n\n    [GetMapping(\"/{operation}\")]\n    Task\u003cHttpResponseMessage\u003e GetResponseWithExplicitPathVariableNameAync([PathVariable(\"operation\")] string method);\n}\n```\n\nBy default, the placeholder name is determined by the parameter name (e.g. \"method\").\nIt can also be specified manually.\n\n*Available properties*\n| Property | Description | Default |\n|----------|-------------|---------|\n| `Name` | The path variable name. | The annotated parameter's name. |\n| `EscapePath` | If `false`, special characters (like `/` and `?`) are not escaped. | `true` |\n\n### Query variables\nParameter values can be send as a query parameter using the `[QueryVariable]` attribute.\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi {\n    [GetMapping(\"/get\")]\n    HttpResponseMessage GetWithQuery([QueryVariable] string testParam);\n\n    [GetMapping(\"/get\")]\n    HttpResponseMessage GetWithExplicitlyNamedQuery([QueryVariable(\"testParam\")] string myValue);\n}\n```\nBy default, the query variable name is determined by the parameter name (e.g. \"method\").\nIt can also be specified manually.\n\n*Available properties*\n| Property | Description | Default |\n|----------|-------------|---------|\n| `Name` | The query parameter name. | The annotated parameter's name. |\n| `OmitEmpty` | If `true`, the query parameter will be omitted for given `null` values. | `true` |\n| `EnumerateUsingMultipleParams` | If the parameter type is an `IEnumerable` and this is set to `true`, the query parameter name will be repeated for each entry. | `true` |\n\n### Static query values\nSimilar to `[QueryVariable]`, the `[QueryValue(string name, object? value)]` attribute can be used to send a static header with every request.\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\n[QueryValue(\"json\", \"true\")]\npublic interface MyApi {    \n    [QueryValue(\"max\", 100)]\n    HttpResponseMessage Get();\n}\n```\n\n*Available properties*\n| Property | Description | Default |\n|----------|-------------|---------|\n| `Name` | The query parameter name. | none |\n| `Value` | The value to be sent. | none |\n| `EnumerateUsingMultipleParams`| If the parameter type is an `IEnumerable` and this is set to `true`, the query parameter name will be repeated for each entry. | `true` |\n\n### Header variables\nParameter values can be sent as request headers using the `[HeaderVariable]` attribute.\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi {\n    [DeleteMapping(\"/delete\")]\n    Task\u003cHttpResponseMessage\u003e DeleteAsyncWithHeader([HeaderVariable] string actor);\n\n    [DeleteMapping(\"/delete\")]\n    Task\u003cHttpResponseMessage\u003e DeleteAsyncWithExplicitHeader([HeaderVariable(\"actor\")] string myValue);\n}\n```\nBy default, the header name is determined by the parameter name (e.g. \"method\").\nIt can also be specified manually.\n\n*Available properties*\n| Property | Description | Default |\n|----------|-------------|---------|\n| `Name` | The header name to be used. | The annotated parameter's name. |\n| `OmitEmpty` | If `true`, the header will be omitted for given `null` values. | `true` |\n\n### Static header values\nIn order to always send a specific header, the `[HeaderValue]` attribute can be applied to a method or to the entire interface.\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\n[HeaderValue(\"Interface-Header\", \"interface\")]\npublic interface MyApi {\n    [GetMapping(\"/get\")]\n    [HeaderValue(\"Method-Header\", \"method\")]\n    Task\u003cHttpResponseMessage\u003e GetAyncWithStaticHeader();\n}\n```\n\n### Raw http response\nIf a method's return type is `HttpResponseMessage` or `Task\u003cHttpResponseMessage\u003e` the raw `HttpResponseMessage` will be returned.\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi {\n    [GetMapping(\"/get\")]\n    Task\u003cHttpResponseMessage\u003e GetRawResponseAsnc();\n}\n```\n\n\u003e **Note**: Even though the response body will not be parsed and the entire http response body is getting returned, other response processing (e.g. checking the response status) still applies.\n\n### Async and synchronous requests\nYou can send both asynchronous and synchronous requests.\nIf a request should be performed asynchronously is determined be the method's return type (being `Task\u003c\u003e` or `Task`) only.\nAync methods do not have to end with the `Async`-suffix.\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi\n{\n    [GetMapping(\"/get\")]\n    Task\u003cHttpResponseMessage\u003e GetAsync();\n\n    [GetMapping(\"/get\")]\n    HttpResponseMessage Get();\n}\n```\n\n\n### Void methods\nMethods returning either `void` or `Task`, will not return anything.\nNevertheless, the response will still be received and processed (e.g. checking the response code) by the library.\nA call will be blocked and a returned Task will not complete until a response is received.\n\n```csharp\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi\n{\n    [GetMapping(\"/get\")]\n    Task GetAsync();\n\n    [GetMapping(\"/get\")]\n    void Get();\n}\n```\n\n### Request body\nParameter values without an attribute will be serialized and sent using the request body.\n\n```csharp\npublic class Person {\n    public string? Firstname { get; set; }\n    public string? Lastname { get; set; }\n    public int Age { get; set; }\n}\n\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi\n{\n    [PostMapping(\"/post\")]\n    HttpResponseMessage Post(Person p);\n\n    [PostMapping(\"/post\")]\n    Task\u003cHttpResponseMessage\u003e PostAsync(Person p);\n}\n```\n\nThe serialization format depends on the current configuration and can be customized using the `HttpGossipBuilder`.\nThe default is JSON.\n\n**See also**:\n- [Json (de)serialization](#json-deserialization)\n\n\n### Response body\nGossip4Net will attempt to return an instance of the method's specified return type.\nIt will be deserialized using the response body.\nThe deserialization format depends on the current configuration and the received response headers.\nIt can be customized using the `HttpGossipBuilder`.\n\n```csharp\npublic record HttpBinResponse(IDictionary\u003cstring, string\u003e Headers, string Origin, string Url, IDictionary\u003cstring, string\u003e Args);\n\n[HttpApi(\"https://httpbin.org\")]\npublic interface MyApi\n{\n    [GetMapping(\"/get\")]\n    Task\u003cHttpBinResponse\u003e GetAsync();\n\n    [GetMapping(\"/get\")]\n    HttpBinResponse Get();\n}\n```\n\n### Json (de)serialization\nBy default, JSON is used to serialize request and response bodies.\n\nJSON serialization is added by the `JsonRequestBodyRegistration` and `JsonResponseConstructorRegistration`. \n\n\nYou can configure the JSON serializer using one of the following extension/helper methods.\n\n*Constructing a builder and adding default behavior*\n```csharp\nIHttpGossipBuilder\u003cMyApi\u003e builder = new HttpGossipBuilder\u003cMyApi\u003e()\n    .AddDefaultBehavior(new JsonSerializerOptions()\n    {\n        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n    });\n```\n\n*Creating a builder using a builder helper method*\n```csharp\nIHttpGossipBuilder\u003cMyApi\u003e builder = HttpGossipBuilder\u003cMyApi\u003e.NewDefaultBuilder(\n    new JsonSerializerOptions\n    {\n        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n    });\n```\n\n*While using dependency injection*\n```csharp\nIServiceCollection services = new ServiceCollection();\nservices.AddGossipHttpClient\u003cMyApi\u003e(\n    builder =\u003e builder.AddDefaultBehavior(new JsonSerializerOptions\n    {\n        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n    })\n);\n```\n\n\nIf you don't intend to use the helper/extension methods, you can add JSON support like this:\n```csharp\nIHttpGossipBuilder\u003cMyApi\u003e builder = new HttpGossipBuilder\u003cMyApi\u003e();\nJsonSerializerOptions options = new JsonSerializerOptions\n{\n    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n};\nbuilder.Registrations.RequestAttributes.Add(new JsonRequestBodyRegistration(options));\nbuilder.Registrations.ResponseConstructors.Add(new JsonResponseConstructorRegistration(options));\n```\n\n### XML (de)serialization\nXML support is provided by `Gossip4Net.Http.Xml` which must first be added via NuGet.\n\nIn order to use it, add it to your gossip builder.\n```csharp\nIHttpGossipBuilder\u003cIXmlHttpBinClient\u003e builder = new HttpGossipBuilder\u003cIXmlHttpBinClient\u003e()\n    .AddXmlBehavior()\n    .AddDefaultBehavior();\n```\n\nThere is also an `.AddXmlBehavior(bool applyByDefault)` overload, allowing you to\nenable/disable the automatic XML response deserialization for every API method. If `applyByDefault` is set to false or the parameterless overload is used, methods receiving XML responses need to have the `[XmlResponse]` attribute.\n\nIf you want to provide a customized `XmlSerializer`, you may use the `.AddXmlBehavior(bool applyByDefault, Func\u003cType, XmlSerializer\u003e serializerProvider)` overload.\n\nAll attributes provided by `Gossip4Net.Http.Xml` are documented below.\n\n#### [NoXmlResponse]\nCan be applied to an API method, if the builder should not register a XML deserializer for this method (even if `applyByDefault` has been set to `true`).\n\n#### [XmlResponse]\nNeeds to be applied to API methods expected to receive a XML response body, if `applyByDefault` has not been set to `true`.\n\n#### [XmlBody]\nCan be applied to a method parameter, in order to serialize it as a XML request body.\n\n*Available properties*\n| Property | Description | Default |\n|----------|-------------|---------|\n| `OmitEmpty` | If `true` and the argument value is `null`, no request body will be sent. | `true` |\n\n\n## Authentication\nAuthentication can be implemented using third-party `HttpClient` middleware tools (like the common [IdentityModel project](https://github.com/IdentityModel/IdentityModel) or another library of your choice).\nThe main entry point for configuring authentication is always the `IHttpGossipBuilder\u003c\u003e.ClientProvider` property, which holds a factory method returning a fresh `HttpClient` instance.\n\nRefer to the examples below for suggestions on how certain authentication methods can be configured. If not stated otherwise, [IdentityModel project](https://github.com/IdentityModel/IdentityModel) will be used.\n\n\u003e **Warning**\n\u003e The examples are written to be as concise and minimal as possible. \nCommon and required security measures (e.g. obtaining passwords and secrets from secure sources) are omitted for clearity. \nPlease consult the documentation relevant to your setup and tooling, in order to learn about safe ways to handle secrets. \n\u003e\n\u003eA good starting point might be the following MSDN documentation:\n\u003e-  [Safe storage of app secrets in development in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets)\n\n\n### Basic Auth\n\n*API definition*\n```csharp\n[HttpApi(\"https://httpbin.org\")]\npublic interface IHttpBinStaticBasicAuthApi\n{\n    [GetMapping(\"/basic-auth/myuser/mypassword\")]\n    HttpResponseMessage GetWithBasicAuth();\n}\n```\n\n*Example usage with basic auth*\n```csharp\npublic class BasicAuthTest\n{\n    [Fact]\n    public void HttpClientWithBasicAuthShouldWork()\n    {\n        // Arrange\n        IHttpGossipBuilder\u003cIHttpBinStaticBasicAuthApi\u003e gossipBuilder = HttpGossipBuilder\u003cIHttpBinStaticBasicAuthApi\u003e.NewDefaultBuilder();\n        gossipBuilder.ClientProvider = () =\u003e\n        {\n            HttpClient httpClient = new HttpClient();\n            httpClient.SetBasicAuthentication(\"myuser\", \"mypassword\");\n            return httpClient;\n        };\n        IHttpBinStaticBasicAuthApi api = gossipBuilder.Build();\n\n        // Act\n        HttpResponseMessage response = api.GetWithBasicAuth();\n\n        // Assert\n        response.IsSuccessStatusCode.Should().BeTrue();\n    }\n}\n```\n\n### OpenID with client secret\nThis example uses a client id and secret in order to authenticate against an OIDC identity provider.\nThe token retrival and management is done automatically by IdentityModel. \n\n*API definition*\n```csharp\n[HttpApi(\"https://demo.duendesoftware.com/api/test\")]\npublic interface IOidcApi\n{\n    [GetMapping]\n    HttpResponseMessage Get();\n}\n```\n\n*Example usage with ASP.NET core and IdentityModel.AspNetCore*\n```csharp\npublic class OidcAuthTest\n{\n    [Fact]\n    public void FullExampleShouldWord()\n    {\n        // Arrange\n        ServiceCollection services = new ServiceCollection();\n        services.AddAccessTokenManagement(options =\u003e // configuring the access token managenment\n        {\n            options.Client.Clients.Add(\"demo\", new ClientCredentialsTokenRequest\n            {\n                Address = \"https://demo.duendesoftware.com/connect/token\",\n                ClientId = \"m2m\",\n                ClientSecret = \"secret\"\n            });\n        }).ConfigureBackchannelHttpClient();\n\n        services.AddGossipHttpClient\u003cIOidcApi\u003e()\n            .AddClientAccessTokenHandler(\"demo\"); // binding the access token handler to the http client used by Gossi4Net\n\n        ServiceProvider sp = services.BuildServiceProvider();\n        IOidcApi client = sp.GetRequiredService\u003cIOidcApi\u003e();\n\n        // Act\n        HttpResponseMessage result = client.Get();\n\n        // Assert\n        result.IsSuccessStatusCode.Should().BeTrue();\n    }\n}\n```\n\n## Extensibility\n\n### The internals\n\n#### Big picture\nA request/response-cycle always follows the process outlined below.\n\n```text\nactual paremeters are received\n            |\n            V\n   create a plain request\n            |\n            V\n   apply request modifiers\n            |\n            V\n obtain a HttpClient instance\n            |\n            V\n       send request\n            |\n            V\n  apply response modifiers\n            |\n            V\n  apply response constructor\n  (creates an instance of the \n    specified return type)\n            |\n            V\n     return response\n```\n\nThe entire behavior is therefore determined by the set of registered request and response modifiers.\nIn order to deterine the behaviors to be applied, the `HttpGossipBuilder\u003c\u003e` uses different modifier registrations.\nThese are responsible for registering intended behaviors based on certain criteria (e.g. an attribute is present).\n\n```text\n    plain HttpGossipBuilder\u003c\u003e is createad\n                    |\n                    V\n    registrations are added to the builder\n                    |\n                    V\n        .Build() method is invoked\n                    |\n                    V\n    the builder scans the API interface\n                    |\n                    V\n    registrations are invoked for each\n        present interface member\n                    |\n                    V\n    if applicable, behaviors are returned\n        by the invoked registration\n                    |\n                    V\nHttpGossipBuilder adds the returned behavior\n        to the request/response cycle\n                    |\n                    V\n instance of the API interface is returned\n```\n\n#### Registrations\nAs outlined [above](#big-picture), a registration is responsible for adding specific behaviors based on the API interface to be implemented.\n\nThe interface to be implemented in order to register request modifiers, looks like that:\n\n```csharp\npublic interface IRequestModifierRegistration\n{\n    IList\u003cIHttpRequestModifier\u003e? ForParameter(RequestParameterContext parameterContext, IEnumerable\u003cAttribute\u003e attributes);\n    IList\u003cIHttpRequestModifier\u003e? ForMethod(RequestMethodContext methodContext, IEnumerable\u003cAttribute\u003e attributes);\n    IList\u003cIHttpRequestModifier\u003e? ForType(RequestTypeContext typeContext, IEnumerable\u003cAttribute\u003e attributes);\n}\n```\n\nDuring the \"implementation phase\", the `ForType`, `ForMethod` and `ForParameter` methods are invoked by the builder.\nThis is done for the interface type itself, each declared method and for each of their parameters.\nThe registration's implementation can now decide, if the discovered member (or one of its attributes) justifies new behaviors to be applied. If so, a list of them are returned.\n\nThe `IResponseModiferRegistration` and `IResponseConstructorRegistration` are following the same general principle.\n\nIf a registration only intends to add behaviors based on an attribute of a given type, the `RequestModifierRegistration\u003cTAttribute\u003e` class can be inherited.\n\n#### Request modifiers\nA request modifier is responsible for manipulating a request before it gets send.\nNote that it is only invoked during the request/response cycle following a call to an API interface method.\n\n```csharp\npublic interface IHttpRequestModifier\n{\n    Task\u003cHttpRequestMessage\u003e ApplyAsync(HttpRequestMessage requestMessage, object?[] args);\n}\n```\nIt receivs the request to be modified as well as the actual parameters that have been passed into the interface method.\nAfter the relevant modifications are done, it must return the manipulated request message.\n\n#### Response modifiers\nA response modifier is responsible for manipulating a received response, before they are converted in the API interface method's return type.\n\n```csharp\npublic interface IHttpResponseModifier\n{\n    HttpResponseMessage Modify(HttpResponseMessage response);\n}\n```\n\n### Response constructors\nA response constructor is the last instance to be invoked, before returning the final result to the caller.\nIt is responsible for converting the manipulated http response into an object of the return type declared on the API's method.\n\n```csharp\npublic interface IResponseConstructor\n{\n    public Task\u003cConstructedResponse\u003e ConstructResponseAsync(HttpResponseMessage response);\n}\n```\n\nIf the implementation has been able to process the response, it should return `ConstructuredResponse.Of(...)`. \nIf it has ben unable, it should usually return `ConstructedResponse.Empty` instead of throwing an exception (e.g. the `JsonResponseConstructor` receiving an \"application/xml\" response).\n\nAn exception should only be thrown, if it should have definitely been able to process it (e.g. if the `JsonResponseConstructor` receivs an \"application/json\" response, but the body contains syntax errors).\n\n### Example (adding XML support) \nIn this example we will add support for XML request and response bodies. This feature is actually availble with `Gossip4Net.Http.Xml`, but you could also implement it yourself using the steps outlined below.\n\n#### Creating a project and adding dependencies\nAt first, we need to make sure to install all required dependencies.\nIf you want to separate Gossip4Net extensions from your \"regular\" code, you might as well create a new project within your solution.\n\nIn this this example, we are creating a new C# library project called `Gossip4Net.Http.Xml` and target .NET 6.\n\nNext we need to be sure to install the following NuGet packages:\n- System.Runtime.Serialization.Xml (providing XML serialization)\n- Gossip4Net.Http\n\n#### Creating a custom ResponseConstructor\nThe component that converts a http response into a model type is called \"ResponseConstructor\". \nIn our example, it should check if the response contains XML and then deserialize it into the requested return type.\n\n1. Create a new class and implement the IResponseConstructorInterface\n```csharp\nusing Gossip4Net.Http.Modifier.Response;\n\nnamespace Gossip4Net.Http.Xml.Modifier.Response\n{\n    internal class XmlResponseConstructor : IResponseConstructor\n    {\n        public async Task\u003cConstructedResponse\u003e ConstructResponseAsync(HttpResponseMessage response)\n        {\n            throw new NotImplementedException();\n        }\n    }\n}\n\n```\n2. Next we are going to check if the received response contains XML by checking the content type\n```csharp\nusing Gossip4Net.Http.Modifier.Response;\n\nnamespace Gossip4Net.Http.Xml.Modifier.Response\n{\n    internal class XmlResponseConstructor : IResponseConstructor\n    {\n        private static readonly ISet\u003cstring\u003e SupportedMediaTypes = new HashSet\u003cstring\u003e()\n        {\n            \"application/xml\",\n            \"text/xml\"\n        };\n\n        public async Task\u003cConstructedResponse\u003e ConstructResponseAsync(HttpResponseMessage response)\n        {\n            string? mediaType = response.Content.Headers.ContentType?.MediaType;\n            if (mediaType == null || !SupportedMediaTypes.Contains(mediaType))\n            {\n                \n            }\n        }\n    }\n}\n```\n3. If the response doesn't contain XML, we give up by returning `ConstructedResponse.Empty`.\n```csharp\nif (mediaType == null || !SupportedMediaTypes.Contains(mediaType))\n{\n    return ConstructedResponse.Empty;\n}\n```\n4. If it does, we will use a `XmlSerializer` to deserialize it. As we do not want to create a\n`XmlSerializer` everytime a request is executed, we inject an apropriate serializer using the constructor.\n```csharp\nusing Gossip4Net.Http.Modifier.Response;\nusing System.Xml.Serialization;\n\nnamespace Gossip4Net.Http.Xml.Modifier.Response\n{\n    internal class XmlResponseConstructor : IResponseConstructor\n    {\n        private readonly XmlSerializer xmlSerializer;\n\n        public XmlResponseConstructor(XmlSerializer xmlSerializer)\n        {\n            this.xmlSerializer = xmlSerializer;\n        }\n\n        private static readonly ISet\u003cstring\u003e SupportedMediaTypes = new HashSet\u003cstring\u003e()\n        {\n            \"application/xml\",\n            \"text/xml\"\n        };\n\n        public async Task\u003cConstructedResponse\u003e ConstructResponseAsync(HttpResponseMessage response)\n        {\n            string? mediaType = response.Content.Headers.ContentType?.MediaType;\n            if (mediaType == null || !SupportedMediaTypes.Contains(mediaType))\n            {\n                return ConstructedResponse.Empty;\n            }\n            object? responseObject = xmlSerializer.Deserialize(await response.Content.ReadAsStreamAsync());\n        }\n    }\n}\n\n```\n5. Once we deserialized the object, we can return it by wrapping it into a `ConstructedResponse`.\n```csharp\nreturn new ConstructedResponse(responseObject);\n```\n6. The final implementation should now look like this:\n```csharp\nusing Gossip4Net.Http.Modifier.Response;\nusing System.Xml.Serialization;\n\nnamespace Gossip4Net.Http.Xml.Modifier.Response\n{\n    internal class XmlResponseConstructor : IResponseConstructor\n    {\n        private readonly XmlSerializer xmlSerializer;\n\n        public XmlResponseConstructor(XmlSerializer xmlSerializer)\n        {\n            this.xmlSerializer = xmlSerializer;\n        }\n\n        private static readonly ISet\u003cstring\u003e SupportedMediaTypes = new HashSet\u003cstring\u003e()\n        {\n            \"application/xml\",\n            \"text/xml\"\n        };\n\n        public async Task\u003cConstructedResponse\u003e ConstructResponseAsync(HttpResponseMessage response)\n        {\n            string? mediaType = response.Content.Headers.ContentType?.MediaType;\n            if (mediaType == null || !SupportedMediaTypes.Contains(mediaType))\n            {\n                return ConstructedResponse.Empty;\n            }\n            object? responseObject = xmlSerializer.Deserialize(await response.Content.ReadAsStreamAsync());\n            return new ConstructedResponse(responseObject);\n        }\n    }\n}\n```\n\n#### Creating a response constructor registration\n1. In order to use our new `XmlResponseConstructor`, we need to be able to inject it into the request/response process. To do so, we create an implementation of `IResponseConstructorRegistration`.\n```csharp\nusing Gossip4Net.Http.Builder.Response;\nusing Gossip4Net.Http.Modifier.Response;\nusing Gossip4Net.Http.Modifier.Response.Registration;\n\nnamespace Gossip4Net.Http.Xml.Modifier.Response.Registration\n{\n    public class XmlResponseConstructorRegistration : IResponseConstructorRegistration\n    {\n        public IList\u003cIResponseConstructor\u003e? ForMethod(ResponseMethodContext responseMethod)\n        {\n            throw new NotImplementedException();\n        }\n    }\n}\n```\n2. As methods returning `void` or `Task` don't need to process the response body at all, we don't want to return a response constructor in these cases.\nInstead we can simply return `null` (or an empty list).\n```csharp\npublic IList\u003cIResponseConstructor\u003e? ForMethod(ResponseMethodContext responseMethod)\n{\n    if (responseMethod.IsVoid)\n    {\n        return null;\n    }\n}\n```\n3. Otherwise we create a new `XmlSerializer` instance, capable of serializing the returned type.\n```csharp\nXmlSerializer xmlSerializer = new XmlSerializer(responseMethod.ProxyReturnType);\n```\n4. Now we can create a new instance of `XmlResponseConstructor` and return it. The implementation should now look like this:\n```csharp\nusing Gossip4Net.Http.Builder.Response;\nusing Gossip4Net.Http.Modifier.Response;\nusing Gossip4Net.Http.Modifier.Response.Registration;\nusing System.Xml.Serialization;\n\nnamespace Gossip4Net.Http.Xml.Modifier.Response.Registration\n{\n    public class XmlResponseConstructorRegistration : IResponseConstructorRegistration\n    {\n        public IList\u003cIResponseConstructor\u003e? ForMethod(ResponseMethodContext responseMethod)\n        {\n            if (responseMethod.IsVoid)\n            {\n                return null;\n            }\n            XmlSerializer xmlSerializer = new XmlSerializer(responseMethod.ProxyReturnType);\n            return new List\u003cIResponseConstructor\u003e\n            {\n                new XmlResponseConstructor(xmlSerializer)\n            };\n        }\n    }\n}\n```\n\n#### Adding XML response support to the builder \u0026 testing it\n1. First we need to define our API interface and models\n```csharp\nusing FluentAssertions;\nusing Gossip4Net.Http.Attributes;\nusing Gossip4Net.Http.Attributes.Mappings;\nusing Gossip4Net.Http.Xml.Modifier.Response.Registration;\nusing System.Xml.Serialization;\n\nnamespace Gossip4Net.Http.Xml.Test\n{\n    [XmlRoot(\"slideshow\")]\n    public record Slideshow([property: XmlAttribute(\"title\")] string Title, [property: XmlAttribute(\"author\")] string Author)\n    {\n        // Default constructor required by XmlSerializer\n        public Slideshow() : this(string.Empty, string.Empty) { }\n    }\n\n    [HttpApi(\"https://httpbin.org\")]\n    public interface IXmlHttpBinClient {\n\n        [GetMapping(\"/xml\")]\n        Task\u003cSlideshow\u003e GetSlideshow();\n    }\n}\n```\n2. Next we create a test case and a builder targeting our API definition\n```csharp\nusing FluentAssertions;\nusing Gossip4Net.Http.Attributes;\nusing Gossip4Net.Http.Attributes.Mappings;\nusing Gossip4Net.Http.Xml.Modifier.Response.Registration;\nusing System.Xml.Serialization;\n\nnamespace Gossip4Net.Http.Xml.Test\n{\n    public class GossipWithXmlTest\n    {\n        [Fact]\n        public async Task XmlResponseBodiesShouldGetDeserialized()\n        {\n            // Arrange\n            IHttpGossipBuilder\u003cIXmlHttpBinClient\u003e builder = new HttpGossipBuilder\u003cIXmlHttpBinClient\u003e();\n        }\n    }\n}\n```\n3. Now we need to add the XmlResponseConstructorRegistration.\n```csharp\n// Arrange\nIHttpGossipBuilder\u003cIXmlHttpBinClient\u003e builder = new HttpGossipBuilder\u003cIXmlHttpBinClient\u003e()\n    .WithRegistrations(reg =\u003e reg.With(new XmlResponseConstructorRegistration()));\n```\n4. Don't forget to add the default behavior (as the builder would instead lack any other \"basic\" feature).\n```csharp\n// Arrange\nIHttpGossipBuilder\u003cIXmlHttpBinClient\u003e builder = new HttpGossipBuilder\u003cIXmlHttpBinClient\u003e()\n    .WithRegistrations(reg =\u003e reg.With(new XmlResponseConstructorRegistration()))\n    .AddDefaultBehavior();\n```\n5. Use the `.Build()` method to implement your API definition and test the call.\n```csharp\nusing FluentAssertions;\nusing Gossip4Net.Http.Attributes;\nusing Gossip4Net.Http.Attributes.Mappings;\nusing Gossip4Net.Http.Xml.Modifier.Response.Registration;\nusing System.Xml.Serialization;\n\nnamespace Gossip4Net.Http.Xml.Test\n{\n    public class GossipWithXmlTest\n    {\n        [Fact]\n        public async Task XmlResponseBodiesShouldGetDeserialized()\n        {\n            // Arrange\n            IHttpGossipBuilder\u003cIXmlHttpBinClient\u003e builder = new HttpGossipBuilder\u003cIXmlHttpBinClient\u003e()\n                .WithRegistrations(reg =\u003e reg.With(new XmlResponseConstructorRegistration()))\n                .AddDefaultBehavior();\n\n            IXmlHttpBinClient client = builder.Build();\n\n            // Act\n            Slideshow slideshow = await client.GetSlideshow();\n\n            // Assert\n            slideshow.Title.Should().Be(\"Sample Slide Show\");\n            slideshow.Author.Should().Be(\"Yours Truly\");\n        }\n    }\n}\n```\n\nCongratulations - you've just implemented support for deserializing XML responses.\n\n## Testing\nTesting a component that relies on the API is as easy as just implementing/mocking the API interface.\n\nAssuming your API definition, model and service are looking like these:\n```csharp\nnamespace Demo\n{\n    public record ExampleResponse(\n        IDictionary\u003cstring, string\u003e Headers,\n        string Origin,\n        string Url,\n        IDictionary\u003cstring, string\u003e Args);\n\n    [HttpApi(\"https://httpbin.org\")]\n    public interface IExampleApi\n    {\n        [GetMapping(\"/get\")]\n        Task\u003cExampleResponse\u003e Get();\n    }\n\n    public class ExampleService\n    {\n        private readonly IExampleApi exampleApi;\n\n        public ExampleService(IExampleApi exampleApi)\n        {\n            this.exampleApi = exampleApi;\n        }\n\n        public async Task\u003cint\u003e CountHeaders()\n        {\n            return (await exampleApi.Get()).Headers.Count;\n        }\n    }\n}\n```\n\nThen your service test could look tike that (using Moq and FluentAssertions):\n\n```csharp\npublic class DemoMockTest\n{\n    [Fact]\n    public async Task CountHeadersShouldReturnNumberOfReceivedHeaders()\n    {\n        // Arrange\n        Mock\u003cIExampleApi\u003e apiMock = new Mock\u003cIExampleApi\u003e();\n        apiMock.Setup(api =\u003e api.Get())\n            .ReturnsAsync(new ExampleResponse(\n                Headers: new Dictionary\u003cstring, string\u003e { { \"Content-Type\", \"Example\" }, { \"Foo\", \"Bar\" } },\n                Origin: \"a string\",\n                Url: \"a url\",\n                Args: new Dictionary\u003cstring, string\u003e()\n            ));\n        \n        IExampleApi exampleApi = apiMock.Object;\n        ExampleService serviceUnderTest = new ExampleService(exampleApi);\n\n        // Act\n        int headerCount = await serviceUnderTest.CountHeaders();\n\n        // Assert\n        headerCount.Should().Be(2);\n    }\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmasinger%2Fgossip4net","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmasinger%2Fgossip4net","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmasinger%2Fgossip4net/lists"}