Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public sealed record RecordContext
var models = await fluentHttp
.AppendPathSegment("example")
.GetJsonAsync(RecordContext.Default.RecordArray, ct);

// When an API may return non-JSON values (e.g. `""`) this method can be used in order to ignore parsing exceptions
fluentHttp.GetJsonOrDefaultAsync(RecordContext.Default.RecordArray, ct);
```

#### PostJsonAsync
Expand All @@ -69,6 +72,9 @@ var req = new Request
var response = await fluentHttp
.AppendPathSegment("example")
.PostJsonAsync(req, RequestContext.Default.Request, ResponseContext.Default.Response, ct);

// When an API may return non-JSON values (e.g. `""`) this method can be used in order to ignore parsing exceptions
fluentHttp.PostJsonOrDefaultAsync(req, RequestContext.Default.Request, ResponseContext.Default.Response, ct);
```

## Fluent extensions
Expand Down
34 changes: 32 additions & 2 deletions src/DefaultFluentHttp.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -30,6 +29,14 @@ public async Task<TResult> GetJsonAsync<TResult>(HttpCallOptions options, JsonTy
.ConfigureAwait(false);
}

public async Task<TResult?> GetJsonOrDefaultAsync<TResult>(HttpCallOptions options, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default)
{
using var req = CreateRequest(HttpMethod.Get, options);

return await GetResponseOrDefaultAsync(req, resultTypeInfo, ct)
.ConfigureAwait(false);
}

public async Task<TResult> PostJsonAsync<TSource, TResult>(TSource source, HttpCallOptions options, JsonTypeInfo<TSource>? sourceTypeInfo = null, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default)
{
using var req = await CreateRequestAsync(HttpMethod.Post, options, source, sourceTypeInfo, ct)
Expand All @@ -39,6 +46,15 @@ public async Task<TResult> PostJsonAsync<TSource, TResult>(TSource source, HttpC
.ConfigureAwait(false);
}

public async Task<TResult?> PostJsonOrDefaultAsync<TSource, TResult>(TSource source, HttpCallOptions options, JsonTypeInfo<TSource>? sourceTypeInfo = null, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default)
{
using var req = await CreateRequestAsync(HttpMethod.Post, options, source, sourceTypeInfo, ct)
.ConfigureAwait(false);

return await GetResponseOrDefaultAsync(req, resultTypeInfo, ct)
.ConfigureAwait(false);
}

private HttpRequestMessage CreateRequest(HttpMethod method, HttpCallOptions options)
{
var uri = options.CreateUri();
Expand Down Expand Up @@ -139,4 +155,18 @@ private async Task<T> GetResponseAsync<T>(HttpRequestMessage req, JsonTypeInfo<T

throw new HttpCallException(res.StatusCode, errorContent);
}
}

private async Task<T?> GetResponseOrDefaultAsync<T>(HttpRequestMessage req, JsonTypeInfo<T>? jsonTypeInfo, CancellationToken ct)
{
try
{
return await GetResponseAsync(req, jsonTypeInfo, ct)
.ConfigureAwait(false);
}
catch (JsonException e)
{
_logger.LogWarning("Cannot deserialize JSON: {Message}", e.Message);
return default;
}
}
}
2 changes: 1 addition & 1 deletion src/Exceptions/HttpCallException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ public HttpCallException(HttpStatusCode statusCode, string content)
public HttpStatusCode StatusCode { get; }

public string Content { get; }
}
}
2 changes: 1 addition & 1 deletion src/Fluent/Extensions/FluentHttpEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ public static IFluentHttpWithOptions WithBasicAuth(this IFluentHttp @this, strin
var (header, value) = AuthUtils.BasicAuth(username, password);
return @this.WithHeader(header, value);
}
}
}
10 changes: 9 additions & 1 deletion src/Fluent/Extensions/FluentHttpWithOptionsEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ public static class FluentHttpWithOptionsEx
public static Task<TResult> GetJsonAsync<TResult>(this IFluentHttpWithOptions @this, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default) =>
@this.Http.GetJsonAsync(@this.Options, resultTypeInfo, ct);

/// <inheritdoc cref="IFluentHttp.GetJsonOrDefaultAsync{T}"/>
public static Task<TResult?> GetJsonOrDefaultAsync<TResult>(this IFluentHttpWithOptions @this, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default) =>
@this.Http.GetJsonOrDefaultAsync(@this.Options, resultTypeInfo, ct);

