diff --git a/Directory.Build.props b/Directory.Build.props index ca19c4e..d21a01d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,8 +28,8 @@ - false - $(NoWarn);1701;1702;1705;1591 + true + $(NoWarn);1701;1702;1705 true 14.0 enable diff --git a/Directory.Packages.props b/Directory.Packages.props index 45763d3..d92e5be 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,7 +14,7 @@ - + @@ -26,7 +26,8 @@ - + + diff --git a/README.md b/README.md index 31df7ae..f227473 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![CI](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml/badge.svg)](https://github.com/sketch7/FluentlyHttpClient/actions/workflows/ci.yml) [![NuGet version](https://badge.fury.io/nu/fluentlyhttpclient.svg)](https://badge.fury.io/nu/fluentlyhttpclient) -Http Client for .NET Standard with fluent APIs which are intuitive, easy to use and also highly extensible. +Http Client for .NET with fluent APIs which are intuitive, easy to use and also highly extensible. **Quick links** @@ -35,6 +35,7 @@ Http Client for .NET Standard with fluent APIs which are intuitive, easy to use | 2.x | .NET Standard 2 | | | 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) | | 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) | +| 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) | ### NuGet ``` @@ -69,7 +70,7 @@ PM> Install-Package FluentlyHttpClient - [Usage](#usage-1) - [Query params](#query-params) - [Interpolate Url](#interpolate-url) - - [ReturnAsReponse, ReturnAsResponse`` and Return``](#returnasreponse-returnasresponset-and-returnt) + - [ReturnAsResponse, ReturnAsResponse`` and Return``](#returnasresponse-returnasresponset-and-returnt) - [GraphQL](#graphql) - [Middleware](#middleware) - [Middleware options](#middleware-options) @@ -84,9 +85,6 @@ PM> Install-Package FluentlyHttpClient - [Simple Single file HttpClient](#simple-single-file-httpclient) - [Testing/Mocking](#testingmocking) - [Test example with RichardSzalay.MockHttp](#test-example-with-richardszalaymockhttp) -- [Contributing](#contributing) - - [Setup Machine for Development](#setup-machine-for-development) - - [Commands](#commands) ## Usage @@ -94,25 +92,21 @@ PM> Install-Package FluentlyHttpClient Add services via `.AddFluentlyHttpClient()`. ```cs -// using Startup.cs (can be elsewhere) -public void ConfigureServices(IServiceCollection services) -{ - services.AddFluentlyHttpClient(); -} +// Program.cs +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddFluentlyHttpClient(); +var app = builder.Build(); ``` -Configure an Http client using the Http Factory (you need at least one). +Configure an HTTP client using the HTTP client factory (you need at least one). ```cs -// using Startup.cs (can be elsewhere) -public void Configure(IApplicationBuilder app, IFluentHttpClientFactory fluentHttpClientFactory) -{ - fluentHttpClientFactory.CreateBuilder(identifier: "platform") // keep a note of the identifier, its needed later - .WithBaseUrl("http://sketch7.com") // required - .WithHeader("user-agent", "slabs-testify") - .WithTimeout(5) - .UseMiddleware() - .Register(); // register client builder to factory -} +var fluentHttpClientFactory = app.Services.GetRequiredService(); +fluentHttpClientFactory.CreateBuilder(identifier: "platform") // keep a note of the identifier, it's needed later + .WithBaseUrl("https://sketch7.com") // required + .WithHeader("user-agent", "my-app") + .WithTimeout(5) + .UseMiddleware() + .Register(); // register client builder to factory ``` ### Basic usage @@ -124,11 +118,11 @@ Simple API (non-fluent) is good for simple requests as it has a clean, minimal A // inject factory and get client var httpClient = fluentHttpClientFactory.Get(identifier: "platform"); -// HTTP GET + deserialize result (non-fleunt API) -Hero hero = await httpClient.Get("/api/heroes/azmodan"); +// HTTP GET + deserialize result (non-fluent API) +Hero? hero = await httpClient.Get("/api/heroes/azmodan"); -// HTTP POST + deserialize result (non-fleunt API) -Hero hero = await httpClient.Post("/api/heroes/azmodan", new +// HTTP POST + deserialize result (non-fluent API) +Hero? hero = await httpClient.Post("/api/heroes/azmodan", new { Title = "Lord of Sin" }); @@ -147,7 +141,7 @@ FluentHttpResponse response = .ReturnAsResponse(); // return with response // HTTP POST + return response and deserialize result (fluent API) -Hero hero = await httpClient.CreateRequest("/api/heroes/azmodan") +Hero? hero = await httpClient.CreateRequest("/api/heroes/azmodan") .AsPost() .WithBody(new { @@ -163,11 +157,11 @@ Http client builder is used to configure http clients in a fluent way. ```cs var clientBuilder = fluentHttpClientFactory.CreateBuilder(identifier: "platform") - .WithBaseUrl("http://sketch7.com"); + .WithBaseUrl("https://sketch7.com"); fluentHttpClientFactory.Add(clientBuilder); // or similarly via the builder itself. -clientBuilder.Register(). +clientBuilder.Register(); ``` #### Register multiple + share @@ -237,7 +231,8 @@ httpClientBuilder.ConfigureFormatters(opts => opts.Formatters.Add(new CustomFormatter()); }); -httpClientBuilder.WithVersion(HttpVersion.Version30) // specify to use http3 (defaults: http2) +// http version - configure per-request via request builder defaults +httpClientBuilder.WithRequestBuilderDefaults(builder => builder.WithVersion(HttpVersion.Version30)); // specify http3 per-request (default: http2) ``` #### Re-using Http Client from Factory @@ -259,7 +254,7 @@ Request builder is used to build http requests in a fluent way. #### Usage ```cs -LoginResponse loginResponse = +LoginResponse? loginResponse = await fluentHttpClient.CreateRequest("/api/auth/login") .AsPost() // set as HTTP Post .WithBody(new @@ -292,7 +287,7 @@ requestBuilder.WithUri("{Language}/heroes/{Hero}", new }); // => /en/heroes/azmodan ``` -#### ReturnAsReponse, ReturnAsResponse`` and Return`` +#### ReturnAsResponse, ReturnAsResponse`` and Return`` ```cs // send and returns HTTP response @@ -301,8 +296,8 @@ FluentHttpResponse response = requestBuilder.ReturnAsResponse(); // send and returns HTTP response + deserialize and return result via `.Data` FluentHttpResponse response = requestBuilder.ReturnAsResponse(); -// send and returns derserialized result directly -Hero hero = requestBuilder.Return(); +// send and returns deserialized result directly +Hero? hero = requestBuilder.Return(); ``` @@ -317,7 +312,7 @@ httpClientBuilder.WithRequestBuilderDefaults(requestBuilder => requestBuilder.Wi FluentHttpResponse response = await fluentHttpClient.CreateGqlRequest("{ hero {name, title } }") .ReturnAsGqlResponse(); - // => response.Data.Title + // => response.Data?.Title ``` @@ -334,9 +329,9 @@ These are provided out of the box: | Timer | Determine how long (timespan) requests takes. | | Logger | Log request/response. | -Two important points to keep in mind: +Three important points to keep in mind: - The first argument within constructor has to be `FluentHttpMiddlewareDelegate` which is generally called `next`. - - The second argument within constructor has to be `FluentHttpMiddlewareClientContext` which is generally called `context`, + - The second argument within constructor has to be `FluentHttpMiddlewareClientContext` which is generally called `context`. - During `Invoke` the `await _next(context);` must be invoked and return the response, in order to continue the flow. The following is the timer middleware implementation *(bit simplified)*. @@ -388,7 +383,7 @@ namespace FluentlyHttpClient // FluentHttpClientBuilder extension methods - add public static class FluentlyHttpMiddlwareExtensions { - public static FluentHttpClientBuilder UseTimer(this FluentHttpClientBuilder builder, TimerHttpMiddlewareOptions options = null) + public static FluentHttpClientBuilder UseTimer(this FluentHttpClientBuilder builder, TimerHttpMiddlewareOptions? options = null) => builder.UseMiddleware(options ?? new TimerHttpMiddlewareOptions()); } } @@ -398,7 +393,7 @@ TimeSpan timeTaken = response.GetTimeTaken(); ``` #### Middleware options -Options to middleware can be passed via an argument. Note it has to be the second argument within the constructor. +Options to middleware can be passed via an argument. Note it has to be the third argument within the constructor (after `next` and `context`). ```cs public TimerHttpMiddleware( @@ -482,13 +477,14 @@ public static class FluentHttpHeaderBuilderExtensions builder.WithHeader(HeaderTypes.Authorization, $"{AuthSchemeTypes.Bearer} {token}"); return (T)builder; } +} ``` #### Extending Request/Response items In order to extend `Items` for both `FluentHttpRequest` and `FluentHttpResponse`, its best to extend `IFluentHttpMessageState`. This way it will be available for both. See example below. ```cs -public static IDictionary GetErrorCodeMappings(this IFluentHttpMessageState message) +public static IDictionary? GetErrorCodeMappings(this IFluentHttpMessageState message) { if (message.Items.TryGetValue(ErrorCodeMappingKey, out var value)) return (IDictionary)value; @@ -526,7 +522,7 @@ public class SelfInfoHttpClient ) { _httpClient = httpClientFactory.CreateBuilder("localhost") - .WithBaseUrl($"http://localhost:5500}") + .WithBaseUrl("http://localhost:5500") .Build(); } @@ -545,14 +541,14 @@ However, we've been using [RichardSzalay.MockHttp](https://github.com/richardsza ```cs [Fact] -public async void ShouldReturnContent() +public async Task ShouldReturnContent() { // build services var servicesProvider = new ServiceCollection() .AddFluentlyHttpClient() .AddLogging() .BuildServiceProvider(); - var fluentHttpClientFactory = servicesProvider.GetService(); + var fluentHttpClientFactory = servicesProvider.GetRequiredService(); // define mocks var mockHttp = new MockHttpMessageHandler(); @@ -561,7 +557,7 @@ public async void ShouldReturnContent() var httpClient = fluentHttpClientFactory.CreateBuilder("platform") .WithBaseUrl("https://sketch7.com") - .AddMiddleware() + .UseMiddleware() .WithMessageHandler(mockHttp) // set message handler to mock .Build(); @@ -573,28 +569,3 @@ public async void ShouldReturnContent() Assert.NotEqual(TimeSpan.Zero, response.GetTimeTaken()); } ``` - -## Contributing - -### Setup Machine for Development -Install/setup the following: - -- NodeJS v8+ -- Visual Studio Code or similar code editor -- Git + SourceTree, SmartGit or similar (optional) - - ### Commands - -```bash -# run tests -npm test - -# bump version -npm version minor --no-git-tag # major | minor | patch | prerelease - -# nuget pack (only) -npm run pack - -# nuget publish dev (pack + publish + clean) -npm run publish:dev -``` \ No newline at end of file diff --git a/benchmark/Benchmarking.cs b/benchmark/Benchmarking.cs index 83d26c5..2489306 100644 --- a/benchmark/Benchmarking.cs +++ b/benchmark/Benchmarking.cs @@ -44,11 +44,11 @@ public void Setup() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/json") - .Respond("application/json", request => request.Content.ReadAsStreamAsync().Result); + .Respond("application/json", request => request.Content!.ReadAsStreamAsync().Result); mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/msgpack") - //.Respond("application/x-msgpack", "��Key�valeera�Name�Valeera�Title�Shadow of the Uncrowned") - .Respond("application/x-msgpack", request => request.Content.ReadAsStreamAsync().Result); + //.Respond("application/x-msgpack", "\x99\xa3Key\xa6valeera...") + .Respond("application/x-msgpack", request => request.Content!.ReadAsStreamAsync().Result); var fluentHttpClientFactory = BuildContainer() .GetRequiredService(); @@ -79,33 +79,33 @@ public void Setup() ; Console.WriteLine($"Setup Complete"); - Console.WriteLine($" - _jsonHttpClient: {_jsonHttpClient.DefaultFormatter.GetType().Name}"); - Console.WriteLine($" - _messagePackHttpClient: {_messagePackHttpClient.DefaultFormatter.GetType().Name}"); - Console.WriteLine($" - _systemTextJsonHttpClient: {_systemTextJsonHttpClient.DefaultFormatter.GetType().Name}"); + Console.WriteLine($" - _jsonHttpClient: {_jsonHttpClient!.DefaultFormatter?.GetType().Name}"); + Console.WriteLine($" - _messagePackHttpClient: {_messagePackHttpClient!.DefaultFormatter?.GetType().Name}"); + Console.WriteLine($" - _systemTextJsonHttpClient: {_systemTextJsonHttpClient!.DefaultFormatter?.GetType().Name}"); } [Benchmark] - public Task PostAsJson() + public Task PostAsJson() { - return _jsonHttpClient.CreateRequest("/api/json") + return _jsonHttpClient!.CreateRequest("/api/json") .AsPost() .WithBody(_request) .Return(); } [Benchmark] - public Task PostAsMessagePack() + public Task PostAsMessagePack() { - return _messagePackHttpClient.CreateRequest("/api/msgpack") + return _messagePackHttpClient!.CreateRequest("/api/msgpack") .AsPost() .WithBody(_request) .Return(); } [Benchmark] - public Task PostAsSystemTextJson() + public Task PostAsSystemTextJson() { - return _systemTextJsonHttpClient.CreateRequest("/api/json") + return _systemTextJsonHttpClient!.CreateRequest("/api/json") .AsPost() .WithBody(_request) .Return(); diff --git a/benchmark/FluentlyHttpClient.Benchmarks.csproj b/benchmark/FluentlyHttpClient.Benchmarks.csproj index 44beddd..60e5443 100644 --- a/benchmark/FluentlyHttpClient.Benchmarks.csproj +++ b/benchmark/FluentlyHttpClient.Benchmarks.csproj @@ -5,6 +5,7 @@ enable enable false + $(NoWarn);1591 diff --git a/package.json b/package.json index 56b6df8..b6136b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sketch7/fluently-http-client", - "version": "4.1.2", + "version": "5.0.0", "scripts": { "pack": "bash ./tools/pack.sh", "prepublish:dev": "npm run pack", diff --git a/samples/FluentlyHttpClient.Sample.Api/FluentlyHttpClient.Sample.Api.csproj b/samples/FluentlyHttpClient.Sample.Api/FluentlyHttpClient.Sample.Api.csproj index 3df8084..76f76a3 100644 --- a/samples/FluentlyHttpClient.Sample.Api/FluentlyHttpClient.Sample.Api.csproj +++ b/samples/FluentlyHttpClient.Sample.Api/FluentlyHttpClient.Sample.Api.csproj @@ -1,5 +1,9 @@ + + $(NoWarn);1591 + + diff --git a/samples/FluentlyHttpClient.Sample.Api/Heroes/Hero.cs b/samples/FluentlyHttpClient.Sample.Api/Heroes/Hero.cs index d440273..2d76bf6 100644 --- a/samples/FluentlyHttpClient.Sample.Api/Heroes/Hero.cs +++ b/samples/FluentlyHttpClient.Sample.Api/Heroes/Hero.cs @@ -5,9 +5,9 @@ namespace FluentlyHttpClient.Sample.Api.Heroes; public record Hero { [Required] - public string Key { get; set; } + public required string Key { get; init; } [Required] - public string Name { get; set; } - public string Title { get; set; } + public required string Name { get; init; } + public string? Title { get; init; } } \ No newline at end of file diff --git a/src/FluentlyHttpClient.Entity/Constants.cs b/src/FluentlyHttpClient.Entity/Constants.cs index 88e0c23..aadf269 100644 --- a/src/FluentlyHttpClient.Entity/Constants.cs +++ b/src/FluentlyHttpClient.Entity/Constants.cs @@ -1,12 +1,18 @@ namespace FluentlyHttpClient.Entity; +/// Internal constants used for EF Core schema configuration. public static class Constants { + /// Max length for short text columns (30). public const int ShortTextLength = 30; + /// Max length for normal text columns (70). public const int NormalTextLength = 70; + /// Max length for long text columns (1500). public const int LongTextLength = 1500; + /// SQL schema name for cache tables. public const string SchemaName = "cache"; + /// Table name for HTTP response cache entries. public const string HttpResponseTable = "HttpResponses"; } \ No newline at end of file diff --git a/src/FluentlyHttpClient.Entity/DataSerializer.cs b/src/FluentlyHttpClient.Entity/DataSerializer.cs index f5bb519..5e36c2b 100644 --- a/src/FluentlyHttpClient.Entity/DataSerializer.cs +++ b/src/FluentlyHttpClient.Entity/DataSerializer.cs @@ -3,6 +3,7 @@ namespace FluentlyHttpClient.Entity; +/// JSON serialization helpers for EF Core value conversions. public static class DataSerializer { private static readonly JsonSerializerSettings Settings = new() @@ -10,6 +11,10 @@ public static class DataSerializer ContractResolver = new DefaultContractResolver() }; + /// Serialize to a JSON string. public static string Serialize(TItem value) => JsonConvert.SerializeObject(value, Settings); - public static TResult Deserialize(string value) => JsonConvert.DeserializeObject(value, Settings); + /// Deserialize a JSON string to . + public static TResult Deserialize(string value) => + // JsonConvert.DeserializeObject returns non-null when given a valid JSON string and valid target type + JsonConvert.DeserializeObject(value, Settings)!; } \ No newline at end of file diff --git a/src/FluentlyHttpClient.Entity/FluentHttpClientDbContext.cs b/src/FluentlyHttpClient.Entity/FluentHttpClientDbContext.cs index c8dfefc..a77237d 100644 --- a/src/FluentlyHttpClient.Entity/FluentHttpClientDbContext.cs +++ b/src/FluentlyHttpClient.Entity/FluentHttpClientDbContext.cs @@ -3,14 +3,18 @@ namespace FluentlyHttpClient.Entity; +/// EF Core database context for the HTTP response cache store. public class FluentHttpClientDbContext : DbContext { + /// Initializes a new instance of . public FluentHttpClientDbContext(DbContextOptions options) : base(options) { } + /// Gets the data set. public DbSet HttpResponses { get; set; } = null!; + /// protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -18,7 +22,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } + /// Apply pending EF Core migrations idempotently. public Task Initialize() => Database.MigrateAsync(); + /// Persist pending changes to the database. public Task Commit() => SaveChangesAsync(); } \ No newline at end of file diff --git a/src/FluentlyHttpClient.Entity/FluentHttpHeadersConverter.cs b/src/FluentlyHttpClient.Entity/FluentHttpHeadersConverter.cs index 00797ba..55180be 100644 --- a/src/FluentlyHttpClient.Entity/FluentHttpHeadersConverter.cs +++ b/src/FluentlyHttpClient.Entity/FluentHttpHeadersConverter.cs @@ -2,10 +2,12 @@ namespace FluentlyHttpClient.Entity; +/// EF Core value conversion helpers for . public static class FluentHttpHeadersConversion { - public static ValueConverter Convert = new( - x => DataSerializer.Serialize(x), + /// Converter that serializes to/from a JSON string. + public static ValueConverter Convert = new( + x => x != null ? DataSerializer.Serialize(x) : string.Empty, x => DataSerializer.Deserialize(x) ); } \ No newline at end of file diff --git a/src/FluentlyHttpClient.Entity/HttpResponseEntity.cs b/src/FluentlyHttpClient.Entity/HttpResponseEntity.cs index fd37b40..484f3c0 100644 --- a/src/FluentlyHttpClient.Entity/HttpResponseEntity.cs +++ b/src/FluentlyHttpClient.Entity/HttpResponseEntity.cs @@ -2,17 +2,29 @@ namespace FluentlyHttpClient.Entity; +/// EF Core entity representing a cached HTTP response. public class HttpResponseEntity : IHttpResponseStore { + /// Gets or sets the primary key (hashed request identifier). public string? Id { get; set; } + /// public string? Name { get; set; } + /// public string? Hash { get; set; } + /// public string? Url { get; set; } + /// public string? Content { get; set; } + /// public FluentHttpHeaders? Headers { get; set; } + /// public int StatusCode { get; set; } + /// public string? ReasonPhrase { get; set; } + /// public string? Version { get; set; } + /// public FluentHttpHeaders? ContentHeaders { get; set; } + /// public HttpRequestMessage? RequestMessage { get; set; } } \ No newline at end of file diff --git a/src/FluentlyHttpClient.Entity/HttpResponseMapping.cs b/src/FluentlyHttpClient.Entity/HttpResponseMapping.cs index 5687b91..76480c1 100644 --- a/src/FluentlyHttpClient.Entity/HttpResponseMapping.cs +++ b/src/FluentlyHttpClient.Entity/HttpResponseMapping.cs @@ -3,8 +3,10 @@ namespace FluentlyHttpClient.Entity; +/// EF Core mapping configuration for . public class HttpResponseMapping : IEntityTypeConfiguration { + /// public void Configure(EntityTypeBuilder builder) { builder.ToTable(Constants.HttpResponseTable, Constants.SchemaName); diff --git a/src/FluentlyHttpClient.Entity/Migrations/20190315104244_Initial.Designer.cs b/src/FluentlyHttpClient.Entity/Migrations/20190315104244_Initial.Designer.cs index f7eaf37..91e6044 100644 --- a/src/FluentlyHttpClient.Entity/Migrations/20190315104244_Initial.Designer.cs +++ b/src/FluentlyHttpClient.Entity/Migrations/20190315104244_Initial.Designer.cs @@ -1,4 +1,5 @@ // +#pragma warning disable CS1591 using FluentlyHttpClient.Entity; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -63,7 +64,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("HttpResponses","cache"); + b.ToTable("HttpResponses", "cache"); }); #pragma warning restore 612, 618 } diff --git a/src/FluentlyHttpClient.Entity/Migrations/20190315104244_Initial.cs b/src/FluentlyHttpClient.Entity/Migrations/20190315104244_Initial.cs index 50494f1..242f012 100644 --- a/src/FluentlyHttpClient.Entity/Migrations/20190315104244_Initial.cs +++ b/src/FluentlyHttpClient.Entity/Migrations/20190315104244_Initial.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Migrations; +#pragma warning disable CS1591 +using Microsoft.EntityFrameworkCore.Migrations; namespace FluentlyHttpClient.Entity.Migrations { diff --git a/src/FluentlyHttpClient.Entity/RemoteResponseCacheService.cs b/src/FluentlyHttpClient.Entity/RemoteResponseCacheService.cs index f7a687e..e7a23b3 100644 --- a/src/FluentlyHttpClient.Entity/RemoteResponseCacheService.cs +++ b/src/FluentlyHttpClient.Entity/RemoteResponseCacheService.cs @@ -4,12 +4,14 @@ namespace FluentlyHttpClient.Entity; +/// SQL Server-backed implementation of using EF Core with a local memory cache layer. public class RemoteResponseCacheService : IResponseCacheService { private readonly FluentHttpClientDbContext _dbContext; private readonly IHttpResponseSerializer _serializer; private readonly IMemoryCache _cache; + /// Initializes a new instance of . public RemoteResponseCacheService( FluentHttpClientDbContext dbContext, IHttpResponseSerializer serializer, @@ -21,10 +23,11 @@ IMemoryCache cache _cache = cache; } + /// public async Task Get(string hash) { var id = await hash.ComputeHash(); - var result = await _cache.GetOrCreate(id, async _ => + var cachedTask = _cache.GetOrCreate(id, async _ => { var item = await _dbContext.HttpResponses.FindAsync(id); @@ -32,10 +35,11 @@ IMemoryCache cache return await _serializer.Deserialize(item); }); - + var result = cachedTask != null ? await cachedTask : null; return result != null ? await result.Clone() : null; } + /// public async Task Set(string hash, FluentHttpResponse response) { var item = await _serializer.Serialize(response); diff --git a/src/FluentlyHttpClient.Entity/ServiceCollectionExtensions.cs b/src/FluentlyHttpClient.Entity/ServiceCollectionExtensions.cs index 8e45bf4..6584758 100644 --- a/src/FluentlyHttpClient.Entity/ServiceCollectionExtensions.cs +++ b/src/FluentlyHttpClient.Entity/ServiceCollectionExtensions.cs @@ -7,8 +7,14 @@ // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; +/// DI registration extensions for FluentlyHttpClient.Entity. public static class ServiceCollectionExtensions { + /// + /// Register FluentlyHttpClient Entity services (SQL Server-backed response cache) using the given connection string. + /// + /// The service collection. + /// SQL Server connection string. public static IServiceCollection AddFluentlyHttpClientEntity(this IServiceCollection services, string connectionString) { if (string.IsNullOrWhiteSpace(connectionString)) @@ -24,6 +30,12 @@ public static IServiceCollection AddFluentlyHttpClientEntity(this IServiceCollec return services.AddFluentlyHttpClientEntity(conn, builder => builder.EnableRetryOnFailure()); } + /// + /// Register FluentlyHttpClient Entity services using a pre-built . + /// + /// The service collection. + /// Pre-configured connection string builder. + /// Optional SQL Server options builder (e.g. retry policy). public static IServiceCollection AddFluentlyHttpClientEntity( this IServiceCollection services, SqlConnectionStringBuilder connectionStringBuilder, diff --git a/src/FluentlyHttpClient/Caching/HttpResponseSerializer.cs b/src/FluentlyHttpClient/Caching/HttpResponseSerializer.cs index 70487ed..ead3678 100644 --- a/src/FluentlyHttpClient/Caching/HttpResponseSerializer.cs +++ b/src/FluentlyHttpClient/Caching/HttpResponseSerializer.cs @@ -4,12 +4,20 @@ namespace FluentlyHttpClient.Caching; +/// Contract for serializing and deserializing objects to and from a store. public interface IHttpResponseSerializer { + /// Serialize a to a store object. + /// Type of store to serialize into. + /// The HTTP response to serialize. Task Serialize(FluentHttpResponse response) where THttpResponseStore : IHttpResponseStore, new(); + + /// Deserialize a store object back to a . + /// The store object to deserialize. Task Deserialize(IHttpResponseStore item); } +/// Default implementation of . public class HttpResponseSerializer : IHttpResponseSerializer { /// diff --git a/src/FluentlyHttpClient/Caching/HttpResponseStore.cs b/src/FluentlyHttpClient/Caching/HttpResponseStore.cs index 95f70ce..00cca66 100644 --- a/src/FluentlyHttpClient/Caching/HttpResponseStore.cs +++ b/src/FluentlyHttpClient/Caching/HttpResponseStore.cs @@ -1,29 +1,55 @@ namespace FluentlyHttpClient.Caching; +/// +/// Contract for storing a serialized HTTP response. +/// public interface IHttpResponseStore { + /// Gets or sets the client identifier name. string? Name { get; set; } + /// Gets or sets the request hash key. string? Hash { get; set; } + /// Gets or sets the request URL. string? Url { get; set; } + /// Gets or sets the response body content as a string. string? Content { get; set; } + /// Gets or sets the response headers. FluentHttpHeaders? Headers { get; set; } + /// Gets or sets the HTTP status code. int StatusCode { get; set; } + /// Gets or sets the reason phrase. string? ReasonPhrase { get; set; } + /// Gets or sets the HTTP version string. string? Version { get; set; } + /// Gets or sets the content headers. FluentHttpHeaders? ContentHeaders { get; set; } + /// Gets or sets the original request message. HttpRequestMessage? RequestMessage { get; set; } } +/// +/// Default in-memory store for a serialized HTTP response. +/// public class HttpResponseStore : IHttpResponseStore { + /// public string? Name { get; set; } + /// public string? Hash { get; set; } + /// public string? Url { get; set; } + /// public string? Content { get; set; } + /// public FluentHttpHeaders? Headers { get; set; } + /// public int StatusCode { get; set; } + /// public string? ReasonPhrase { get; set; } + /// public string? Version { get; set; } + /// public FluentHttpHeaders? ContentHeaders { get; set; } + /// public HttpRequestMessage? RequestMessage { get; set; } } \ No newline at end of file diff --git a/src/FluentlyHttpClient/Caching/ResponseCacheMiddleware.cs b/src/FluentlyHttpClient/Caching/ResponseCacheMiddleware.cs index eaaf327..17d99d7 100644 --- a/src/FluentlyHttpClient/Caching/ResponseCacheMiddleware.cs +++ b/src/FluentlyHttpClient/Caching/ResponseCacheMiddleware.cs @@ -14,6 +14,7 @@ public class ResponseCacheHttpMiddlewareOptions /// Ignore the request from being cached. /// public bool ShouldIgnore { get; set; } + /// Predicate to match which requests should be cached. When all requests are cached. public Predicate? Matcher { get; set; } /// @@ -54,7 +55,7 @@ public async Task Invoke(FluentHttpMiddlewareContext context { var request = context.Request; - var options = request.GetResponseCachingOptions(_options); + var options = request.GetResponseCachingOptions(_options) ?? _options; if (options.ShouldIgnore || options.Matcher != null && !options.Matcher(request)) return await _next(context); diff --git a/src/FluentlyHttpClient/Caching/ResponseCacheService.cs b/src/FluentlyHttpClient/Caching/ResponseCacheService.cs index 1b79ddc..1ccb2ec 100644 --- a/src/FluentlyHttpClient/Caching/ResponseCacheService.cs +++ b/src/FluentlyHttpClient/Caching/ResponseCacheService.cs @@ -2,27 +2,36 @@ namespace FluentlyHttpClient.Caching; +/// Contract for caching HTTP responses by request hash. public interface IResponseCacheService { + /// Get a cached response by hash, or if not found. Task Get(string hash); + + /// Store a response under the given hash. Task Set(string hash, FluentHttpResponse response); } +/// In-memory implementation of backed by . public class MemoryResponseCacheService : IResponseCacheService { private readonly IMemoryCache _cache; + /// Initializes a new instance of . + /// The memory cache to use. public MemoryResponseCacheService(IMemoryCache cache) { _cache = cache; } - public Task Get(string hash) + /// + public async Task Get(string hash) { var result = _cache.Get(hash); - return result?.Clone() ?? Task.FromResult(null); + return result != null ? await result.Clone() : null; } + /// public Task Set(string hash, FluentHttpResponse response) { _cache.Set(hash, response); diff --git a/src/FluentlyHttpClient/Exceptions.cs b/src/FluentlyHttpClient/Exceptions.cs index ee9589f..bce510a 100644 --- a/src/FluentlyHttpClient/Exceptions.cs +++ b/src/FluentlyHttpClient/Exceptions.cs @@ -5,15 +5,13 @@ namespace FluentlyHttpClient; /// public class RequestValidationException : Exception { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// Initializes a new instance with the specified message. public RequestValidationException(string message) : base(message) -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member { } -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// Initializes a new instance with the specified message and inner exception. public RequestValidationException(string message, Exception inner) : base(message, inner) -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member { } @@ -38,15 +36,13 @@ public static RequestValidationException FieldNotSpecified(string field) /// public class ClientBuilderValidationException : Exception { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// Initializes a new instance with the specified message. public ClientBuilderValidationException(string message) : base(message) -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member { } -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// Initializes a new instance with the specified message and inner exception. public ClientBuilderValidationException(string message, Exception inner) : base(message, inner) -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member { } diff --git a/src/FluentlyHttpClient/FluentHttpClient.cs b/src/FluentlyHttpClient/FluentHttpClient.cs index 713fc0d..fa23b59 100644 --- a/src/FluentlyHttpClient/FluentHttpClient.cs +++ b/src/FluentlyHttpClient/FluentHttpClient.cs @@ -15,7 +15,7 @@ public interface IFluentHttpClient : IDisposable /// /// Gets the base uri address for each request. /// - string BaseUrl { get; } + string? BaseUrl { get; } /// /// Underlying HTTP client. This should be avoided from being used, @@ -93,7 +93,7 @@ public class FluentHttpClient : IFluentHttpClient public string Identifier { get; } /// - public string BaseUrl { get; } + public string? BaseUrl { get; } /// public HttpClient RawHttpClient { get; } @@ -191,6 +191,7 @@ public async Task Send(FluentHttpRequest request) return executionContext.Response; } + /// public async Task Send(HttpRequestMessage request) { ArgumentNullException.ThrowIfNull(request, nameof(request)); @@ -237,6 +238,8 @@ private HttpClient Configure(FluentHttpClientOptions options) public void Dispose() => RawHttpClient?.Dispose(); + /// Implicitly converts a to the underlying . + /// The fluent client to convert. public static implicit operator HttpClient(FluentHttpClient client) => client.RawHttpClient; diff --git a/src/FluentlyHttpClient/FluentHttpClientBuilder.cs b/src/FluentlyHttpClient/FluentHttpClientBuilder.cs index afa7979..18298f0 100644 --- a/src/FluentlyHttpClient/FluentHttpClientBuilder.cs +++ b/src/FluentlyHttpClient/FluentHttpClientBuilder.cs @@ -85,36 +85,50 @@ public FluentHttpClientBuilder WithTimeout(TimeSpan timeout) return this; } + /// Set a single HTTP header for all requests built by this client. + /// Header name. + /// Header value. public FluentHttpClientBuilder WithHeader(string key, string value) { _headers.Set(key, value); return this; } + /// Set a single HTTP header with multiple values for all requests built by this client. + /// Header name. + /// Header values. public FluentHttpClientBuilder WithHeader(string key, StringValues values) { _headers.Set(key, values); return this; } + /// Set multiple HTTP headers from a string dictionary for all requests built by this client. + /// Headers to set. public FluentHttpClientBuilder WithHeaders(IDictionary headers) { _headers.SetRange(headers); return this; } + /// Set multiple HTTP headers from a string-array dictionary for all requests built by this client. + /// Headers to set. public FluentHttpClientBuilder WithHeaders(IDictionary headers) { _headers.SetRange(headers); return this; } + /// Set multiple HTTP headers from a dictionary for all requests built by this client. + /// Headers to set. public FluentHttpClientBuilder WithHeaders(IDictionary headers) { _headers.SetRange(headers); return this; } + /// Set multiple HTTP headers from a instance for all requests built by this client. + /// Headers to set. public FluentHttpClientBuilder WithHeaders(FluentHttpHeaders headers) { _headers.SetRange(headers); diff --git a/src/FluentlyHttpClient/FluentHttpClientFactory.cs b/src/FluentlyHttpClient/FluentHttpClientFactory.cs index 02deb19..1e74e6a 100644 --- a/src/FluentlyHttpClient/FluentHttpClientFactory.cs +++ b/src/FluentlyHttpClient/FluentHttpClientFactory.cs @@ -1,5 +1,9 @@ namespace FluentlyHttpClient; +/// +/// Options for allowing global client defaults to be configured. +/// +/// Optional action to configure defaults for every . public record FluentHttpClientFactoryOptions( Action? ConfigureDefaults ); @@ -73,6 +77,7 @@ public class FluentHttpClientFactory : IFluentHttpClientFactory private readonly Dictionary _clientsMap = []; private Action? _configure; + /// Gets the number of registered HTTP clients. public int Count => _clientsMap.Count; /// diff --git a/src/FluentlyHttpClient/FluentHttpHeaders.cs b/src/FluentlyHttpClient/FluentHttpHeaders.cs index f00eb6e..a3cc467 100644 --- a/src/FluentlyHttpClient/FluentHttpHeaders.cs +++ b/src/FluentlyHttpClient/FluentHttpHeaders.cs @@ -39,13 +39,14 @@ public FluentHttpHeadersOptions WithHashingExclude(Predicate /// Collection of headers and their values. /// -public partial class FluentHttpHeaders : IFluentHttpHeaderBuilder, IEnumerable> +public partial class FluentHttpHeaders : IFluentHttpHeaderBuilder, IEnumerable> { private static readonly FluentHttpHeadersOptions DefaultOptions = new(); private FluentHttpHeadersOptions _options = DefaultOptions; - private readonly Dictionary _data = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _data = new(StringComparer.OrdinalIgnoreCase); - public string[]? this[string key] + /// Gets or sets the header values for the specified key. + public string?[]? this[string key] { get => _data[key]; set => _data[key] = value; @@ -200,7 +201,7 @@ public FluentHttpHeaders AddRange(IDictionary headers) public FluentHttpHeaders AddRange(IDictionary headers) { foreach (var header in headers) - Add(header.Key, header.Value.ToArray()); + Add(header.Key, header.Value); return this; } @@ -231,7 +232,7 @@ public StringValues Get(string header) /// /// Header to try get. public string? GetValue(string header) - => _data.TryGetValue(header, out var value) ? value[0] : null; + => _data.TryGetValue(header, out var value) ? value?[0] : null; /// /// Set single header add/update if exists instead of throwing. @@ -362,7 +363,8 @@ public string ToHashString() return headers.ToFormattedString(); } - public IEnumerator> GetEnumerator() => _data.GetEnumerator(); + /// + public IEnumerator> GetEnumerator() => _data.GetEnumerator(); /// /// Converts to string. @@ -374,7 +376,7 @@ public string ToHashString() /// /// Converts to dictionary. /// - public Dictionary ToDictionary() => _data; + public Dictionary ToDictionary() => _data; FluentHttpHeaders IFluentHttpHeaderBuilder.WithHeader(string key, string value) => Add(key, value); FluentHttpHeaders IFluentHttpHeaderBuilder.WithHeader(string key, StringValues values) => Add(key, values); @@ -411,7 +413,7 @@ public StringValues AcceptLanguage public string? Authorization { get => Get(HeaderTypes.Authorization); - set => this[HeaderTypes.Authorization] = [value]; + set => this[HeaderTypes.Authorization] = value is null ? null : [value]; } /// @@ -420,7 +422,7 @@ public string? Authorization public string? CacheControl { get => Get(HeaderTypes.CacheControl); - set => this[HeaderTypes.CacheControl] = [value]; + set => this[HeaderTypes.CacheControl] = value is null ? null : [value]; } /// @@ -429,7 +431,7 @@ public string? CacheControl public string? ContentType { get => Get(HeaderTypes.ContentType); - set => this[HeaderTypes.ContentType] = [value]; + set => this[HeaderTypes.ContentType] = value is null ? null : [value]; } /// @@ -438,7 +440,7 @@ public string? ContentType public string? UserAgent { get => Get(HeaderTypes.UserAgent); - set => this[HeaderTypes.UserAgent] = [value]; + set => this[HeaderTypes.UserAgent] = value is null ? null : [value]; } /// @@ -456,6 +458,6 @@ public StringValues? XForwardedFor public string? XForwardedHost { get => Get(HeaderTypes.XForwardedHost); - set => this[HeaderTypes.XForwardedHost] = [value]; + set => this[HeaderTypes.XForwardedHost] = value is null ? null : [value]; } } \ No newline at end of file diff --git a/src/FluentlyHttpClient/FluentHttpRequestBuilder.cs b/src/FluentlyHttpClient/FluentHttpRequestBuilder.cs index b452cfd..df1b1dc 100644 --- a/src/FluentlyHttpClient/FluentHttpRequestBuilder.cs +++ b/src/FluentlyHttpClient/FluentHttpRequestBuilder.cs @@ -57,7 +57,7 @@ public FluentHttpHeaders Headers /// /// Gets the base url from the HTTP client. /// - public string BaseUrl => _fluentHttpClient.BaseUrl; + public string? BaseUrl => _fluentHttpClient.BaseUrl; /// /// @@ -112,36 +112,50 @@ public FluentHttpRequestBuilder WithVersionPolicy(HttpVersionPolicy policy) return this; } + /// Set a single HTTP header for this request. + /// Header name. + /// Header value. public FluentHttpRequestBuilder WithHeader(string key, string value) { Headers.Set(key, value); return this; } + /// Set a single HTTP header with multiple values for this request. + /// Header name. + /// Header values. public FluentHttpRequestBuilder WithHeader(string key, StringValues values) { Headers.Set(key, values); return this; } + /// Set multiple HTTP headers from a string dictionary for this request. + /// Headers to set. public FluentHttpRequestBuilder WithHeaders(IDictionary headers) { Headers.SetRange(headers); return this; } + /// Set multiple HTTP headers from a string-array dictionary for this request. + /// Headers to set. public FluentHttpRequestBuilder WithHeaders(IDictionary headers) { Headers.SetRange(headers); return this; } + /// Set multiple HTTP headers from a dictionary for this request. + /// Headers to set. public FluentHttpRequestBuilder WithHeaders(IDictionary headers) { Headers.SetRange(headers); return this; } + /// Set multiple HTTP headers from a instance for this request. + /// Headers to set. public FluentHttpRequestBuilder WithHeaders(FluentHttpHeaders headers) { Headers.SetRange(headers); @@ -349,6 +363,8 @@ public FluentHttpRequest Build() { foreach (var header in _headers) { + if (header.Value is null) + continue; if (header.Key == HeaderTypes.UserAgent) httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); else @@ -385,7 +401,7 @@ private static string BuildQueryString(object? queryParams, QueryStringOptions? if (dict.Count == 0) return string.Empty; - var queryCollection = new Dictionary(); + var queryCollection = new Dictionary(); foreach (var item in dict) queryCollection[item.Key] = item.Value; diff --git a/src/FluentlyHttpClient/FluentHttpResponse.cs b/src/FluentlyHttpClient/FluentHttpResponse.cs index 4bfcd98..27b0b78 100644 --- a/src/FluentlyHttpClient/FluentHttpResponse.cs +++ b/src/FluentlyHttpClient/FluentHttpResponse.cs @@ -37,7 +37,7 @@ public class FluentHttpResponse : IFluentHttpMessageState /// /// Gets readable string for debugger. /// - protected string DebuggerDisplay => $"[{(int)StatusCode}] '{ReasonPhrase}', Request: {{ [{Message.RequestMessage.Method}] '{Message.RequestMessage.RequestUri}' }}"; + protected string DebuggerDisplay => $"[{(int)StatusCode}] '{ReasonPhrase}', Request: {{ [{Message.RequestMessage?.Method}] '{Message.RequestMessage?.RequestUri}' }}"; /// /// Gets the underlying HTTP response message. diff --git a/src/FluentlyHttpClient/GraphQL/GqlRequest.cs b/src/FluentlyHttpClient/GraphQL/GqlRequest.cs index a7b74f3..0f77913 100644 --- a/src/FluentlyHttpClient/GraphQL/GqlRequest.cs +++ b/src/FluentlyHttpClient/GraphQL/GqlRequest.cs @@ -13,15 +13,10 @@ public class GqlRequest /// /// Gets or sets GraphQL query. /// - public string Query { get; set; } + public required string Query { get; set; } /// /// Gets or sets GraphQL query variables. /// public object? Variables { get; set; } -} - -[Obsolete("Use 'GqlRequest' instead.")] -public class GqlQuery : GqlRequest -{ } \ No newline at end of file diff --git a/src/FluentlyHttpClient/HttpMessageExtensions.cs b/src/FluentlyHttpClient/HttpMessageExtensions.cs index 3458656..81e26c3 100644 --- a/src/FluentlyHttpClient/HttpMessageExtensions.cs +++ b/src/FluentlyHttpClient/HttpMessageExtensions.cs @@ -3,6 +3,7 @@ namespace FluentlyHttpClient; internal static class HttpMessageExtensions { private const string RequestIdProperty = "request-id"; + private static readonly HttpRequestOptionsKey RequestIdKey = new(RequestIdProperty); internal static FluentHttpRequest ToFluentHttpRequest( this HttpRequestMessage request, @@ -12,20 +13,25 @@ FluentHttpClient client var builder = client .CreateRequest() .WithMethod(request.Method) - .WithUri(request.RequestUri.ToString()) - .WithBodyContent(request.Content) + .WithUri(request.RequestUri?.ToString() ?? string.Empty) ; - foreach (var prop in request.Properties) - builder.WithItem(prop.Key, prop.Value); + if (request.Content is not null) + builder.WithBodyContent(request.Content); + + foreach (var prop in request.Options) + { + if (prop.Value is not null) + builder.WithItem(prop.Key, prop.Value); + } return new(builder, request); } internal static string? GetRequestId(this HttpRequestMessage request) { - if (request.Properties.TryGetValue(RequestIdProperty, out var requestKey)) - return (string)requestKey; + if (request.Options.TryGetValue(RequestIdKey, out var requestKey)) + return requestKey; return null; } @@ -33,7 +39,7 @@ FluentHttpClient client internal static string AddRequestId(this HttpRequestMessage request, string? id = null) { var requestId = id ?? Guid.NewGuid().ToString(); - request.Properties.Add(RequestIdProperty, requestId); + request.Options.Set(RequestIdKey, requestId); return requestId; } diff --git a/src/FluentlyHttpClient/MediaFormatters/SystemTextJsonMediaTypeFormatter.cs b/src/FluentlyHttpClient/MediaFormatters/SystemTextJsonMediaTypeFormatter.cs index 0feaede..04d9338 100644 --- a/src/FluentlyHttpClient/MediaFormatters/SystemTextJsonMediaTypeFormatter.cs +++ b/src/FluentlyHttpClient/MediaFormatters/SystemTextJsonMediaTypeFormatter.cs @@ -4,6 +4,7 @@ namespace FluentlyHttpClient.MediaFormatters; // todo: move to separate lib? +/// Media type formatter that uses System.Text.Json for JSON serialization. public class SystemTextJsonMediaTypeFormatter : MediaTypeFormatter { private const string MediaType = "application/json"; @@ -30,12 +31,14 @@ public SystemTextJsonMediaTypeFormatter(JsonSerializerOptions? options) }; } + /// public override bool CanReadType(Type type) { ArgumentNullException.ThrowIfNull(type, nameof(type)); return IsAllowedType(type); } + /// public override bool CanWriteType(Type type) { ArgumentNullException.ThrowIfNull(type, nameof(type)); @@ -53,14 +56,17 @@ private static bool IsAllowedType(Type t) return false; } + /// public override async Task ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger) => await JsonSerializer.DeserializeAsync(readStream, type, _options); + /// public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext) => await JsonSerializer.SerializeAsync(writeStream, value, type, _options); } +/// Extension methods for to register additional formatters. public static partial class MediaTypeFormattingExtensions { /// diff --git a/src/FluentlyHttpClient/Middleware/FluentHttpMiddlewareBuilder.cs b/src/FluentlyHttpClient/Middleware/FluentHttpMiddlewareBuilder.cs index ef93939..7f89ef3 100644 --- a/src/FluentlyHttpClient/Middleware/FluentHttpMiddlewareBuilder.cs +++ b/src/FluentlyHttpClient/Middleware/FluentHttpMiddlewareBuilder.cs @@ -93,7 +93,7 @@ public IFluentHttpMiddlewareRunner Build(IFluentHttpClient httpClient) } } else - ctor = new object[] { }; + ctor = []; var instance = (IFluentHttpMiddleware)ActivatorUtilities.CreateInstance(_serviceProvider, pipe.Type, ctor); if (isFirst) diff --git a/src/FluentlyHttpClient/Middleware/FluentMiddlewareHttpHandler.cs b/src/FluentlyHttpClient/Middleware/FluentMiddlewareHttpHandler.cs index 7efe32b..da6d784 100644 --- a/src/FluentlyHttpClient/Middleware/FluentMiddlewareHttpHandler.cs +++ b/src/FluentlyHttpClient/Middleware/FluentMiddlewareHttpHandler.cs @@ -38,9 +38,9 @@ CancellationToken cancellationToken return response.ToFluentHttpResponse(fluentlyRequest.Items); }); - if (context != null) + if (context != null && requestId != null) _requestTracker.Push(requestId, fluentlyResponse); - return fluentlyResponse?.Message; + return fluentlyResponse.Message; } } \ No newline at end of file diff --git a/src/FluentlyHttpClient/Middleware/TimerHttpMiddleware.cs b/src/FluentlyHttpClient/Middleware/TimerHttpMiddleware.cs index 572d13d..e1ac099 100644 --- a/src/FluentlyHttpClient/Middleware/TimerHttpMiddleware.cs +++ b/src/FluentlyHttpClient/Middleware/TimerHttpMiddleware.cs @@ -25,6 +25,7 @@ public class TimerHttpMiddleware : IFluentHttpMiddleware private readonly TimerHttpMiddlewareOptions _options; private readonly ILogger _logger; + /// Initializes a new instance of . public TimerHttpMiddleware( FluentHttpMiddlewareDelegate next, FluentHttpMiddlewareClientContext context, diff --git a/src/FluentlyHttpClient/MultipartFormDataContentExtensions.cs b/src/FluentlyHttpClient/MultipartFormDataContentExtensions.cs index abc0541..656524b 100644 --- a/src/FluentlyHttpClient/MultipartFormDataContentExtensions.cs +++ b/src/FluentlyHttpClient/MultipartFormDataContentExtensions.cs @@ -2,6 +2,7 @@ namespace FluentlyHttpClient; +/// Extension methods for to add files easily. public static class MultipartFormDataContentExtensions { /// diff --git a/src/FluentlyHttpClient/RequestHashingExtensions.cs b/src/FluentlyHttpClient/RequestHashingExtensions.cs index 65f32de..c98129a 100644 --- a/src/FluentlyHttpClient/RequestHashingExtensions.cs +++ b/src/FluentlyHttpClient/RequestHashingExtensions.cs @@ -3,6 +3,7 @@ namespace FluentlyHttpClient; +/// Extension methods for computing and managing request hashes used by the response cache. public static class RequestHashingExtensions { private const string HashKey = "REQUEST_HASH"; @@ -62,9 +63,11 @@ internal static string GenerateHash(this FluentHttpRequest request) var headersHash = headers.ToHashString(); - var uri = request.Uri.IsAbsoluteUri - ? request.Uri - : new($"{request.Builder.BaseUrl.TrimEnd('/')}/{request.Uri.ToString().TrimStart('/')}"); + Uri uri; + if (request.Uri is { IsAbsoluteUri: true } absUri) + uri = absUri; + else + uri = new Uri($"{request.Builder.BaseUrl?.TrimEnd('/')}/{request.Uri?.ToString().TrimStart('/')}"); var uriHash = options?.UriManipulation == null ? uri.ToString() @@ -102,10 +105,11 @@ public static FluentHttpRequestBuilder WithRequestHashOptions(this FluentHttpReq public static RequestHashOptions? GetRequestHashOptions(this FluentHttpRequest request) { request.Items.TryGetValue(HashOptionsKey, out var result); - return (RequestHashOptions)result; + return result as RequestHashOptions; } } +/// Options that control how a request hash is computed by the response cache middleware. public class RequestHashOptions { /// diff --git a/src/FluentlyHttpClient/RequestTracker.cs b/src/FluentlyHttpClient/RequestTracker.cs index d865e4e..39670e7 100644 --- a/src/FluentlyHttpClient/RequestTracker.cs +++ b/src/FluentlyHttpClient/RequestTracker.cs @@ -27,9 +27,9 @@ public void Push(string key, FluentHttpResponse response) public FluentlyExecutionContext Pop(string key) { _contexts.TryRemove(key, out var context); - return context; + return context ?? throw new InvalidOperationException($"No execution context found for request ID '{key}'."); } - public bool TryPeek(string key, out FluentlyExecutionContext context) + public bool TryPeek(string key, out FluentlyExecutionContext? context) => _contexts.TryGetValue(key, out context); } \ No newline at end of file diff --git a/src/FluentlyHttpClient/Utils/HttpExtensions.cs b/src/FluentlyHttpClient/Utils/HttpExtensions.cs index 2206fa3..29b9b45 100644 --- a/src/FluentlyHttpClient/Utils/HttpExtensions.cs +++ b/src/FluentlyHttpClient/Utils/HttpExtensions.cs @@ -27,7 +27,8 @@ public static void CopyFrom(this HttpHeaders destination, HttpHeaders source) public static void AddRange(this HttpHeaders headers, FluentHttpHeaders values) { foreach (var headerEntry in values) - headers.Add(headerEntry.Key, (IEnumerable)headerEntry.Value); + if (headerEntry.Value != null) + headers.Add(headerEntry.Key, headerEntry.Value); } /// diff --git a/src/FluentlyHttpClient/Utils/ObjectExtensions.cs b/src/FluentlyHttpClient/Utils/ObjectExtensions.cs index ddc4350..8cc9d26 100644 --- a/src/FluentlyHttpClient/Utils/ObjectExtensions.cs +++ b/src/FluentlyHttpClient/Utils/ObjectExtensions.cs @@ -11,18 +11,18 @@ public static class ObjectExtensions { /// Get the key=>value pairs represented by a dictionary or anonymous object. /// The key=>value pairs in the query argument. If this is a dictionary, the keys and values are used. Otherwise, the property names and values are used. - public static IDictionary ToDictionary(this object arguments) + public static IDictionary ToDictionary(this object arguments) { switch (arguments) { case null: - return new Dictionary(); - case IDictionary args: + return new Dictionary(); + case IDictionary args: return args; case IDictionary argDict: - IDictionary dict = new Dictionary(); + IDictionary dict = new Dictionary(); foreach (var key in argDict.Keys) - dict.Add(key.ToString(), argDict[key]); + dict.Add(key.ToString() ?? string.Empty, argDict[key]); return dict; } // object diff --git a/src/FluentlyHttpClient/Utils/QueryStringOptions.cs b/src/FluentlyHttpClient/Utils/QueryStringOptions.cs index 05d50f1..a4c3bb6 100644 --- a/src/FluentlyHttpClient/Utils/QueryStringOptions.cs +++ b/src/FluentlyHttpClient/Utils/QueryStringOptions.cs @@ -37,6 +37,7 @@ public record QueryStringOptions /// public static readonly Func DefaultValueEncoder = HttpEncode; + /// Debugger display string. protected string DebuggerDisplay => $"CollectionMode: '{CollectionMode}'"; /// @@ -48,6 +49,7 @@ public record QueryStringOptions /// Gets or sets the function to format a collection item. This will allow you to manipulate the value. /// public Func? CollectionItemFormatter { get; set; } + /// Gets or sets per-key collection item formatters, keyed by query parameter name. public Dictionary>? CollectionValuePerKeyItemFormatter { get; set; } /// diff --git a/src/FluentlyHttpClient/Utils/RegexExtensions.cs b/src/FluentlyHttpClient/Utils/RegexExtensions.cs index ac647d4..2a66e46 100644 --- a/src/FluentlyHttpClient/Utils/RegexExtensions.cs +++ b/src/FluentlyHttpClient/Utils/RegexExtensions.cs @@ -14,7 +14,7 @@ public static class RegexExtensions /// Template used for replacement/interpolation. e.g. "/person/{id}" /// Arguments to interpolate with template. /// Returns string with tokens replaced. - public static string ReplaceTokens(this Regex re, string template, IDictionary args) + public static string ReplaceTokens(this Regex re, string template, IDictionary args) { string Evaluator(Match match) { @@ -22,7 +22,7 @@ string Evaluator(Match match) var paramValue = args[paramName]; if (paramValue == null) throw new ArgumentNullException(nameof(args), $"Template has a param which its value is not provided. Param: '{paramName}'"); - return args[match.Groups[1].Value].ToString(); + return args[match.Groups[1].Value]?.ToString() ?? string.Empty; } return re.Replace(template, Evaluator); diff --git a/test/FluentHttpClientFactoryTest.cs b/test/FluentHttpClientFactoryTest.cs index 200c6d1..f5e3994 100644 --- a/test/FluentHttpClientFactoryTest.cs +++ b/test/FluentHttpClientFactoryTest.cs @@ -13,7 +13,7 @@ public void ShouldAllowEmptyBaseUrl() var httpClient = GetNewClientFactory().CreateBuilder("abc") .Build(); - Assert.Null(httpClient.BaseUrl); + httpClient.BaseUrl.ShouldBeNull(); } } @@ -30,7 +30,7 @@ public void AddFluentlyHttpClient_Defaults_ShouldBeSet() .GetRequiredService(); var client = f.CreateBuilder("sketch7").Build(); - Assert.Equal("default-config", client.Headers.UserAgent.ToString()); + client.Headers.UserAgent.ToString().ShouldBe("default-config"); } @@ -57,13 +57,13 @@ public void Build_RegisterMulti_ShouldNotReplacePrevious() httpClientB.Headers.TryGetValues("X-S7", out var s7HeadersB); httpClientA.Headers.TryGetValues("X-Org", out var orgHeadersB); - Assert.Single(orgHeadersA, "s7"); - Assert.Single(s7HeadersA, "a"); - Assert.Equal("dXNlcjpwYSQk", httpClientA.Headers.Authorization.Parameter); + orgHeadersA.ShouldNotBeNull().ShouldHaveSingleItem().ShouldBe("s7"); + s7HeadersA.ShouldNotBeNull().ShouldHaveSingleItem().ShouldBe("a"); + httpClientA.Headers.Authorization!.Parameter.ShouldBe("dXNlcjpwYSQk"); - Assert.Single(orgHeadersB, "s7"); - Assert.Single(s7HeadersB, "b"); - Assert.Equal("dXNlci0yOnBhJCQ=", httpClientB.Headers.Authorization.Parameter); + orgHeadersB.ShouldNotBeNull().ShouldHaveSingleItem().ShouldBe("s7"); + s7HeadersB.ShouldNotBeNull().ShouldHaveSingleItem().ShouldBe("b"); + httpClientB.Headers.Authorization!.Parameter.ShouldBe("dXNlci0yOnBhJCQ="); } [Fact] @@ -77,8 +77,8 @@ public void ShouldHaveWithCustomDefaultsSet() var request = httpClient.CreateRequest("/api") .Build(); - Assert.NotNull(request); - Assert.Equal(HttpMethod.Put, request.Method); + request.ShouldNotBeNull(); + request.Method.ShouldBe(HttpMethod.Put); } [Fact] @@ -93,9 +93,9 @@ public void ShouldHaveCustomDefaultsCombined() var request = httpClient.CreateRequest("/api") .Build(); - Assert.NotNull(request); - Assert.Equal(HttpMethod.Put, request.Method); - Assert.Equal("user", request.Items["context"]); + request.ShouldNotBeNull(); + request.Method.ShouldBe(HttpMethod.Put); + request.Items["context"].ShouldBe("user"); } [Fact] @@ -110,9 +110,9 @@ public void ShouldHavePreviousCustomDefaultsReplaced() var request = httpClient.CreateRequest("/api") .Build(); - Assert.NotNull(request); - Assert.Equal(HttpMethod.Get, request.Method); - Assert.Equal("user", request.Items["context"]); + request.ShouldNotBeNull(); + request.Method.ShouldBe(HttpMethod.Get); + request.Items["context"].ShouldBe("user"); } [Fact] @@ -136,7 +136,7 @@ public void ShouldHaveQueryParamsDefaultsSet() }) .Build(); - Assert.Equal("/api/heroes?ROLES=warrior,assassin", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/api/heroes?ROLES=warrior,assassin"); } [Fact] @@ -154,8 +154,8 @@ public void ShouldAppendToParentsBaseUrl() .Build() ; - Assert.Equal("http://abc.com/v1/", subHttpClient.BaseUrl); - Assert.Equal("http://abc.com/", httpClient.BaseUrl); + subHttpClient.BaseUrl.ShouldBe("http://abc.com/v1/"); + httpClient.BaseUrl.ShouldBe("http://abc.com/"); } } @@ -169,7 +169,7 @@ public void ShouldSetClientFormatters() .ConfigureFormatters(opts => { opts.Formatters.Clear(); }) .Build(); - Assert.Empty(httpClient.Formatters); + httpClient.Formatters.ShouldBeEmpty(); } [Fact] @@ -183,7 +183,7 @@ public void ShouldSetDefaultFormatter() }) .Build(); - Assert.Equal(httpClient.Formatters.XmlFormatter, httpClient.DefaultFormatter); + httpClient.DefaultFormatter.ShouldBe(httpClient.Formatters.XmlFormatter); } [Fact] @@ -203,8 +203,8 @@ public void SetDefaultFormatterMany_ShouldBeSetCorrectly() .Build() ; - Assert.Equal(httpClient.Formatters.XmlFormatter, httpClient.DefaultFormatter); - Assert.Equal(httpClient2.Formatters.FormUrlEncodedFormatter, httpClient2.DefaultFormatter); + httpClient.DefaultFormatter.ShouldBe(httpClient.Formatters.XmlFormatter); + httpClient2.DefaultFormatter.ShouldBe(httpClient2.Formatters.FormUrlEncodedFormatter); } [Fact] @@ -220,7 +220,7 @@ public void ShouldAutoRegisterDefault() }) .Build(); - Assert.Equal(jsonFormatter, httpClient.DefaultFormatter); + httpClient.DefaultFormatter.ShouldBe(jsonFormatter); } [Fact] @@ -234,7 +234,7 @@ public void DefaultFormatterShouldBePlacedFirst() }) .Build(); - Assert.Equal(httpClient.Formatters.First(), httpClient.DefaultFormatter); + httpClient.DefaultFormatter.ShouldBe(httpClient.Formatters.First()); } } @@ -249,8 +249,7 @@ public void ShouldSetClientFormatters() .WithBaseUrl("http://abc.com") .Build(); - var userAgentHeader = httpClient.Headers.GetValues("User-Agent").FirstOrDefault(); - Assert.Equal("hots", userAgentHeader); + httpClient.Headers.GetValues("User-Agent").FirstOrDefault().ShouldBe("hots"); } } @@ -263,15 +262,15 @@ public void ShouldRegisterSuccessfully() .WithBaseUrl("http://abc.com") .Build(); - Assert.NotNull(httpClient); - Assert.Equal("abc", httpClient.Identifier); + httpClient.ShouldNotBeNull(); + httpClient.Identifier.ShouldBe("abc"); } [Fact] public void ThrowsErrorWhenIdentifierNotSpecified() { var clientBuilder = GetNewClientFactory().CreateBuilder(null!); - Assert.Throws(() => clientBuilder.Register()); + Should.Throw(() => clientBuilder.Register()); } [Fact] @@ -281,14 +280,14 @@ public void ThrowsErrorWhenAlreadyRegistered() .WithBaseUrl("http://abc.com") .Register(); - Assert.Throws(() => clientBuilder.Register()); + Should.Throw(() => clientBuilder.Register()); } } public class ClientFactory_Remove { [Fact] - public async void ShouldDisposeClient() + public async Task ShouldDisposeClient() { var fluentHttpClientFactory = GetNewClientFactory(); var clientBuilder = fluentHttpClientFactory.CreateBuilder("abc") @@ -298,7 +297,7 @@ public async void ShouldDisposeClient() var isRegistered = fluentHttpClientFactory.Remove("abc") .Has("abc"); - await Assert.ThrowsAsync(() => httpClient.Get("/api/heroes/azmodan")); - Assert.False(isRegistered); + await Should.ThrowAsync(() => httpClient.Get("/api/heroes/azmodan")); + isRegistered.ShouldBeFalse(); } } \ No newline at end of file diff --git a/test/FluentHttpClientTest.cs b/test/FluentHttpClientTest.cs index b4e5aa1..24b778f 100644 --- a/test/FluentHttpClientTest.cs +++ b/test/FluentHttpClientTest.cs @@ -10,7 +10,7 @@ public class HttpClient private readonly MessagePackMediaTypeFormatter _messagePackMediaTypeFormatter = new(); [Fact] - public async void Get_ShouldReturnContent() + public async Task Get_ShouldReturnContent() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When("https://sketch7.com/api/heroes/azmodan") @@ -23,12 +23,12 @@ public async void Get_ShouldReturnContent() var hero = await httpClient.Get("/api/heroes/azmodan"); - Assert.NotNull(hero); - Assert.Equal("Azmodan", hero.Name); + hero.ShouldNotBeNull(); + hero.Name.ShouldBe("Azmodan"); } [Fact] - public async void Post_ShouldReturnContent() + public async Task Post_ShouldReturnContent() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/heroes/azmodan") @@ -51,8 +51,8 @@ public async void Post_ShouldReturnContent() Title = "Lord of Sin" }); - Assert.NotNull(hero); - Assert.Equal("Lord of Sin", hero.Title); + hero.ShouldNotBeNull(); + hero.Title.ShouldBe("Lord of Sin"); } [Fact] @@ -92,25 +92,25 @@ public void CreateClient_ShouldInheritOptions() httpClient.Headers.TryGetValues("country", out var countryValues); var subClientCountry = subClient.Headers.GetValues("country").FirstOrDefault(); - Assert.Equal("sketch7", httpClient.Identifier); - Assert.Equal("sketch7.subclient", subClient.Identifier); - Assert.Equal("en-GB", httpClientLocale); - Assert.Equal("de", subClientLocale); - Assert.Null(countryValues?.FirstOrDefault()); - Assert.Equal("de", subClientCountry); - - Assert.Equal(httpClientRequest.HttpMethod, subClientRequest.HttpMethod); - Assert.Equal(httpClientRequest.Items["error-mapping"], subClientRequest.Items["error-mapping"]); - Assert.Equal("user", httpClientRequest.Items["context"]); - Assert.Equal("reward", subClientRequest.Items["context"]); - Assert.Equal(httpClient.Formatters.Count, subClient.Formatters.Count); + httpClient.Identifier.ShouldBe("sketch7"); + subClient.Identifier.ShouldBe("sketch7.subclient"); + httpClientLocale.ShouldBe("en-GB"); + subClientLocale.ShouldBe("de"); + countryValues?.FirstOrDefault().ShouldBeNull(); + subClientCountry.ShouldBe("de"); + + subClientRequest.HttpMethod.ShouldBe(httpClientRequest.HttpMethod); + subClientRequest.Items["error-mapping"].ShouldBe(httpClientRequest.Items["error-mapping"]); + httpClientRequest.Items["context"].ShouldBe("user"); + subClientRequest.Items["context"].ShouldBe("reward"); + subClient.Formatters.Count.ShouldBe(httpClient.Formatters.Count); // todo: check middleware count? - Assert.Equal(2, httpClientFactory.Count); + httpClientFactory.Count.ShouldBe(2); } [Fact] - public async void GraphQL_ShouldReturnContent() + public async Task GraphQL_ShouldReturnContent() { const string query = "{hero {name,title}}"; const string operationName = "heroGet"; @@ -136,8 +136,8 @@ public async void GraphQL_ShouldReturnContent() var response = await httpClient.CreateGqlRequest(query, operationName) .ReturnAsGqlResponse(); - Assert.True(response.IsSuccessStatusCode); - Assert.NotNull(response.Data); - Assert.Equal("Lord of Sin", response.Data.Title); + response.IsSuccessStatusCode.ShouldBeTrue(); + response.Data.ShouldNotBeNull(); + response.Data.Title.ShouldBe("Lord of Sin"); } } \ No newline at end of file diff --git a/test/FluentHttpHeadersTest.cs b/test/FluentHttpHeadersTest.cs index 59b33ca..5d140cd 100644 --- a/test/FluentHttpHeadersTest.cs +++ b/test/FluentHttpHeadersTest.cs @@ -11,15 +11,16 @@ public void ToDictionary_ShouldBeConverted() { var headers = new FluentHttpHeaders { - {HeaderTypes.Accept, new[] {"json", "msgpack"}} + {HeaderTypes.Accept, ["json", "msgpack"] } }; var dictionary = headers.ToDictionary(); var result = dictionary.GetValueOrDefault(HeaderTypes.Accept); - Assert.Equal(2, result.Length); - Assert.Equal("json", result[0]); - Assert.Equal("msgpack", result[1]); + result.ShouldNotBeNull(); + result.Length.ShouldBe(2); + result[0].ShouldBe("json"); + result[1].ShouldBe("msgpack"); } [Fact] @@ -27,30 +28,24 @@ public void ShouldBeSerializable() { var headers = new FluentHttpHeaders { - {HeaderTypes.Authorization, new[]{"the-xx"}}, - {HeaderTypes.Accept, new[] {"json", "msgpack"}}, - {HeaderTypes.XForwardedHost, new[] {"sketch7.com"}}, + {HeaderTypes.Authorization, ["the-xx"] }, + {HeaderTypes.Accept, ["json", "msgpack"] }, + {HeaderTypes.XForwardedHost, ["sketch7.com"] }, }; var headersJson = JsonConvert.SerializeObject(headers); var headersCopied = JsonConvert.DeserializeObject(headersJson); - Assert.Collection(headersCopied, x => - { - Assert.Equal(HeaderTypes.Authorization, x.Key); - Assert.Equal("the-xx", x.Value[0]); - }, - x => - { - Assert.Equal(HeaderTypes.Accept, x.Key); - Assert.Equal("json,msgpack", string.Join(",", x.Value)); - }, - x => - { - Assert.Equal(HeaderTypes.XForwardedHost, x.Key); - Assert.Equal("sketch7.com", x.Value[0]); - }); - Assert.Equal("the-xx", headersCopied.Authorization); + headersCopied.ShouldNotBeNull(); + headersCopied.Count.ShouldBe(3); + var items = headersCopied.ToList(); + items[0].Key.ShouldBe(HeaderTypes.Authorization); + items[0].Value![0].ShouldBe("the-xx"); + items[1].Key.ShouldBe(HeaderTypes.Accept); + string.Join(",", items[1].Value!).ShouldBe("json,msgpack"); + items[2].Key.ShouldBe(HeaderTypes.XForwardedHost); + items[2].Value![0].ShouldBe("sketch7.com"); + headersCopied.Authorization.ShouldBe("the-xx"); } [Fact] @@ -58,7 +53,7 @@ public void HeaderBuilderExtMethod_ShouldBeAvailable() { var headers = new FluentHttpHeaders() .WithUserAgent("leoric"); - Assert.Equal("leoric", headers.UserAgent); + headers.UserAgent.ShouldBe("leoric"); } } @@ -67,10 +62,9 @@ public class FluentHttpHeaders_Add [Fact] public void ShouldAdd() { - var headers = new FluentHttpHeaders(); + var headers = new FluentHttpHeaders { { HeaderTypes.Authorization, "the-xx" } }; - headers.Add(HeaderTypes.Authorization, "the-xx"); - Assert.Equal("the-xx", headers.Authorization); + headers.Authorization.ShouldBe("the-xx"); } [Fact] @@ -80,7 +74,7 @@ public void ShouldAddWithStringValues() StringValues str = new[] { "the-xx", "supersecret" }; headers.Add(HeaderTypes.Authorization, str); - Assert.Equal("the-xx,supersecret", headers.Authorization); + headers.Authorization.ShouldBe("the-xx,supersecret"); } [Fact] @@ -88,9 +82,9 @@ public void ShouldAddEnumerable() { var headers = new FluentHttpHeaders { - { HeaderTypes.Accept, new[] { "json", "msgpack" } } + { HeaderTypes.Accept, ["json", "msgpack"] } }; - Assert.Equal("json,msgpack", headers.Accept); + ((string?)headers.Accept).ShouldBe("json,msgpack"); } [Fact] @@ -100,7 +94,7 @@ public void AddRangeSame_ShouldThrow() headers.AddRange(new Dictionary{ {HeaderTypes.Accept, new[] {"json", "msgpack"}} }); - Assert.Throws(() => headers.AddRange(new Dictionary{ + Should.Throw(() => headers.AddRange(new Dictionary{ {HeaderTypes.Accept, "xml"} })); } @@ -115,7 +109,7 @@ public void SetRangeSame_ShouldUpdateWithLatest() headers.SetRange(new Dictionary{ {HeaderTypes.Accept, "xml"} }); - Assert.Equal("xml", headers.Accept); + ((string?)headers.Accept).ShouldBe("xml"); } } @@ -129,7 +123,7 @@ public void ShouldRemoveExisting() { HeaderTypes.Authorization, "the-xx" } }; headers.Remove(HeaderTypes.Authorization); - Assert.Null(headers.Authorization); + headers.Authorization.ShouldBeNull(); } [Fact] @@ -137,7 +131,7 @@ public void ShouldNotThrowWhenRemovingNonExisting() { var headers = new FluentHttpHeaders(); headers.Remove(HeaderTypes.Authorization); - Assert.Null(headers.Authorization); + headers.Authorization.ShouldBeNull(); } } @@ -152,8 +146,8 @@ public void ShouldBeCaseInsensitive() }; var value = headers.GetValue("X-Custom"); var value2 = headers.GetValue("x-custom"); - Assert.Equal("the-xx", value); - Assert.Equal("the-xx", value2); + value.ShouldBe("the-xx"); + value2.ShouldBe("the-xx"); } } @@ -163,15 +157,15 @@ public class FluentHttpHeaders_Accessors public void GetNotExists_ShouldReturnNull() { var headers = new FluentHttpHeaders(); - Assert.Null(headers.UserAgent); + headers.UserAgent.ShouldBeNull(); } [Fact] public void GetExists_ShouldReturn() { var headers = new FluentHttpHeaders() - .Add(HeaderTypes.Accept, new[] { "json", "msgpack" }); - Assert.Equal("json,msgpack", headers.Accept); + .Add(HeaderTypes.Accept, ["json", "msgpack"]); + ((string?)headers.Accept).ShouldBe("json,msgpack"); } [Fact] @@ -181,7 +175,7 @@ public void SetNotExists_ShouldBeAdded() { Accept = new[] { "json", "msgpack" } }; - Assert.Equal("json,msgpack", headers.Accept); + ((string?)headers.Accept).ShouldBe("json,msgpack"); } [Fact] @@ -192,7 +186,7 @@ public void SetExists_ShouldBeUpdated() Accept = new[] { "json", "msgpack" } }; headers.Accept = "json"; - Assert.Equal("json", headers.Accept); + ((string?)headers.Accept).ShouldBe("json"); } } @@ -208,9 +202,9 @@ public void ShouldInitializeFromDictionaryOfString() }; var headers = new FluentHttpHeaders(headersMap); - Assert.Equal("the-xx", headers.Authorization); - Assert.Equal("json", headers.ContentType); - Assert.Equal(headersMap.Count, headers.Count); + headers.Authorization.ShouldBe("the-xx"); + headers.ContentType.ShouldBe("json"); + headers.Count.ShouldBe(headersMap.Count); } [Fact] @@ -223,9 +217,9 @@ public void ShouldInitializeFromDictionaryOfStringValues() }; var headers = new FluentHttpHeaders(headersMap); - Assert.Equal("the-xx", headers.Authorization); - Assert.Equal("json", headers.ContentType); - Assert.Equal(headersMap.Count, headers.Count); + headers.Authorization.ShouldBe("the-xx"); + headers.ContentType.ShouldBe("json"); + headers.Count.ShouldBe(headersMap.Count); } [Fact] @@ -233,14 +227,14 @@ public void ShouldInitializeFromDictionaryOfEnumerableString() { var headersMap = new Dictionary> { - {HeaderTypes.Accept, new[] {"json", "msgpack"} }, - {HeaderTypes.XForwardedFor, new[] {"192.168.1.1", "127.0.0.1"} }, + {HeaderTypes.Accept, ["json", "msgpack"] }, + {HeaderTypes.XForwardedFor, ["192.168.1.1", "127.0.0.1"] }, }; var headers = new FluentHttpHeaders(headersMap); - Assert.Equal("json,msgpack", headers.Accept); - Assert.Equal("192.168.1.1,127.0.0.1", headers.XForwardedFor); - Assert.Equal(headersMap.Count, headers.Count); + ((string?)headers.Accept).ShouldBe("json,msgpack"); + ((string?)headers.XForwardedFor).ShouldBe("192.168.1.1,127.0.0.1"); + headers.Count.ShouldBe(headersMap.Count); } [Fact] @@ -248,12 +242,12 @@ public void ShouldInitializeFromHttpHeaders() { var httpHeaders = new HttpRequestMessage().Headers; httpHeaders.Add(HeaderTypes.Authorization, "the-xx"); - httpHeaders.Add(HeaderTypes.XForwardedFor, new[] { "192.168.1.1", "127.0.0.1" }); + httpHeaders.Add(HeaderTypes.XForwardedFor, ["192.168.1.1", "127.0.0.1"]); var headers = new FluentHttpHeaders(httpHeaders); - Assert.Equal("the-xx", headers.Authorization); - Assert.Equal("192.168.1.1,127.0.0.1", headers.XForwardedFor); - Assert.Equal(2, headers.Count); + headers.Authorization.ShouldBe("the-xx"); + ((string?)headers.XForwardedFor).ShouldBe("192.168.1.1,127.0.0.1"); + headers.Count.ShouldBe(2); } } @@ -269,7 +263,7 @@ public void ShouldHashSimple() }; var hash = headers.ToHashString(); - Assert.Equal("Authorization=the-xx&Content-Type=json", hash); + hash.ShouldBe("Authorization=the-xx&Content-Type=json"); } [Fact] @@ -278,11 +272,11 @@ public void ShouldHashWithEnumerable() var headers = new FluentHttpHeaders { {HeaderTypes.Authorization, "the-xx"}, - {HeaderTypes.Accept, new[] {"json", "msgpack"}} + {HeaderTypes.Accept, ["json", "msgpack"] } }; var hash = headers.ToHashString(); - Assert.Equal("Authorization=the-xx&Accept=json,msgpack", hash); + hash.ShouldBe("Authorization=the-xx&Accept=json,msgpack"); } [Fact] @@ -291,12 +285,12 @@ public void ShouldFilterWithHashingFilter() var headers = new FluentHttpHeaders { {HeaderTypes.Authorization, "the-xx"}, - {HeaderTypes.Accept, new[] {"json", "msgpack"}}, + {HeaderTypes.Accept, ["json", "msgpack"] }, {HeaderTypes.XForwardedHost, "sketch7.com"}, } .WithOptions(opts => opts.WithHashingExclude(pair => pair.Key == HeaderTypes.Authorization)); var hash = headers.ToHashString(); - Assert.Equal("Accept=json,msgpack&X-Forwarded-Host=sketch7.com", hash); + hash.ShouldBe("Accept=json,msgpack&X-Forwarded-Host=sketch7.com"); } } \ No newline at end of file diff --git a/test/FluentHttpRequestBuilderTest.cs b/test/FluentHttpRequestBuilderTest.cs index 5e5f40a..066d7d2 100644 --- a/test/FluentHttpRequestBuilderTest.cs +++ b/test/FluentHttpRequestBuilderTest.cs @@ -33,7 +33,7 @@ public async Task WithoutUrl_ShouldUseBaseUrl() .CreateRequest() .ReturnAsResponse(); - Assert.Equal("https://sketch7.com/api/heroes/", response.Message.RequestMessage.RequestUri.ToString()); + response.Message.RequestMessage?.RequestUri?.ToString().ShouldBe("https://sketch7.com/api/heroes/"); } [Fact] @@ -42,7 +42,7 @@ public void WithBaseUrlEmpty_ShouldBeValid() var request = GetNewRequestBuilder(configureClient: c => c.WithBaseUrl(string.Empty)) .Build(); - Assert.Equal("/api", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/api"); } [Fact] @@ -56,7 +56,7 @@ public async Task WithBaseUrlTrailingSlash_ShouldNotIncludeTrailingSlash() .CreateRequest() .ReturnAsResponse(); - Assert.Equal("https://sketch7.com/oauth/token", response.Message.RequestMessage.RequestUri.ToString()); + response.Message.RequestMessage?.RequestUri?.ToString().ShouldBe("https://sketch7.com/oauth/token"); } [Fact] @@ -72,7 +72,7 @@ public async Task WithBaseUrlTrailingSlash_SubClient_ShouldNotIncludeTrailingSla .CreateRequest() .ReturnAsResponse(); - Assert.Equal("https://sketch7.com/oauth/token", response.Message.RequestMessage.RequestUri.ToString()); + response.Message.RequestMessage?.RequestUri?.ToString().ShouldBe("https://sketch7.com/oauth/token"); } [Fact] @@ -88,7 +88,7 @@ public async Task SubClientWithoutUrl_ShouldUseBaseUrl() .CreateRequest() .ReturnAsResponse(); - Assert.Equal("https://sketch7.com/api/heroes/v1/", response.Message.RequestMessage.RequestUri.ToString()); + response.Message.RequestMessage?.RequestUri?.ToString().ShouldBe("https://sketch7.com/api/heroes/v1/"); } [Fact] @@ -102,7 +102,7 @@ public async Task WithoutUrlAndWithQueryString_ShouldInterpolateQueryString() .WithQueryParams(new { Language = "en" }) .ReturnAsResponse(); - Assert.Equal("https://sketch7.com/api/heroes/?language=en", response.Message.RequestMessage.RequestUri.ToString()); + response.Message.RequestMessage?.RequestUri?.ToString().ShouldBe("https://sketch7.com/api/heroes/?language=en"); } } @@ -118,14 +118,14 @@ public void ShouldInterpolate() Hero = "azmodan" }).Build(); - Assert.Equal("en/heroes/azmodan", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("en/heroes/azmodan"); } [Fact] public void NullValue_ShouldThrow() { var requestBuilder = GetNewRequestBuilder(); - Assert.Throws("args", () => requestBuilder.WithUri("{Language}/heroes", new + Should.Throw(() => requestBuilder.WithUri("{Language}/heroes", new { Language = (string?)null })); @@ -145,7 +145,7 @@ public void AddQuery() Filter = "all" }).Build(); - Assert.Equal("/org/sketch7?page=1&filter=all", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7?page=1&filter=all"); } [Fact] @@ -157,17 +157,17 @@ public void AddWithoutAsModel() { Page = 1, Filter = "all" - }, c => c.WithKeyFormatter(null)).Build(); + }, c => c.WithKeyFormatter(null!)).Build(); - Assert.Equal("/org/sketch7?Page=1&Filter=all", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7?Page=1&Filter=all"); } [Theory] [InlineData(null)] [InlineData("")] - public void NullOrEmptyValue_RemainAsIs(string data) + public void NullOrEmptyValue_RemainAsIs(string? data) { - string filter = data; + string? filter = data; var builder = GetNewRequestBuilder(); var request = builder.WithUri("/org/sketch7") .WithQueryParams(new @@ -175,7 +175,7 @@ public void NullOrEmptyValue_RemainAsIs(string data) filter, }).Build(); - Assert.Equal("/org/sketch7", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7"); } [Fact] @@ -190,7 +190,7 @@ public void NullValue_Omitted() Page = 1 }).Build(); - Assert.Equal("/org/sketch7?page=1", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7?page=1"); } [Fact] @@ -204,7 +204,7 @@ public void AppendQuery() Filter = "all" }).Build(); - Assert.Equal("/org/sketch7?hero=rex&page=1&filter=all", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7?hero=rex&page=1&filter=all"); } [Fact] @@ -215,7 +215,7 @@ public void EmptyObject_RemainAsIs() .WithQueryParams(new { }) .Build(); - Assert.Equal("/org/sketch7", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7"); } [Fact] @@ -233,7 +233,7 @@ public void IgnoredAndInaccessibleProps_ShouldBeStripped() .WithQueryParams(qsParams) .Build(); - Assert.Equal("/org/sketch7?role=assassin&class=rogue", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7?role=assassin&class=rogue"); } [Fact] @@ -247,7 +247,7 @@ public void CollectionQueryString() Powers = new List { 1337, 2337 } }).Build(); - Assert.Equal("/org/sketch7/heroes?roles=warrior&roles=assassin&powers=1337&powers=2337", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7/heroes?roles=warrior&roles=assassin&powers=1337&powers=2337"); } [Fact] @@ -261,7 +261,7 @@ public void CollectionQueryString_CommaSeparated() }, opts => opts.CollectionMode = QueryStringCollectionMode.CommaSeparated ).Build(); - Assert.Equal("/org/sketch7/heroes?roles=warrior,assassin", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7/heroes?roles=warrior,assassin"); } [Fact] @@ -276,7 +276,7 @@ public void InheritOptions_AsDefaults() }, opts => opts.WithKeyFormatter(s => s.ToUpper()) ).Build(); - Assert.Equal("/org/sketch7/heroes?ROLES=warrior,assassin", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7/heroes?ROLES=warrior,assassin"); } [Fact] @@ -288,7 +288,7 @@ public void WithQueryParamsOptions_MultipleCallsShouldKeepConfiguring() .WithQueryParamsOptions(opts => opts.WithKeyFormatter(s => s.ToUpper())) .WithQueryParamsOptions(opts => opts.WithValuePerKeyFormatter(new Dictionary>() { - ["POWERS"] = val => (val is string valStr) ? valStr.ToUpper() : null! + ["POWERS"] = val => (val is string valStr) ? valStr.ToUpper() : null! })) .WithQueryParams(new { @@ -297,7 +297,7 @@ public void WithQueryParamsOptions_MultipleCallsShouldKeepConfiguring() } ).Build(); - Assert.Equal("/org/sketch7/heroes?ROLES=warrior,assassin&POWERS=MEDIUM,HIGH", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7/heroes?ROLES=warrior,assassin&POWERS=MEDIUM,HIGH"); } [Fact] @@ -319,7 +319,7 @@ public void WithQueryParamsOptions_ValuePerKeyTransform() } ).Build(); - Assert.Equal("/org/sketch7/heroes?role=warrior&power=MEDIUM&dateTime=2025-01-01T00:00:00Z", request.Uri.ToString()); + request.Uri?.ToString().ShouldBe("/org/sketch7/heroes?role=warrior&power=MEDIUM&dateTime=2025-01-01T00:00:00Z"); } } @@ -329,7 +329,7 @@ public class RequestBuilder_BuildValidation public void ThrowsErrorWhenMethodNotSpecified() { var builder = GetNewRequestBuilder(); - Assert.Throws(() => builder.WithMethod(null).WithUri("/org").Build()); + Should.Throw(() => builder.WithMethod(null!).WithUri("/org").Build()); } [Fact] @@ -338,7 +338,7 @@ public void ThrowsErrorWhenGetAndHasBodySpecified() var requestBuilder = GetNewRequestBuilder() .AsGet() .WithBody(new { Name = "Kaboom!" }); - Assert.Throws(() => requestBuilder.Build()); + Should.Throw(() => requestBuilder.Build()); } } @@ -354,8 +354,8 @@ public void AddHeader() var request = builder.Build(); var header = request.Headers.GetValues("chiko").FirstOrDefault(); - Assert.NotNull(header); - Assert.Equal("hex", header); + header.ShouldNotBeNull(); + header.ShouldBe("hex"); } [Fact] @@ -363,17 +363,13 @@ public void AddHeaderStringValues() { var builder = GetNewRequestBuilder() .WithUri("/org/sketch7") - .WithHeader("locale", new StringValues(new[] { "mt", "en" })) + .WithHeader("locale", new StringValues(["mt", "en"])) ; var request = builder.Build(); var headers = request.Headers.GetValues("locale"); - Assert.NotNull(headers); - Assert.Collection( - headers, - x => Assert.Equal("mt", x), - x => Assert.Equal("en", x) - ); + headers.ShouldNotBeNull(); + headers.ShouldBe(["mt", "en"]); } [Fact] @@ -387,8 +383,8 @@ public void AddAlreadyExistsHeader_ShouldReplace() var request = builder.Build(); var header = request.Headers.GetValues("chiko").FirstOrDefault(); - Assert.NotNull(header); - Assert.Equal("hexII", header); + header.ShouldNotBeNull(); + header.ShouldBe("hexII"); } [Fact] @@ -406,10 +402,10 @@ public void AddHeaders() var chikoHeader = request.Headers.GetValues("chiko").FirstOrDefault(); var localeHeader = request.Headers.GetValues("locale").FirstOrDefault(); - Assert.NotNull(chikoHeader); - Assert.Equal("hexII", chikoHeader); - Assert.NotNull(localeHeader); - Assert.Equal("mt-MT", localeHeader); + chikoHeader.ShouldNotBeNull(); + chikoHeader.ShouldBe("hexII"); + localeHeader.ShouldNotBeNull(); + localeHeader.ShouldBe("mt-MT"); } [Fact] @@ -427,10 +423,10 @@ public void AddHeadersStringValues() var chikoHeader = request.Headers.GetValues("chiko").FirstOrDefault(); var localeHeader = request.Headers.GetValues("locale").FirstOrDefault(); - Assert.NotNull(chikoHeader); - Assert.Equal("hexII", chikoHeader); - Assert.NotNull(localeHeader); - Assert.Equal("mt-MT", localeHeader); + chikoHeader.ShouldNotBeNull(); + chikoHeader.ShouldBe("hexII"); + localeHeader.ShouldNotBeNull(); + localeHeader.ShouldBe("mt-MT"); } [Fact] @@ -443,8 +439,8 @@ public void WithUserAgent() var request = builder.Build(); var userAgentHeader = request.Headers.GetValues(HeaderTypes.UserAgent).FirstOrDefault(); - Assert.NotNull(userAgentHeader); - Assert.Equal("fluently", userAgentHeader); + userAgentHeader.ShouldNotBeNull(); + userAgentHeader.ShouldBe("fluently"); } [Fact] @@ -458,15 +454,15 @@ public void WithUserAgent_WeirdButValid() var request = builder.Build(); var userAgentHeader = request.Headers.GetValues(HeaderTypes.UserAgent).FirstOrDefault(); - Assert.NotNull(userAgentHeader); - Assert.Equal(userAgent, userAgentHeader); + userAgentHeader.ShouldNotBeNull(); + userAgentHeader.ShouldBe(userAgent); } } public class RequestBuilder_Return { [Fact] - public async void ReturnAsString() + public async Task ReturnAsString() { var mockResponse = "{ 'name': 'Azmodan' }"; var mockHttp = new MockHttpMessageHandler(); @@ -482,15 +478,15 @@ public async void ReturnAsString() var response = await httpClient.CreateRequest("/api/heroes/azmodan") .ReturnAsString(); - Assert.NotNull(response); - Assert.Equal(mockResponse, response); + response.ShouldNotBeNull(); + response.ShouldBe(mockResponse); } } public class RequestBuilder_PostWithBody { [Fact] - public async void ReturnAsTypedObject() + public async Task ReturnAsTypedObject() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/heroes/azmodan") @@ -512,15 +508,15 @@ public async void ReturnAsTypedObject() .ReturnAsResponse(); response.EnsureSuccessStatusCode(); - Assert.NotNull(response); - Assert.Equal("Lord of Sin", response.Data!.Title); + response.ShouldNotBeNull(); + response.Data!.Title.ShouldBe("Lord of Sin"); } } public class RequestBuilder_ReturnMulti { [Fact] - public async void ShouldSendTwoRequests() + public async Task ShouldSendTwoRequests() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/heroes/azmodan") @@ -545,12 +541,12 @@ public async void ShouldSendTwoRequests() var response2 = await requestBuilder.ReturnAsResponse(); response1.EnsureSuccessStatusCode(); - Assert.NotNull(response1); - Assert.Equal("Lord of Sin", response1.Data!.Title); + response1.ShouldNotBeNull(); + response1.Data!.Title.ShouldBe("Lord of Sin"); response2.EnsureSuccessStatusCode(); - Assert.NotNull(response2); - Assert.Equal("Lord of Sin", response2.Data!.Title); + response2.ShouldNotBeNull(); + response2.Data!.Title.ShouldBe("Lord of Sin"); } } @@ -577,7 +573,7 @@ public void ShouldBeEqual() var request1Hash = request1.Build().GetHash(); var request2Hash = request2.Build().GetHash(); - Assert.Equal(request1Hash, request2Hash); + request1Hash.ShouldBe(request2Hash); } [Fact] @@ -594,14 +590,14 @@ public void ShouldHandleHeaders() var requestHashWithHeaders = requestWithHeaders.Build().GetHash(); var noTokenRequestHash = requestNoHeaders.Build().GetHash(); - Assert.NotEqual(requestHashWithHeaders, noTokenRequestHash); + requestHashWithHeaders.ShouldNotBe(noTokenRequestHash); const string requestHashWithHeadersAssert = "method=GET;url=http://local.sketch7.io:5000/api/heroes/azmodan;headers=Accept=application/json,text/json,application/xml,text/xml,application/x-www-form-urlencoded&User-Agent=fluently&locale=en-GB&X-SSV-VERSION=2019.02-2&Authorization=Bearer XXX&local=en-GB;content="; const string noHeadersRequestHashAssert = "method=GET;url=http://local.sketch7.io:5000/api/heroes/azmodan;headers=Accept=application/json,text/json,application/xml,text/xml,application/x-www-form-urlencoded&User-Agent=fluently&locale=en-GB&X-SSV-VERSION=2019.02-2;content="; - Assert.Equal(requestHashWithHeadersAssert, requestHashWithHeaders); - Assert.Equal(noHeadersRequestHashAssert, noTokenRequestHash); + requestHashWithHeaders.ShouldBe(requestHashWithHeadersAssert); + noTokenRequestHash.ShouldBe(noHeadersRequestHashAssert); } public class WithHashingOptions @@ -620,7 +616,7 @@ public void WithHeadersExclude_ShouldExclude() var hash = requestBuilder.Build().GetHash(); const string assertHash = "method=GET;url=https://sketch7.com/api/heroes/azmodan;headers=Accept=application/json,text/json,application/xml,text/xml,application/x-www-form-urlencoded&User-Agent=fluently&local=en-GB;content="; - Assert.Equal(assertHash, hash); + hash.ShouldBe(assertHash); } [Fact] @@ -636,7 +632,7 @@ public void WithHeadersExcludeByKey_ShouldExclude() const string assertHash = "method=GET;url=https://sketch7.com/api/heroes/azmodan;headers=User-Agent=fluently&local=en-GB;content="; - Assert.Equal(assertHash, hash); + hash.ShouldBe(assertHash); } [Fact] @@ -644,7 +640,7 @@ public void WithHeadersExcludeByKeys_ShouldExclude() { var requestBuilder = GetNewRequestBuilder() .WithRequestHashOptions(opts => - opts.WithHeadersExcludeByKeys(new[] { HeaderTypes.Accept, HeaderTypes.UserAgent })) + opts.WithHeadersExcludeByKeys([HeaderTypes.Accept, HeaderTypes.UserAgent])) .WithUri("/api/heroes/azmodan") .WithHeader("local", "en-GB") ; @@ -652,7 +648,7 @@ public void WithHeadersExcludeByKeys_ShouldExclude() var hash = requestBuilder.Build().GetHash(); const string assertHash = "method=GET;url=https://sketch7.com/api/heroes/azmodan;headers=local=en-GB;content="; - Assert.Equal(assertHash, hash); + hash.ShouldBe(assertHash); } [Fact] @@ -677,7 +673,7 @@ public void WithHeadersExclude_ShouldCombinedExclusions() var hash = requestBuilder.Build().GetHash(); - Assert.Equal("method=GET;url=https://sketch7.com/api/heroes/azmodan;headers=local=en-GB;content=", hash); + hash.ShouldBe("method=GET;url=https://sketch7.com/api/heroes/azmodan;headers=local=en-GB;content="); } [Fact] @@ -697,7 +693,7 @@ public void WithUri_ShouldRemoveQuerystringParam() var hash = requestBuilder.Build().GetHash(); const string assertHash = "method=GET;url=https://sketch7.com/api/heroes/azmodan;headers=Accept=application/json,text/json,application/xml,text/xml,application/x-www-form-urlencoded&User-Agent=fluently;content="; - Assert.Equal(assertHash, hash); + hash.ShouldBe(assertHash); } [Fact] @@ -712,7 +708,7 @@ public void WithUriQueryString_ShouldRemoveQuerystringParam() var hash = requestBuilder.Build().GetHash(); const string assertHash = "method=GET;url=https://sketch7.com/api/heroes/azmodan;headers=Accept=application/json,text/json,application/xml,text/xml,application/x-www-form-urlencoded&User-Agent=fluently;content="; - Assert.Equal(assertHash, hash); + hash.ShouldBe(assertHash); } [Fact] @@ -730,7 +726,7 @@ public void Body_ShouldBeIncluded() var requestHash = requestBuilder.Build().GetHash(); const string assertHash = "method=POST;url=https://sketch7.com/api/heroes/azmodan;headers=Accept=application/json,text/json,application/xml,text/xml,application/x-www-form-urlencoded&User-Agent=fluently;content={\"email\":\"chiko@sketch7.com\"}"; - Assert.Equal(assertHash, requestHash); + requestHash.ShouldBe(assertHash); } [Fact] @@ -749,7 +745,7 @@ public void WithBodyInvariant_BodyShouldBeExcluded() var hash = requestBuilder.Build().GetHash(); const string assertHash = "method=POST;url=https://sketch7.com/api/heroes/azmodan;headers=Accept=application/json,text/json,application/xml,text/xml,application/x-www-form-urlencoded&User-Agent=fluently;content="; - Assert.Equal(assertHash, hash); + hash.ShouldBe(assertHash); } } } \ No newline at end of file diff --git a/test/FluentlyHttpClient.Test.csproj b/test/FluentlyHttpClient.Test.csproj index a20bbcf..2858381 100644 --- a/test/FluentlyHttpClient.Test.csproj +++ b/test/FluentlyHttpClient.Test.csproj @@ -2,12 +2,18 @@ false + Exe + false $(MSBuildThisFileDirectory)..\test.runsettings + $(NoWarn);1591 - + + + buildTransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/GlobalUsings.cs b/test/GlobalUsings.cs index 119c346..affb24a 100644 --- a/test/GlobalUsings.cs +++ b/test/GlobalUsings.cs @@ -1,6 +1,7 @@ // Global using directives global using FluentlyHttpClient; +global using Shouldly; global using FluentlyHttpClient.GraphQL; global using FluentlyHttpClient.Test; global using RichardSzalay.MockHttp; diff --git a/test/HttpMiddlewareTest.cs b/test/HttpMiddlewareTest.cs index 9e137e3..9e38512 100644 --- a/test/HttpMiddlewareTest.cs +++ b/test/HttpMiddlewareTest.cs @@ -38,7 +38,7 @@ public async Task Invoke(FluentHttpMiddlewareContext context { foreach (var (key, value) in response.Items) { - response.Headers.Add(key.ToString(), value.ToString()); + response.Headers.Add(key?.ToString() ?? string.Empty, value?.ToString()); } } @@ -49,7 +49,7 @@ public async Task Invoke(FluentHttpMiddlewareContext context public class HttpMiddlewareTest { [Fact] - public async void ShouldHaveRequestItem() + public async Task ShouldHaveRequestItem() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When("https://sketch7.com/api/heroes/azmodan") @@ -70,12 +70,12 @@ public async void ShouldHaveRequestItem() response.Items.TryGetValue("request", out var requestItem); response.Items.TryGetValue("monster", out var monsterItem); - Assert.Equal("item", requestItem); - Assert.Equal("orsachiottolo", monsterItem); + requestItem.ShouldBe("item"); + monsterItem.ShouldBe("orsachiottolo"); } [Fact] - public async void ShouldHaveRequestItem_WhenRawRequestProps() + public async Task ShouldHaveRequestItem_WhenRawRequestProps() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When("https://sketch7.com/api/heroes/azmodan") @@ -90,19 +90,19 @@ public async void ShouldHaveRequestItem_WhenRawRequestProps() var httpClient = fluentHttpClientFactory.Get("sketch7"); var request = httpClient.CreateRequest("/api/heroes/azmodan").Build().Message; - request.Properties["monster"] = "orsachiottolo"; + request.Options.Set(new HttpRequestOptionsKey("monster"), "orsachiottolo"); var response = await httpClient.Send(request); response.Items.TryGetValue("request", out var requestItem); response.Items.TryGetValue("monster", out var monsterItem); - Assert.Equal("item", requestItem); - Assert.Equal("orsachiottolo", monsterItem); + requestItem.ShouldBe("item"); + monsterItem.ShouldBe("orsachiottolo"); } [Fact] - public async void RawClient_ShouldGoThroughMiddleware() + public async Task RawClient_ShouldGoThroughMiddleware() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When("https://sketch7.com/api/heroes/azmodan") @@ -117,14 +117,14 @@ public async void RawClient_ShouldGoThroughMiddleware() .Register(); var httpClient = fluentHttpClientFactory.Get("sketch7"); - var response = await httpClient.RawHttpClient.GetAsync("/api/heroes/azmodan"); + var response = await httpClient.RawHttpClient.GetAsync("/api/heroes/azmodan", TestContext.Current.CancellationToken); var brand = response.Headers.GetValues("X-Brand-Id"); - Assert.Equal("snorlax", brand.First()); + brand.First().ShouldBe("snorlax"); } [Fact] - public async void RawClient_ShouldGoThroughMiddlewarePreservingItems() + public async Task RawClient_ShouldGoThroughMiddlewarePreservingItems() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When("https://sketch7.com/api/heroes/azmodan") @@ -140,11 +140,11 @@ public async void RawClient_ShouldGoThroughMiddlewarePreservingItems() var httpClient = fluentHttpClientFactory.Get("sketch7"); - var response = await httpClient.RawHttpClient.GetAsync("/api/heroes/azmodan"); + var response = await httpClient.RawHttpClient.GetAsync("/api/heroes/azmodan", TestContext.Current.CancellationToken); var brand = response.Headers.GetValues("X-Brand-Id"); var monster = response.Headers.GetValues("monster"); - Assert.Equal("snorlax", brand.First()); - Assert.Equal("orsachiottolo", monster.First()); + brand.First().ShouldBe("snorlax"); + monster.First().ShouldBe("orsachiottolo"); } } \ No newline at end of file diff --git a/test/HttpResponseSerializerTest.cs b/test/HttpResponseSerializerTest.cs index 5a2a084..7ddeb47 100644 --- a/test/HttpResponseSerializerTest.cs +++ b/test/HttpResponseSerializerTest.cs @@ -6,7 +6,7 @@ namespace FluentlyHttpClient.Test; public class HttpResponseSerializerTest { //[Fact] - public async void ShouldBeSerialized() + public async Task ShouldBeSerialized() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When(HttpMethod.Post, "http://local.sketch7.io:5000/api/heroes/azmodan") @@ -32,6 +32,6 @@ public async void ShouldBeSerialized() Assert.Equal("Lord of Sin", hero.Title); Assert.Equal(HttpStatusCode.OK, response2.StatusCode); Assert.Equal(response.Headers.Count(), response2.Headers.Count()); - Assert.Equal(response.Message.RequestMessage.RequestUri.ToString(), response2.Message.RequestMessage.RequestUri.ToString()); + Assert.Equal(response.Message.RequestMessage?.RequestUri?.ToString(), response2.Message.RequestMessage?.RequestUri?.ToString()); } } \ No newline at end of file diff --git a/test/Integration/FileUploadIntegrationTest.cs b/test/Integration/FileUploadIntegrationTest.cs index 7967b9e..2be632d 100644 --- a/test/Integration/FileUploadIntegrationTest.cs +++ b/test/Integration/FileUploadIntegrationTest.cs @@ -40,10 +40,10 @@ public async Task MultipartForm_AddFile_Path() .WithBodyContent(multiForm) .ReturnAsResponse(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("animal-mustache.jpg", response.Data.FileName); - Assert.Equal("image/jpeg", response.Data.ContentType); - Assert.Equal(3.96875, response.Data.Size); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.FileName.ShouldBe("animal-mustache.jpg"); + response.Data!.ContentType.ShouldBe("image/jpeg"); + response.Data!.Size.ShouldBe(3.96875); } [Fact] @@ -64,10 +64,10 @@ public async Task MultipartForm_AddFile_FileStream() .WithBodyContent(multiForm) .ReturnAsResponse(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("animal-mustache.jpg", response.Data.FileName); - Assert.Equal("image/jpeg", response.Data.ContentType); - Assert.Equal(3.96875, response.Data.Size); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.FileName.ShouldBe("animal-mustache.jpg"); + response.Data!.ContentType.ShouldBe("image/jpeg"); + response.Data!.Size.ShouldBe(3.96875); } [Fact] @@ -89,9 +89,9 @@ public async Task MultipartForm_AddFile_Bytes() .WithBodyContent(multiForm) .ReturnAsResponse(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("animal-mustache.jpg", response.Data.FileName); - Assert.Equal("image/jpeg", response.Data.ContentType); - Assert.Equal(3.96875, response.Data.Size); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.FileName.ShouldBe("animal-mustache.jpg"); + response.Data!.ContentType.ShouldBe("image/jpeg"); + response.Data!.Size.ShouldBe(3.96875); } } \ No newline at end of file diff --git a/test/Integration/LoadTest.cs b/test/Integration/LoadTest.cs index 30ada9a..be35657 100644 --- a/test/Integration/LoadTest.cs +++ b/test/Integration/LoadTest.cs @@ -6,14 +6,14 @@ namespace FluentlyHttpClient.Test.Integration; public record MimirGqlSchema { - public List UniversesIndex { get; set; } + public required List UniversesIndex { get; init; } } public record UniverseModel { - public string Id { get; set; } - public string Key { get; set; } - public string Name { get; set; } + public required string Id { get; init; } + public required string Key { get; init; } + public required string Name { get; init; } } public class LoadTest @@ -32,7 +32,7 @@ private static IServiceProvider BuildContainer() [Fact] [Trait("Category", "e2e")] - public async void GqlHttp2Test() + public async Task GqlHttp2Test() { var socketsHandler = new SocketsHttpHandler { diff --git a/test/Integration/MessagePackIntegrationTest.cs b/test/Integration/MessagePackIntegrationTest.cs index 479b91e..fc3feb9 100644 --- a/test/Integration/MessagePackIntegrationTest.cs +++ b/test/Integration/MessagePackIntegrationTest.cs @@ -18,10 +18,10 @@ public async Task ShouldMakeRequest_Get() var response = await httpClient.CreateRequest("/api/heroes/azmodan") .ReturnAsResponse(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("azmodan", response.Data.Key); - Assert.Equal("Azmodan", response.Data.Name); - Assert.Equal("Lord of Sin", response.Data.Title); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.Key.ShouldBe("azmodan"); + response.Data!.Name.ShouldBe("Azmodan"); + response.Data!.Title.ShouldBe("Lord of Sin"); } [Fact] @@ -42,9 +42,9 @@ public async Task ShouldMakeRequest_Post() }) .ReturnAsResponse(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("valeera", response.Data.Key); - Assert.Equal("Valeera", response.Data.Name); - Assert.Equal("Shadow of the Uncrowned", response.Data.Title); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.Key.ShouldBe("valeera"); + response.Data!.Name.ShouldBe("Valeera"); + response.Data!.Title.ShouldBe("Shadow of the Uncrowned"); } } \ No newline at end of file diff --git a/test/Integration/ResponseCacheIntegrationTest.cs b/test/Integration/ResponseCacheIntegrationTest.cs index 1639e77..3df6239 100644 --- a/test/Integration/ResponseCacheIntegrationTest.cs +++ b/test/Integration/ResponseCacheIntegrationTest.cs @@ -35,23 +35,23 @@ public async Task ShouldMakeRequest_Memory_Get() var responseReason = response.ReasonPhrase; - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("azmodan", response.Data.Key); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.Key.ShouldBe("azmodan"); response = await httpClient.CreateRequest("/api/heroes/azmodan") .ReturnAsResponse(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("azmodan", response.Data.Key); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.Key.ShouldBe("azmodan"); response = await httpClient.CreateRequest("/api/heroes/azmodan") .ReturnAsResponse(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("azmodan", response.Data.Key); - Assert.Equal("Azmodan", response.Data.Name); - Assert.Equal("Lord of Sin", response.Data.Title); - Assert.Equal(responseReason, response.ReasonPhrase); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.Key.ShouldBe("azmodan"); + response.Data!.Name.ShouldBe("Azmodan"); + response.Data!.Title.ShouldBe("Lord of Sin"); + response.ReasonPhrase.ShouldBe(responseReason); } [Fact] @@ -80,24 +80,24 @@ public async Task ShouldMakeRequest_Remote_Get() var responseReason = response.ReasonPhrase; - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("azmodan", response.Data.Key); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.Key.ShouldBe("azmodan"); response = await httpClient.CreateRequest("/api/heroes/azmodan") .ReturnAsResponse(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("azmodan", response.Data.Key); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.Key.ShouldBe("azmodan"); response = await httpClient.CreateRequest("/api/heroes/azmodan") .ReturnAsResponse(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("azmodan", response.Data.Key); - Assert.Equal("Azmodan", response.Data.Name); - Assert.Equal("Lord of Sin", response.Data.Title); - Assert.Equal("Kestrel", response.Headers.Server.ToString()); - Assert.Equal(responseReason, response.ReasonPhrase); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.Data!.Key.ShouldBe("azmodan"); + response.Data!.Name.ShouldBe("Azmodan"); + response.Data!.Title.ShouldBe("Lord of Sin"); + response.Headers.Server.ToString().ShouldBe("Kestrel"); + response.ReasonPhrase.ShouldBe(responseReason); //Assert.Equal(HttpStatusCode.OK, response.Headers.); } diff --git a/test/Integration/SerilogIntegrationTest.cs b/test/Integration/SerilogIntegrationTest.cs index 85d69e6..7411a43 100644 --- a/test/Integration/SerilogIntegrationTest.cs +++ b/test/Integration/SerilogIntegrationTest.cs @@ -19,7 +19,7 @@ private static IServiceProvider BuildContainer() } [Fact] - public async void ShouldLogAll() + public async Task ShouldLogAll() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/heroes") @@ -55,7 +55,7 @@ public async void ShouldLogAll() }) .Return(); - Assert.NotNull(hero); - Assert.Equal("Azmodan", hero.Name); + hero.ShouldNotBeNull(); + hero.Name.ShouldBe("Azmodan"); } } \ No newline at end of file diff --git a/test/LoggingHttpMiddlewareTest.cs b/test/LoggingHttpMiddlewareTest.cs index dff20ec..7e43402 100644 --- a/test/LoggingHttpMiddlewareTest.cs +++ b/test/LoggingHttpMiddlewareTest.cs @@ -7,7 +7,7 @@ namespace Test; public class LoggingHttpMiddlewareTest { [Fact] - public async void RequestBodyWithoutContent_ShouldNotThrow() + public async Task RequestBodyWithoutContent_ShouldNotThrow() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When("https://sketch7.com/api/heroes/azmodan") @@ -26,12 +26,12 @@ public async void RequestBodyWithoutContent_ShouldNotThrow() var hero = await httpClient.Get("/api/heroes/azmodan"); - Assert.NotNull(hero); - Assert.Equal("Azmodan", hero.Name); + hero.ShouldNotBeNull(); + hero.Name.ShouldBe("Azmodan"); } [Fact] - public async void ResponseBodyWithoutContent_ShouldNotThrow() + public async Task ResponseBodyWithoutContent_ShouldNotThrow() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/heroes") @@ -57,12 +57,12 @@ public async void ResponseBodyWithoutContent_ShouldNotThrow() }) .ReturnAsResponse(); - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + response.ShouldNotBeNull(); + response.StatusCode.ShouldBe(HttpStatusCode.OK); } [Fact] - public async void UsingActionBasedConfiguration() + public async Task UsingActionBasedConfiguration() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When(HttpMethod.Post, "https://sketch7.com/api/heroes") @@ -88,8 +88,8 @@ public async void UsingActionBasedConfiguration() }) .ReturnAsResponse(); - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); + response.ShouldNotBeNull(); + response.StatusCode.ShouldBe(HttpStatusCode.OK); } [Fact] @@ -111,8 +111,9 @@ public void DefaultLoggingOptions_ShouldBeMerged() var options = request.GetLoggingOptions(loggerHttpMiddlewareOptions); - Assert.True(options.ShouldLogDetailedRequest); - Assert.True(options.ShouldLogDetailedResponse); + options.ShouldNotBeNull(); + options.ShouldLogDetailedRequest.ShouldBe(true); + options.ShouldLogDetailedResponse.ShouldBe(true); } [Fact] @@ -139,8 +140,9 @@ public void RequestSpecificOptions_ShouldOverride() var options = request.GetLoggingOptions(loggerHttpMiddlewareOptions); - Assert.True(options.ShouldLogDetailedRequest); - Assert.True(options.ShouldLogDetailedResponse); + options.ShouldNotBeNull(); + options.ShouldLogDetailedRequest.ShouldBe(true); + options.ShouldLogDetailedResponse.ShouldBe(true); } [Fact] @@ -167,7 +169,8 @@ public void RequestSpecificOptions_ActionBased() var options = request.GetLoggingOptions(loggerHttpMiddlewareOptions); - Assert.True(options.ShouldLogDetailedRequest); - Assert.True(options.ShouldLogDetailedResponse); + options.ShouldNotBeNull(); + options.ShouldLogDetailedRequest.ShouldBe(true); + options.ShouldLogDetailedResponse.ShouldBe(true); } } \ No newline at end of file diff --git a/test/TimerHttpMiddlewareTest.cs b/test/TimerHttpMiddlewareTest.cs index 154d204..9958749 100644 --- a/test/TimerHttpMiddlewareTest.cs +++ b/test/TimerHttpMiddlewareTest.cs @@ -5,7 +5,7 @@ namespace Test; public class TimerHttpMiddlewareTest { [Fact] - public async void ShouldHaveTimeTaken() + public async Task ShouldHaveTimeTaken() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When("https://sketch7.com/api/heroes/azmodan") @@ -20,13 +20,13 @@ public async void ShouldHaveTimeTaken() var response = await httpClient.CreateRequest("/api/heroes/azmodan") .ReturnAsResponse(); - Assert.NotNull(response.Data); - Assert.Equal("Azmodan", response.Data.Name); - Assert.NotEqual(TimeSpan.Zero, response.GetTimeTaken()); + response.Data.ShouldNotBeNull(); + response.Data.Name.ShouldBe("Azmodan"); + response.GetTimeTaken().ShouldNotBe(TimeSpan.Zero); } [Fact] - public async void ShouldWorkWithRequestThresholdOption() + public async Task ShouldWorkWithRequestThresholdOption() { var mockHttp = new MockHttpMessageHandler(); mockHttp.When("https://sketch7.com/api/heroes/azmodan") @@ -42,8 +42,8 @@ public async void ShouldWorkWithRequestThresholdOption() .WithTimerWarnThreshold(TimeSpan.FromSeconds(1)) .ReturnAsResponse(); - Assert.NotNull(response.Data); - Assert.NotEqual(TimeSpan.Zero, response.GetTimeTaken()); + response.Data.ShouldNotBeNull(); + response.GetTimeTaken().ShouldNotBe(TimeSpan.Zero); } [Fact] @@ -56,6 +56,6 @@ public void ThrowsWhenWarnThresholdIsZero() x.WarnThreshold = TimeSpan.Zero; }); - Assert.Throws(() => httpClientBuilder.Build()); + Should.Throw(() => httpClientBuilder.Build()); } } \ No newline at end of file diff --git a/test/Utils/QueryStringUtilsTest.cs b/test/Utils/QueryStringUtilsTest.cs index e140bca..38c1543 100644 --- a/test/Utils/QueryStringUtilsTest.cs +++ b/test/Utils/QueryStringUtilsTest.cs @@ -96,13 +96,11 @@ public void ShouldFormatCollectionsWithFormatter() // deprecated: remove var result = queryCollection.ToQueryString(opts => { opts.CollectionMode = QueryStringCollectionMode.CommaSeparated; -#pragma warning disable 618 opts.WithCollectionItemFormatter(valueObj => -#pragma warning restore 618 { if (valueObj is Enum @enum) return @enum.GetEnumDescription(); - return valueObj.ToString(); + return valueObj.ToString() ?? string.Empty; }); }); @@ -126,7 +124,7 @@ public void ShouldFormatValueWithFormatter() { if (valueObj is Enum @enum) return @enum.GetEnumDescription(); - return valueObj.ToString(); + return valueObj.ToString() ?? string.Empty; }); });