|
1 | 1 | # Scrutor.Extensions.HttpClient |
2 | | -A solution for https://github.com/khellang/Scrutor/issues/180 baked into a Nuget Package |
| 2 | + |
| 3 | +[](https://www.nuget.org/packages/Scrutor.Extensions.HttpClient/) |
| 4 | +[](https://www.nuget.org/packages/Scrutor.Extensions.HttpClient/) |
| 5 | +[](https://github.com/Chrison-dev/Scrutor.Extensions.HttpClient/actions/workflows/build.yml) |
| 6 | +[](https://github.com/Fallout-build/Fallout) |
| 7 | +[](LICENSE) |
| 8 | + |
| 9 | +A tiny companion for [Scrutor](https://github.com/khellang/Scrutor) that lets its |
| 10 | +assembly-scanning register each matched class as a **typed |
| 11 | +[`HttpClient`](https://learn.microsoft.com/dotnet/core/extensions/httpclient-factory#typed-clients)** |
| 12 | +(via `IHttpClientFactory`) instead of a plain service — the one thing Scrutor |
| 13 | +[deliberately doesn't do out of the box](https://github.com/khellang/Scrutor/issues/180). |
| 14 | + |
| 15 | +```csharp |
| 16 | +services.Scan(scan => scan |
| 17 | + .FromAssemblyOf<IWeatherClient>() |
| 18 | + .AddClasses(c => c.AssignableTo<IApiClient>()) |
| 19 | + .AsMatchingInterface() |
| 20 | + .AsHttpClient("weather")); // ← every scanned client becomes a typed HttpClient |
| 21 | +``` |
| 22 | + |
| 23 | +## Install |
| 24 | + |
| 25 | +```sh |
| 26 | +dotnet add package Scrutor.Extensions.HttpClient |
| 27 | +``` |
3 | 28 |
|
4 | 29 | ## Usage |
5 | 30 |
|
6 | | -1. Inject your Http Client as a named Client |
7 | | -2. Make sure that all your Api Clients are assignable to a common interface |
8 | | -3. Register all your Api Clients with Scrutor.Extensions.HttpClient |
| 31 | +1. Register a **named** `HttpClient` with your shared configuration. |
| 32 | +2. Make your API clients implement a common marker interface (and, for |
| 33 | + `.AsMatchingInterface()`, an `I{ClassName}` interface each). |
| 34 | +3. Scan for them and finish the chain with `.AsHttpClient("<name>")`. |
9 | 35 |
|
10 | 36 | ```csharp |
11 | | -/* Inject named HttpClient */ |
12 | | -services.AddHttpClient("MyHttpClient", client => |
| 37 | +// 1. A named client carrying the shared config (base address, handlers, Polly, …). |
| 38 | +services.AddHttpClient("weather", client => |
13 | 39 | { |
14 | | - client.BaseAddress = new Uri("https://api.example.com"); |
| 40 | + client.BaseAddress = new Uri("https://api.example.com"); |
15 | 41 | }); |
16 | 42 |
|
17 | | -/* Inject all implementations of IMyHttpApiClient */ |
| 43 | +// 2 + 3. Register every IApiClient implementation as a typed client of "weather". |
18 | 44 | services.Scan(scan => scan |
19 | | - .FromCallingAssembly() |
20 | | - .AddClasses(classes => classes.AssignableTo<IMyHttpApiClient>()) |
21 | | - .AsMatchingInterface() |
22 | | - .AsHttpClient("MyHttpClient") |
23 | | - ); |
24 | | -``` |
| 45 | + .FromAssemblyOf<IWeatherClient>() |
| 46 | + .AddClasses(classes => classes.AssignableTo<IApiClient>()) |
| 47 | + .AsMatchingInterface() |
| 48 | + .AsHttpClient("weather")); |
| 49 | +``` |
| 50 | + |
| 51 | +Now each scanned client is resolved from `IHttpClientFactory` with the `"weather"` |
| 52 | +client's configuration injected: |
| 53 | + |
| 54 | +```csharp |
| 55 | +public sealed class WeatherClient(HttpClient http) : IWeatherClient |
| 56 | +{ |
| 57 | + public Task<Forecast?> GetAsync(string city) => |
| 58 | + http.GetFromJsonAsync<Forecast>($"forecast/{city}"); |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +### Overloads |
| 63 | + |
| 64 | +| Call | Behaviour | |
| 65 | +|---|---| |
| 66 | +| `.AsHttpClient("name")` | Registers each scanned class as a typed client bound to the **named** client `"name"` — they share its configuration. | |
| 67 | +| `.AsHttpClient()` | Registers each scanned class as a typed client with **its own** default client (named after the service type). | |
| 68 | + |
| 69 | +Both are `IServiceTypeSelector` extensions, so they slot onto the end of any Scrutor |
| 70 | +`.Scan(...)` chain (`.AsMatchingInterface()`, `.AsSelf()`, `.As<T>()`, …). |
| 71 | + |
| 72 | +## Compatibility |
| 73 | + |
| 74 | +- **Scrutor 7.x** — the package major tracks Scrutor's major (`7.x` ↔ Scrutor 7.x). |
| 75 | +- Targets **`net8.0`** and **`netstandard2.0`** (so it's usable from .NET Framework 4.6.2+, |
| 76 | + .NET Core 2.0+, and modern .NET alike). |
| 77 | + |
| 78 | +### Trimming / AOT |
| 79 | + |
| 80 | +This bridges Scrutor's **runtime** assembly scan and closes the generic |
| 81 | +`AddHttpClient<TClient,TImplementation>` over the scanned types via reflection, so it |
| 82 | +is **not** trim- or Native-AOT-safe. The public methods are annotated with |
| 83 | +`[RequiresUnreferencedCode]` / `[RequiresDynamicCode]`, so a trimmed/AOT build gets an |
| 84 | +honest warning at the call site rather than silent breakage. |
| 85 | + |
| 86 | +## Building |
| 87 | + |
| 88 | +CI is defined in C# with [Fallout](https://github.com/Fallout-build/Fallout) (a NUKE |
| 89 | +fork); the GitHub Actions workflow is generated from the build, never hand-edited. |
| 90 | + |
| 91 | +```sh |
| 92 | +./build.cmd Test # run the spec suite |
| 93 | +./build.cmd Pack # test + pack the NuGet package |
| 94 | +``` |
| 95 | + |
| 96 | +## License |
| 97 | + |
| 98 | +[MIT](LICENSE) |
0 commit comments