/// <inheritdoc cref="IFluentHttp.PostJsonAsync{T,T}"/>
public static Task<TResult> PostJsonAsync<TSource, TResult>(this IFluentHttpWithOptions @this, TSource source, JsonTypeInfo<TSource>? sourceTypeInfo = null, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default) =>
@this.Http.PostJsonAsync(source, @this.Options, sourceTypeInfo, resultTypeInfo, ct);

/// <inheritdoc cref="IFluentHttp.PostJsonOrDefaultAsync{T,T}"/>
public static Task<TResult?> PostJsonOrDefaultAsync<TSource, TResult>(this IFluentHttpWithOptions @this, TSource source, JsonTypeInfo<TSource>? sourceTypeInfo = null, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default) =>
@this.Http.PostJsonOrDefaultAsync(source, @this.Options, sourceTypeInfo, resultTypeInfo, ct);

/// <inheritdoc cref="FluentHttpEx.AppendPathSegment"/>
public static IFluentHttpWithOptions AppendPathSegment(this IFluentHttpWithOptions @this, string pathSegment)
{
Expand Down Expand Up @@ -39,4 +47,4 @@ public static IFluentHttpWithOptions WithBasicAuth(this IFluentHttpWithOptions @
var (header, value) = AuthUtils.BasicAuth(username, password);
return @this.WithHeader(header, value);
}
}
}
2 changes: 1 addition & 1 deletion src/Fluent/FluentHttpWithOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ HttpCallOptions IFluentHttpWithOptions.Options
get => Options;
set => Options = value;
}
}
}
2 changes: 1 addition & 1 deletion src/Fluent/IFluentHttpWithOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ public interface IFluentHttpWithOptions
internal IFluentHttp Http { get; }

internal HttpCallOptions Options { get; set; }
}
}
16 changes: 15 additions & 1 deletion src/IFluentHttp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,22 @@ namespace MyNihongo.FluentHttp;
public interface IFluentHttp
{
/// <exception cref="HttpCallException"></exception>
/// <exception cref="JsonException"></exception>
Task<TResult> GetJsonAsync<TResult>(HttpCallOptions options, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default);

/// <summary>
/// Attempts to GET a JSON value. Returns <see langword="null"/> in case the returned model is not a valid JSON.
/// </summary>
/// <exception cref="HttpCallException"></exception>
Task<TResult?> GetJsonOrDefaultAsync<TResult>(HttpCallOptions options, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default);

/// <exception cref="HttpCallException"></exception>
/// <exception cref="JsonException"></exception>
Task<TResult> PostJsonAsync<TSource, TResult>(TSource source, HttpCallOptions options, JsonTypeInfo<TSource>? sourceTypeInfo = null, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default);
}

/// <summary>
/// Attempts to POST a JSON value. Returns <see langword="null"/> in case the returned model is not a valid JSON.
/// </summary>
/// <exception cref="HttpCallException"></exception>
Task<TResult?> PostJsonOrDefaultAsync<TSource, TResult>(TSource source, HttpCallOptions options, JsonTypeInfo<TSource>? sourceTypeInfo = null, JsonTypeInfo<TResult>? resultTypeInfo = null, CancellationToken ct = default);
}
2 changes: 1 addition & 1 deletion src/Models/HeaderModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ public void Deconstruct(out string key, out string value)
key = Key;
value = Value;
}
}
}
2 changes: 1 addition & 1 deletion src/Models/HttpCallOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ public sealed record HttpCallOptions
public List<string> PathSegments { get; } = new();

public Dictionary<string, string> Headers { get; } = new();
}
}
2 changes: 1 addition & 1 deletion src/MyNihongo.FluentHttp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.0.3</Version>
<Version>1.0.4</Version>
<Authors>MyNihongo</Authors>
<Description>Fluent wrapper around IHttpClientFactory</Description>
<Copyright>Copyright © 2022 MyNihongo</Copyright>
Expand Down
2 changes: 1 addition & 1 deletion src/Resources/ConfigKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ internal static class ConfigKeys
BaseAddress = "BaseAddress",
UseNtlmAuthentication = "NtlmEnabled",
Timeout = "Timeout";
}
}
2 changes: 1 addition & 1 deletion src/Resources/Const.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ internal static class Const
{
public const string FactoryName = "default";
public const char UriSeparator = '/';
}
}
6 changes: 2 additions & 4 deletions src/Utils/AuthUtils.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Text;

namespace MyNihongo.FluentHttp;
namespace MyNihongo.FluentHttp;

