Skip to content

Commit a857f3f

Browse files
feat: enhance nullability handling and improve tests (#96)
* feat: address nullability warnings and improve test methods - Suppress CS1591 warnings in test project for missing XML documentation. - Update response header handling to safely add headers with null checks. - Change test methods from async void to async Task for better async handling. - Update response assertions to use null-forgiving operators and ensure non-null values. - Migrate to required properties in records for DTOs in integration tests. - Replace obsolete HttpRequestMessage.Properties with HttpRequestMessage.Options. - Fix various nullability warnings across integration and utility tests. - Add a comprehensive warnless plan to eliminate all compiler warnings. * feat: enhance nullability handling in FluentHttpHeaders and related classes * feat: update package version to 5.0.0 * chore: remove contributing section from README.md * docs: update README to reflect .NET version changes and add support for net10 * fix: correct typos and improve clarity in README.md * refactor: remove obsolete GqlQuery class and related documentation * chore: remove warnless-plan.md as part of the effort to eliminate compiler and analyzer warnings
1 parent a5e49a7 commit a857f3f

55 files changed

Lines changed: 365 additions & 234 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828

2929
<!-- Common compile parameters -->
3030
<PropertyGroup>
31-
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
32-
<NoWarn>$(NoWarn);1701;1702;1705;1591</NoWarn>
31+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
32+
<NoWarn>$(NoWarn);1701;1702;1705</NoWarn>
3333
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
3434
<LangVersion>14.0</LangVersion>
3535
<Nullable>enable</Nullable>

README.md

Lines changed: 39 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml/badge.svg)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml)
77
[![NuGet version](https://badge.fury.io/nu/fluentlyhttpclient.svg)](https://badge.fury.io/nu/fluentlyhttpclient)
88

9-
Http Client for .NET Standard with fluent APIs which are intuitive, easy to use and also highly extensible.
9+
Http Client for .NET with fluent APIs which are intuitive, easy to use and also highly extensible.
1010

1111
**Quick links**
1212

@@ -35,6 +35,7 @@ Http Client for .NET Standard with fluent APIs which are intuitive, easy to use
3535
| 2.x | .NET Standard 2 | |
3636
| 3.x | .NET Standard 2 | [![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml/badge.svg?branch=3.x)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml) |
3737
| 4.x | net8 | [![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml/badge.svg?branch=v4)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml) |
38+
| 5.x | net10 | [![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml/badge.svg?branch=v5)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml) |
3839

3940
### NuGet
4041
```
@@ -69,7 +70,7 @@ PM> Install-Package FluentlyHttpClient
6970
- [Usage](#usage-1)
7071
- [Query params](#query-params)
7172
- [Interpolate Url](#interpolate-url)
72-
- [ReturnAsReponse, ReturnAsResponse`<T>` and Return`<T>`](#returnasreponse-returnasresponset-and-returnt)
73+
- [ReturnAsResponse, ReturnAsResponse`<T>` and Return`<T>`](#returnasresponse-returnasresponset-and-returnt)
7374
- [GraphQL](#graphql)
7475
- [Middleware](#middleware)
7576
- [Middleware options](#middleware-options)
@@ -84,35 +85,28 @@ PM> Install-Package FluentlyHttpClient
8485
- [Simple Single file HttpClient](#simple-single-file-httpclient)
8586
- [Testing/Mocking](#testingmocking)
8687
- [Test example with RichardSzalay.MockHttp](#test-example-with-richardszalaymockhttp)
87-
- [Contributing](#contributing)
88-
- [Setup Machine for Development](#setup-machine-for-development)
89-
- [Commands](#commands)
9088

9189
## Usage
9290

9391
### Configure
9492
Add services via `.AddFluentlyHttpClient()`.
9593

9694
```cs
97-
// using Startup.cs (can be elsewhere)
98-
public void ConfigureServices(IServiceCollection services)
99-
{
100-
services.AddFluentlyHttpClient();
101-
}
95+
// Program.cs
96+
var builder = WebApplication.CreateBuilder(args);
97+
builder.Services.AddFluentlyHttpClient();
98+
var app = builder.Build();
10299
```
103100

104-
Configure an Http client using the Http Factory (you need at least one).
101+
Configure an HTTP client using the HTTP client factory (you need at least one).
105102
```cs
106-
// using Startup.cs (can be elsewhere)
107-
public void Configure(IApplicationBuilder app, IFluentHttpClientFactory fluentHttpClientFactory)
108-
{
109-
fluentHttpClientFactory.CreateBuilder(identifier: "platform") // keep a note of the identifier, its needed later
110-
.WithBaseUrl("http://sketch7.com") // required
111-
.WithHeader("user-agent", "slabs-testify")
112-
.WithTimeout(5)
113-
.UseMiddleware<LoggerHttpMiddleware>()
114-
.Register(); // register client builder to factory
115-
}
103+
var fluentHttpClientFactory = app.Services.GetRequiredService<IFluentHttpClientFactory>();
104+
fluentHttpClientFactory.CreateBuilder(identifier: "platform") // keep a note of the identifier, it's needed later
105+
.WithBaseUrl("https://sketch7.com") // required
106+
.WithHeader("user-agent", "my-app")
107+
.WithTimeout(5)
108+
.UseMiddleware<LoggerHttpMiddleware>()
109+
.Register(); // register client builder to factory
116110
```
117111

118112
### Basic usage
@@ -124,11 +118,11 @@ Simple API (non-fluent) is good for simple requests as it has a clean, minimal A
124118
// inject factory and get client
125119
var httpClient = fluentHttpClientFactory.Get(identifier: "platform");
126120

127-
// HTTP GET + deserialize result (non-fleunt API)
128-
Hero hero = await httpClient.Get<Hero>("/api/heroes/azmodan");
121+
// HTTP GET + deserialize result (non-fluent API)
122+
Hero? hero = await httpClient.Get<Hero>("/api/heroes/azmodan");
129123

130-
// HTTP POST + deserialize result (non-fleunt API)
131-
Hero hero = await httpClient.Post<Hero>("/api/heroes/azmodan", new
124+
// HTTP POST + deserialize result (non-fluent API)
125+
Hero? hero = await httpClient.Post<Hero>("/api/heroes/azmodan", new
132126
{
133127
Title = "Lord of Sin"
134128
});
@@ -147,7 +141,7 @@ FluentHttpResponse<Hero> response =
147141
.ReturnAsResponse<Hero>(); // return with response
148142
149143
// HTTP POST + return response and deserialize result (fluent API)
150-
Hero hero = await httpClient.CreateRequest("/api/heroes/azmodan")
144+
Hero? hero = await httpClient.CreateRequest("/api/heroes/azmodan")
151145
.AsPost()
152146
.WithBody(new
153147
{
@@ -163,11 +157,11 @@ Http client builder is used to configure http clients in a fluent way.
163157

164158
```cs
165159
var clientBuilder = fluentHttpClientFactory.CreateBuilder(identifier: "platform")
166-
.WithBaseUrl("http://sketch7.com");
160+
.WithBaseUrl("https://sketch7.com");
167161
fluentHttpClientFactory.Add(clientBuilder);
168162

169163
// or similarly via the builder itself.
170-
clientBuilder.Register().
164+
clientBuilder.Register();
171165
```
172166

173167
#### Register multiple + share
@@ -237,7 +231,8 @@ httpClientBuilder.ConfigureFormatters(opts =>
237231
opts.Formatters.Add(new CustomFormatter());
238232
});
239233

240-
httpClientBuilder.WithVersion(HttpVersion.Version30) // specify to use http3 (defaults: http2)
234+
// http version - configure per-request via request builder defaults
235+
httpClientBuilder.WithRequestBuilderDefaults(builder => builder.WithVersion(HttpVersion.Version30)); // specify http3 per-request (default: http2)
241236
```
242237

243238
#### Re-using Http Client from Factory
@@ -259,7 +254,7 @@ Request builder is used to build http requests in a fluent way.
259254
#### Usage
260255

261256
```cs
262-
LoginResponse loginResponse =
257+
LoginResponse? loginResponse =
263258
await fluentHttpClient.CreateRequest("/api/auth/login")
264259
.AsPost() // set as HTTP Post
265260
.WithBody(new
@@ -292,7 +287,7 @@ requestBuilder.WithUri("{Language}/heroes/{Hero}", new
292287
}); // => /en/heroes/azmodan
293288
```
294289

295-
#### ReturnAsReponse, ReturnAsResponse`<T>` and Return`<T>`
290+
#### ReturnAsResponse, ReturnAsResponse`<T>` and Return`<T>`
296291

297292
```cs
298293
// send and returns HTTP response
@@ -301,8 +296,8 @@ FluentHttpResponse response = requestBuilder.ReturnAsResponse();
301296
// send and returns HTTP response + deserialize and return result via `.Data`
302297
FluentHttpResponse<Hero> response = requestBuilder.ReturnAsResponse<Hero>();
303298

304-
// send and returns derserialized result directly
305-
Hero hero = requestBuilder.Return<Hero>();
299+
// send and returns deserialized result directly
300+
Hero? hero = requestBuilder.Return<Hero>();
306301
```
307302

308303

@@ -317,7 +312,7 @@ httpClientBuilder.WithRequestBuilderDefaults(requestBuilder => requestBuilder.Wi
317312
FluentHttpResponse<Hero> response =
318313
await fluentHttpClient.CreateGqlRequest("{ hero {name, title } }")
319314
.ReturnAsGqlResponse<Hero>();
320-
// => response.Data.Title
315+
// => response.Data?.Title
321316
```
322317

323318

@@ -334,9 +329,9 @@ These are provided out of the box:
334329
| Timer | Determine how long (timespan) requests takes. |
335330
| Logger | Log request/response. |
336331

337-
Two important points to keep in mind:
332+
Three important points to keep in mind:
338333
- The first argument within constructor has to be `FluentHttpMiddlewareDelegate` which is generally called `next`.
339-
- The second argument within constructor has to be `FluentHttpMiddlewareClientContext` which is generally called `context`,
334+
- The second argument within constructor has to be `FluentHttpMiddlewareClientContext` which is generally called `context`.
340335
- During `Invoke` the `await _next(context);` must be invoked and return the response, in order to continue the flow.
341336

342337
The following is the timer middleware implementation *(bit simplified)*.
@@ -388,7 +383,7 @@ namespace FluentlyHttpClient
388383
// FluentHttpClientBuilder extension methods - add
389384
public static class FluentlyHttpMiddlwareExtensions
390385
{
391-
public static FluentHttpClientBuilder UseTimer(this FluentHttpClientBuilder builder, TimerHttpMiddlewareOptions options = null)
386+
public static FluentHttpClientBuilder UseTimer(this FluentHttpClientBuilder builder, TimerHttpMiddlewareOptions? options = null)
392387
=> builder.UseMiddleware<TimerHttpMiddleware>(options ?? new TimerHttpMiddlewareOptions());
393388
}
394389
}
@@ -398,7 +393,7 @@ TimeSpan timeTaken = response.GetTimeTaken();
398393
```
399394

400395
#### Middleware options
401-
Options to middleware can be passed via an argument. Note it has to be the second argument within the constructor.
396+
Options to middleware can be passed via an argument. Note it has to be the third argument within the constructor (after `next` and `context`).
402397

403398
```cs
404399
public TimerHttpMiddleware(
@@ -482,13 +477,14 @@ public static class FluentHttpHeaderBuilderExtensions
482477
builder.WithHeader(HeaderTypes.Authorization, $"{AuthSchemeTypes.Bearer} {token}");
483478
return (T)builder;
484479
}
480+
}
485481
```
486482
#### Extending Request/Response items
487483
In order to extend `Items` for both `FluentHttpRequest` and `FluentHttpResponse`, its best to extend `IFluentHttpMessageState`.
488484
This way it will be available for both. See example below.
489485

490486
```cs
491-
public static IDictionary<string, string> GetErrorCodeMappings(this IFluentHttpMessageState message)
487+
public static IDictionary<string, string>? GetErrorCodeMappings(this IFluentHttpMessageState message)
492488
{
493489
if (message.Items.TryGetValue(ErrorCodeMappingKey, out var value))
494490
return (IDictionary<string, string>)value;
@@ -526,7 +522,7 @@ public class SelfInfoHttpClient
526522
)
527523
{
528524
_httpClient = httpClientFactory.CreateBuilder("localhost")
529-
.WithBaseUrl($"http://localhost:5500}")
525+
.WithBaseUrl("http://localhost:5500")
530526
.Build();
531527
}
532528

@@ -545,14 +541,14 @@ However, we've been using [RichardSzalay.MockHttp](https://github.com/richardsza
545541

546542
```cs
547543
[Fact]
548-
public async void ShouldReturnContent()
544+
public async Task ShouldReturnContent()
549545
{
550546
// build services
551547
var servicesProvider = new ServiceCollection()
552548
.AddFluentlyHttpClient()
553549
.AddLogging()
554550
.BuildServiceProvider();
555-
var fluentHttpClientFactory = servicesProvider.GetService<IFluentHttpClientFactory>();
551+
var fluentHttpClientFactory = servicesProvider.GetRequiredService<IFluentHttpClientFactory>();
556552

557553
// define mocks
558554
var mockHttp = new MockHttpMessageHandler();
@@ -561,7 +557,7 @@ public async void ShouldReturnContent()
561557

562558
var httpClient = fluentHttpClientFactory.CreateBuilder("platform")
563559
.WithBaseUrl("https://sketch7.com")
564-
.AddMiddleware<TimerHttpMiddleware>()
560+
.UseMiddleware<TimerHttpMiddleware>()
565561
.WithMessageHandler(mockHttp) // set message handler to mock
566562
.Build();
567563

@@ -573,28 +569,3 @@ public async void ShouldReturnContent()
573569
Assert.NotEqual(TimeSpan.Zero, response.GetTimeTaken());
574570
}
575571
```
576-
577-
## Contributing
578-
579-
### Setup Machine for Development
580-
Install/setup the following:
581-
582-
- NodeJS v8+
583-
- Visual Studio Code or similar code editor
584-
- Git + SourceTree, SmartGit or similar (optional)
585-
586-
### Commands
587-
588-
```bash
589-
# run tests
590-
npm test
591-
592-
# bump version
593-
npm version minor --no-git-tag # major | minor | patch | prerelease
594-
595-
# nuget pack (only)
596-
npm run pack
597-
598-
# nuget publish dev (pack + publish + clean)
599-
npm run publish:dev
600-
```

benchmark/Benchmarking.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ public void Setup()
4444
{
4545
var mockHttp = new MockHttpMessageHandler();
4646
mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/json")
47-
.Respond("application/json", request => request.Content.ReadAsStreamAsync().Result);
47+
.Respond("application/json", request => request.Content!.ReadAsStreamAsync().Result);
4848

4949
mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/msgpack")
50-
//.Respond("application/x-msgpack", "��Key�valeera�Name�Valeera�Title�Shadow of the Uncrowned")
51-
.Respond("application/x-msgpack", request => request.Content.ReadAsStreamAsync().Result);
50+
//.Respond("application/x-msgpack", "\x99\xa3Key\xa6valeera...")
51+
.Respond("application/x-msgpack", request => request.Content!.ReadAsStreamAsync().Result);
5252

5353
var fluentHttpClientFactory = BuildContainer()
5454
.GetRequiredService<IFluentHttpClientFactory>();
@@ -79,33 +79,33 @@ public void Setup()
7979
;
8080

8181
Console.WriteLine($"Setup Complete");
82-
Console.WriteLine($" - _jsonHttpClient: {_jsonHttpClient.DefaultFormatter.GetType().Name}");
83-
Console.WriteLine($" - _messagePackHttpClient: {_messagePackHttpClient.DefaultFormatter.GetType().Name}");
84-
Console.WriteLine($" - _systemTextJsonHttpClient: {_systemTextJsonHttpClient.DefaultFormatter.GetType().Name}");
82+
Console.WriteLine($" - _jsonHttpClient: {_jsonHttpClient!.DefaultFormatter?.GetType().Name}");
83+
Console.WriteLine($" - _messagePackHttpClient: {_messagePackHttpClient!.DefaultFormatter?.GetType().Name}");
84+
Console.WriteLine($" - _systemTextJsonHttpClient: {_systemTextJsonHttpClient!.DefaultFormatter?.GetType().Name}");
8585
}
8686

8787
[Benchmark]
88-
public Task<Hero> PostAsJson()
88+
public Task<Hero?> PostAsJson()
8989
{
90-
return _jsonHttpClient.CreateRequest("/api/json")
90+
return _jsonHttpClient!.CreateRequest("/api/json")
9191
.AsPost()
9292
.WithBody(_request)
9393
.Return<Hero>();
9494
}
9595

9696
[Benchmark]
97-
public Task<Hero> PostAsMessagePack()
97+
public Task<Hero?> PostAsMessagePack()
9898
{
99-
return _messagePackHttpClient.CreateRequest("/api/msgpack")
99+
return _messagePackHttpClient!.CreateRequest("/api/msgpack")
100100
.AsPost()
101101
.WithBody(_request)
102102
.Return<Hero>();
103103
}
104104

105105
[Benchmark]
106-
public Task<Hero> PostAsSystemTextJson()
106+
public Task<Hero?> PostAsSystemTextJson()
107107
{
108-
return _systemTextJsonHttpClient.CreateRequest("/api/json")
108+
return _systemTextJsonHttpClient!.CreateRequest("/api/json")
109109
.AsPost()
110110
.WithBody(_request)
111111
.Return<Hero>();

benchmark/FluentlyHttpClient.Benchmarks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<IsPackable>false</IsPackable>
8+
<NoWarn>$(NoWarn);1591</NoWarn>
89
</PropertyGroup>
910

1011
<ItemGroup>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sketch7/fluently-http-client",
3-
"version": "4.1.2",
3+
"version": "5.0.0",
44
"scripts": {
55
"pack": "bash ./tools/pack.sh",
66
"prepublish:dev": "npm run pack",

samples/FluentlyHttpClient.Sample.Api/FluentlyHttpClient.Sample.Api.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
22

3+
<PropertyGroup>
4+
<NoWarn>$(NoWarn);1591</NoWarn>
5+
</PropertyGroup>
6+
37
<ItemGroup>
48
<PackageReference Include="Humanizer.Core" />
59
</ItemGroup>

samples/FluentlyHttpClient.Sample.Api/Heroes/Hero.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ namespace FluentlyHttpClient.Sample.Api.Heroes;
55
public record Hero
66
{
77
[Required]
8-
public string Key { get; set; }
8+
public required string Key { get; init; }
99

1010
[Required]
11-
public string Name { get; set; }
12-
public string Title { get; set; }
11+
public required string Name { get; init; }
12+
public string? Title { get; init; }
1313
}

0 commit comments

Comments
 (0)