internal static class AuthUtils
{
Expand All @@ -11,4 +9,4 @@ public static HeaderModel BasicAuth(string username, string password)
var value = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
return new HeaderModel(AuthHeaderKey, $"Basic {value}");
}
}
}
2 changes: 1 addition & 1 deletion src/Utils/Extensions/ConfigurationEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ public static string CreateAbsoluteUrl(this IConfiguration @this, Uri relative)

return uri.AbsoluteUri;
}
}
}
2 changes: 1 addition & 1 deletion src/Utils/Extensions/HttpCallOptionsEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ internal static class HttpCallOptionsEx
{
public static Uri CreateUri(this HttpCallOptions @this) =>
new(@this.PathSegments.Join(Const.UriSeparator), UriKind.Relative);
}
}
5 changes: 2 additions & 3 deletions src/Utils/Extensions/ObjectEx.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Serialization.Metadata;

namespace MyNihongo.FluentHttp;

Expand All @@ -23,4 +22,4 @@ public static async Task<Stream> SerializeAsync<T>(this T @this, JsonTypeInfo<T>
stream.Position = 0;
return stream;
}
}
}
5 changes: 2 additions & 3 deletions src/Utils/Extensions/StreamEx.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Serialization.Metadata;

namespace MyNihongo.FluentHttp;

Expand All @@ -21,4 +20,4 @@ public static async ValueTask<T> DeserializeAsync<T>(this Stream @this, JsonType

return await valueTask.ConfigureAwait(false) ?? throw new NullReferenceException("Cannot deserialize");
}
}
}
6 changes: 2 additions & 4 deletions src/Utils/Extensions/StringEx.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Extensions.ObjectPool;

namespace MyNihongo.FluentHttp;
Expand All @@ -26,4 +24,4 @@ public static T Deserialize<T>(this string @this, JsonTypeInfo<T>? jsonTypeInfo

return obj ?? throw new NullReferenceException("Cannot deserialize");
}
}
}
2 changes: 1 addition & 1 deletion src/Utils/Extensions/UriEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ public static string GetAbsoluteUri(this Uri? httpClientUri, Uri? httpRequestUri

return httpClientUri.AbsoluteUri;
}
}
}
2 changes: 1 addition & 1 deletion src/Utils/ServiceRegistration/ServiceCollectionEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ private static bool TryGetTimeout(this IConfiguration configuration, out TimeSpa

return true;
}
}
}
2 changes: 2 additions & 0 deletions src/_Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global using System.Text;
global using System.Text.Json;
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ private static string GetExecutionPath(string projectDirectory, Type classType)

return Path.Combine(dirs);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ protected LogEventLevel LogLevel

protected IFluentHttp CreateFixture() =>
_serviceProvider.GetRequiredService<IFluentHttp>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,20 @@ public async Task GetDataWithoutVerboseLogging()

await Verify(result);
}
}

[Fact]
public async Task GetInvalidModel()
{
var options = new HttpCallOptions
{
PathSegments = { "users" }
};

var result = await CreateFixture()
.GetJsonOrDefaultAsync(options, PostRecordContext.Default.PostRecord);

result
.Should()
.BeNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,32 @@ public async Task PostDataWithoutVerboseLogging()

await Verify(result);
}
}

[Fact]
public async Task RetrieveInvalidModel()
{
var options = new HttpCallOptions
{
PathSegments = { "posts" }
};

var data = new PostCreateRecord
{
UserId = 2,
Title = "Japanese grammar",
Body = "Verbs, adjectives and nouns"
};

var postContext = new UserRecordContext(new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});

var result = await CreateFixture()
.PostJsonOrDefaultAsync(data, options, PostCreateRecordContext.Default.PostCreateRecord, postContext.UserRecordArray);

result
.Should()
.BeNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ public sealed record PostRecord : PostRecordBase
public int Id { get; set; }
}

public sealed record PostCreateRecord : PostRecordBase;
public sealed record PostCreateRecord : PostRecordBase;
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ public sealed record CompanyRecord
[JsonProperty(BusinessTypeName)]
public string BusinessType { get; set; } = string.Empty;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
Expand Down
4 changes: 2 additions & 2 deletions tests/MyNihongo.FluentHttp.Tests.Integration/_Usings.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
global using MyNihongo.FluentHttp.Tests.Integration.Models;
global using FluentAssertions;
global using MyNihongo.FluentHttp.Tests.Integration.Models;
global using Serilog.Events;
global using Xunit;
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@ await CreateFixture()

VerifyPost(req, expectedOptions, cts.Token);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@ await CreateFixture()

VerifyPost(req, expectedOptions, cts.Token);
}
}
}
Loading