Created
April 19, 2024 13:28
-
-
Save binarycow/2727bcf0b9e12598e910ea0fe6b74045 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Diagnostics; | |
using System.Net; | |
using APITesting.Contracts; | |
namespace YOUR_NAMESPACE_HERE; | |
public readonly struct ApiError : IEquatable<ApiError> | |
{ | |
private const string DefaultErrorMessage = "An error occurred"; | |
internal ApiError(HttpStatusCode statusCode, object? data) | |
{ | |
Debug.Assert(data is null or APITesting.Client.ProblemDetails or System.Exception or string); | |
this.data = data; | |
this.StatusCode = statusCode; | |
} | |
public ApiError(ProblemDetails problemDetails, HttpStatusCode statusCode) | |
{ | |
ArgumentNullException.ThrowIfNull(problemDetails); | |
ThrowHelper.ThrowIfSuccessfulStatusCode(statusCode); | |
this.data = problemDetails; | |
this.StatusCode = statusCode; | |
} | |
public ApiError(Exception exception, HttpStatusCode statusCode) | |
{ | |
ArgumentNullException.ThrowIfNull(exception); | |
ThrowHelper.ThrowIfSuccessfulStatusCode(statusCode); | |
this.data = exception; | |
this.StatusCode = statusCode; | |
} | |
public ApiError(string message, HttpStatusCode statusCode) | |
{ | |
ArgumentException.ThrowIfNullOrWhiteSpace(message); | |
ThrowHelper.ThrowIfSuccessfulStatusCode(statusCode); | |
this.data = message; | |
this.StatusCode = statusCode; | |
} | |
private readonly object? data; | |
public HttpStatusCode StatusCode { get; } | |
public ProblemDetails? ProblemDetails => this.data as ProblemDetails; | |
public Exception? Exception => this.data as Exception; | |
public string Message => (HasStatusCode: this.StatusCode != default, Data: this.data) switch | |
{ | |
(HasStatusCode: false, Data: string message) => message, | |
(HasStatusCode: false, Data: Exception exception) => exception.Message, | |
(HasStatusCode: false, Data: ProblemDetails problemDetails) => problemDetails.Title ?? problemDetails.Detail ?? DefaultErrorMessage, | |
(HasStatusCode: false, Data: _) => DefaultErrorMessage, | |
(HasStatusCode: true, Data: string message) => $"HTTP {(int)this.StatusCode} ({this.StatusCode.GetReasonPhrase()}){Environment.NewLine}{message}", | |
(HasStatusCode: true, Data: Exception exception) => $"HTTP {(int)this.StatusCode} ({this.StatusCode.GetReasonPhrase()}){Environment.NewLine}{exception.Message}", | |
(HasStatusCode: true, Data: ProblemDetails problemDetails) =>$"HTTP {(int)this.StatusCode} ({this.StatusCode.GetReasonPhrase()}){Environment.NewLine}{problemDetails.Title}", | |
(HasStatusCode: true, Data: _) => $"HTTP {(int)this.StatusCode} ({this.StatusCode.GetReasonPhrase()})", | |
}; | |
public override string ToString() => (HasStatusCode: this.StatusCode != default, Data: this.data) switch | |
{ | |
(HasStatusCode: false, Data: string message) => message, | |
(HasStatusCode: false, Data: Exception exception) => exception.ToString(), // TODO: Are we okay with Exception.ToString() | |
(HasStatusCode: false, Data: ProblemDetails problemDetails) => problemDetails.Title ?? problemDetails.Detail ?? DefaultErrorMessage, | |
(HasStatusCode: false, Data: _) => DefaultErrorMessage, | |
(HasStatusCode: true, Data: string message) => $"HTTP {(int)this.StatusCode} ({this.StatusCode.GetReasonPhrase()}){Environment.NewLine}{message}", | |
(HasStatusCode: true, Data: Exception exception) => $"HTTP {(int)this.StatusCode} ({this.StatusCode.GetReasonPhrase()}){Environment.NewLine}{exception}",// TODO: Are we okay with Exception.ToString() | |
(HasStatusCode: true, Data: ProblemDetails problemDetails) =>$"HTTP {(int)this.StatusCode} ({this.StatusCode.GetReasonPhrase()}){Environment.NewLine}{problemDetails.Title}", | |
(HasStatusCode: true, Data: _) => $"HTTP {(int)this.StatusCode} ({this.StatusCode.GetReasonPhrase()})", | |
}; | |
public bool Equals(ApiError other, bool ignoreStatusCode) => (ignoreStatusCode is false || this.StatusCode == other.StatusCode) && Equals(this.data, other.data); | |
public bool Equals(ApiError other) => this.StatusCode == other.StatusCode && Equals(this.data, other.data); | |
public bool Equals(HttpStatusCode other) => this.StatusCode == other; | |
public bool Equals(ProblemDetails? other) => other is null ? this.StatusCode.IsSuccessfulStatusCode() : this.ProblemDetails == other; | |
public bool Equals(Exception? other) => other is null ? this.StatusCode.IsSuccessfulStatusCode() : this.Exception == other; | |
public override bool Equals(object? obj) => obj switch | |
{ | |
ApiError other => this.Equals(other), | |
HttpStatusCode other => this.Equals(other), | |
ProblemDetails other => this.Equals(other), | |
Exception other => this.Equals(other), | |
_ => false, | |
}; | |
public override int GetHashCode() => HashCode.Combine(this.data, this.StatusCode); | |
public static bool operator ==(ApiError left, ApiError right) => left.Equals(right); | |
public static bool operator !=(ApiError left, ApiError right) => !left.Equals(right); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Net.Http.Json; | |
using System.Text.Json; | |
namespace YOUR_NAMESPACE_HERE; | |
public readonly partial struct ApiResult | |
{ | |
public static async Task<ApiResult<T>> CreateAsync<T>( | |
HttpResponseMessage response, | |
JsonSerializerOptions? jsonOptions, | |
CancellationToken cancellationToken | |
) where T : notnull | |
{ | |
try | |
{ | |
if (response.IsSuccessStatusCode) | |
{ | |
return await response.Content.ReadFromJsonAsync<T>(jsonOptions, cancellationToken) is { } result | |
? Success(result) | |
: NullDeserializationResult; | |
} | |
return await response.Content.ReadFromJsonAsync<ProblemDetails>(jsonOptions, cancellationToken) is { } problem | |
? Fail(problem, response.StatusCode) | |
: Fail(response.StatusCode); | |
} | |
catch (Exception ex) | |
{ | |
return Fail(ex, response.StatusCode); | |
} | |
} | |
public static async Task<ApiResult> CreateAsync( | |
HttpResponseMessage response, | |
JsonSerializerOptions? jsonOptions, | |
CancellationToken cancellationToken | |
) | |
{ | |
try | |
{ | |
if (response.IsSuccessStatusCode) | |
return Success(response.StatusCode); | |
return await response.Content.ReadFromJsonAsync<ProblemDetails>(jsonOptions, cancellationToken) is { } problem | |
? Fail(problem, response.StatusCode) | |
: Fail(response.StatusCode); | |
} | |
catch (Exception ex) | |
{ | |
return Fail(ex, response.StatusCode); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Net; | |
using System.Net.Http.Json; | |
using System.Text.Json; | |
using APITesting.Contracts; | |
namespace YOUR_NAMESPACE_HERE; | |
public readonly partial struct ApiResult : IEquatable<ApiResult> | |
{ | |
private const string SuccessString = "Success"; | |
private static readonly ApiError NullDeserializationResult = new("Null deserialization result", default); | |
public ApiError Error { get; } | |
public HttpStatusCode StatusCode => this.Error.StatusCode; | |
public bool IsSuccess => this.StatusCode.IsSuccessfulStatusCode(); | |
private ApiResult(ApiError error) | |
{ | |
this.Error = error; | |
} | |
public static ApiError Fail(ProblemDetails problemDetails, HttpStatusCode statusCode) | |
{ | |
ThrowHelper.ThrowIfSuccessfulStatusCode(statusCode); | |
return new(problemDetails, statusCode); | |
} | |
public static ApiError Fail(Exception exception, HttpStatusCode statusCode) | |
{ | |
ThrowHelper.ThrowIfSuccessfulStatusCode(statusCode); | |
return new(exception, statusCode); | |
} | |
public static ApiError Fail(string message, HttpStatusCode statusCode) | |
{ | |
ThrowHelper.ThrowIfSuccessfulStatusCode(statusCode); | |
return new(message, statusCode); | |
} | |
public static ApiError Fail(HttpStatusCode statusCode) | |
{ | |
ThrowHelper.ThrowIfSuccessfulStatusCode(statusCode); | |
return new(statusCode, null); | |
} | |
public static ApiResult Success(HttpStatusCode statusCode) | |
{ | |
ThrowHelper.ThrowIfUnsuccessfulStatusCode(statusCode); | |
return new ApiError(statusCode, null); | |
} | |
public static ApiResult<T> Success<T>(T value, HttpStatusCode statusCode = HttpStatusCode.OK) where T : notnull | |
{ | |
ThrowHelper.ThrowIfUnsuccessfulStatusCode(statusCode); | |
return new(value, statusCode); | |
} | |
public static implicit operator ApiResult(ApiError error) => new(error); | |
public override string ToString() => this.IsSuccess ? SuccessString : this.Error.ToString(); | |
public bool Equals(ApiResult other, bool ignoreStatusCode) => (IgnoreStatusCode: ignoreStatusCode, this.IsSuccess) switch | |
{ | |
(IgnoreStatusCode: true, IsSuccess: true) => other.IsSuccess, | |
(IgnoreStatusCode: false, IsSuccess: true) => other.IsSuccess && this.StatusCode == other.StatusCode, | |
(IgnoreStatusCode: _, IsSuccess: false) => this.Error.Equals(other.Error, ignoreStatusCode) | |
}; | |
public bool Equals(ApiResult other) | |
=> this.Error.Equals(other.Error, ignoreStatusCode: false); | |
public bool Equals(HttpStatusCode other) | |
=> this.StatusCode == other; | |
public bool Equals(Exception? other) | |
=> this.Error.Exception == other; | |
public bool Equals(ProblemDetails? other) | |
=> this.Error.ProblemDetails == other; | |
public override bool Equals(object? obj) => obj switch | |
{ | |
HttpStatusCode other => this.Equals(other), | |
ApiResult other => this.Equals(other), | |
Exception other => this.Equals(other), | |
ProblemDetails other => this.Equals(other), | |
_ => false, | |
}; | |
public override int GetHashCode() => HashCode.Combine(this.Error, this.StatusCode); | |
public static bool operator ==(ApiResult left, ApiResult right) => left.Equals(right); | |
public static bool operator !=(ApiResult left, ApiResult right) => !left.Equals(right); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Diagnostics.CodeAnalysis; | |
using System.Net; | |
using APITesting.Contracts; | |
namespace YOUR_NAMESPACE_HERE; | |
public readonly struct ApiResult<T> : IEquatable<ApiResult<T>> | |
where T : notnull | |
{ | |
public ApiError Error { get; } | |
public T? Value { get; } | |
[MemberNotNullWhen(true, nameof(Value))] | |
public bool IsSuccess => this.StatusCode.IsSuccessfulStatusCode(); | |
public HttpStatusCode StatusCode => this.Error.StatusCode; | |
private ApiResult(T value) | |
{ | |
ThrowHelper.ThrowIfNull(value); | |
this.Value = value; | |
this.Error = new (HttpStatusCode.OK, null); | |
} | |
public ApiResult(T value, HttpStatusCode statusCode) | |
{ | |
ThrowHelper.ThrowIfNull(value); | |
ThrowHelper.ThrowIfUnsuccessfulStatusCode(statusCode); | |
this.Value = value; | |
this.Error = new (statusCode, null); | |
} | |
public ApiResult(ApiError error) | |
{ | |
this.Error = error; | |
} | |
public static implicit operator ApiResult<T>(ApiError error) => new(error); | |
public static implicit operator ApiResult<T>(T value) => new(value); | |
public override string? ToString() => this.IsSuccess ? this.Value.ToString() : this.Error.ToString(); | |
public bool Equals(ApiResult<T> other, bool ignoreStatusCode) => (this.IsSuccess, IgnoreStatusCode: ignoreStatusCode) switch | |
{ | |
(IsSuccess: true, IgnoreStatusCode: true) => other.IsSuccess && EqualityComparer<T>.Default.Equals(this.Value, other.Value), | |
(IsSuccess: true, IgnoreStatusCode: false) => this.StatusCode == other.StatusCode && EqualityComparer<T>.Default.Equals(this.Value, other.Value), | |
(IsSuccess: false, IgnoreStatusCode: _) => this.Error.Equals(other.Error, ignoreStatusCode), | |
}; | |
public bool Equals(ApiResult<T> other) => Equals(other, ignoreStatusCode: false); | |
public bool Equals(T? other) => EqualityComparer<T>.Default.Equals(this.Value, other); | |
public bool Equals(ApiError other, bool ignoreStatusCode) => this.Error.Equals(other, ignoreStatusCode: ignoreStatusCode); | |
public bool Equals(ApiError other) => this.Error.Equals(other, ignoreStatusCode: false); | |
public bool Equals(Exception? other) => other is null ? this.IsSuccess : this.Error.Equals(other); | |
public bool Equals(ProblemDetails? other) => other is null ? this.IsSuccess : this.Error.Equals(other); | |
public override bool Equals(object? obj) => obj switch | |
{ | |
ApiResult<T> other => this.Equals(other), | |
T other => this.Equals(other), | |
Exception other => this.Equals(other), | |
ProblemDetails other => this.Equals(other), | |
_ => false, | |
}; | |
public override int GetHashCode() => HashCode.Combine(this.Error, this.Value); | |
public static bool operator ==(ApiResult<T> left, ApiResult<T> right) => left.Equals(right); | |
public static bool operator !=(ApiResult<T> left, ApiResult<T> right) => !left.Equals(right); | |
public bool TryGetValue([NotNullWhen(true)] out T? value, out ApiError error) | |
{ | |
value = this.Value; | |
error = this.Error; | |
return value is not null && this.IsSuccess; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Net; | |
using System.Runtime.CompilerServices; | |
namespace YOUR_NAMESPACE_HERE; | |
internal static class HttpStatusCodeExtensions | |
{ | |
public static bool IsSuccessfulStatusCode(this HttpStatusCode statusCode) => | |
((int)statusCode >= 200) && (int)statusCode <= 299; | |
public static bool IsUnsuccessfulStatusCode(this HttpStatusCode statusCode) => | |
statusCode.IsSuccessfulStatusCode() is false; | |
public static string? GetReasonPhrase(this HttpStatusCode code) => GetReasonPhrase((int)code); | |
public static string? GetReasonPhrase(int code) => code switch | |
{ | |
100 => "Continue", | |
101 => "Switching Protocols", | |
102 => "Processing", | |
103 => "Early Hints", | |
200 => "OK", | |
201 => "Created", | |
202 => "Accepted", | |
203 => "Non-Authoritative Information", | |
204 => "No Content", | |
205 => "Reset Content", | |
206 => "Partial Content", | |
207 => "Multi-Status", | |
208 => "Already Reported", | |
226 => "IM Used", | |
300 => "Multiple Choices", | |
301 => "Moved Permanently", | |
302 => "Found", | |
303 => "See Other", | |
304 => "Not Modified", | |
305 => "Use Proxy", | |
307 => "Temporary Redirect", | |
308 => "Permanent Redirect", | |
400 => "Bad Request", | |
401 => "Unauthorized", | |
402 => "Payment Required", | |
403 => "Forbidden", | |
404 => "Not Found", | |
405 => "Method Not Allowed", | |
406 => "Not Acceptable", | |
407 => "Proxy Authentication Required", | |
408 => "Request Timeout", | |
409 => "Conflict", | |
410 => "Gone", | |
411 => "Length Required", | |
412 => "Precondition Failed", | |
413 => "Request Entity Too Large", | |
414 => "Request-Uri Too Long", | |
415 => "Unsupported Media Type", | |
416 => "Requested Range Not Satisfiable", | |
417 => "Expectation Failed", | |
421 => "Misdirected Request", | |
422 => "Unprocessable Entity", | |
423 => "Locked", | |
424 => "Failed Dependency", | |
426 => "Upgrade Required", // RFC 2817 | |
428 => "Precondition Required", | |
429 => "Too Many Requests", | |
431 => "Request Header Fields Too Large", | |
451 => "Unavailable For Legal Reasons", | |
500 => "Internal Server Error", | |
501 => "Not Implemented", | |
502 => "Bad Gateway", | |
503 => "Service Unavailable", | |
504 => "Gateway Timeout", | |
505 => "Http Version Not Supported", | |
506 => "Variant Also Negotiates", | |
507 => "Insufficient Storage", | |
508 => "Loop Detected", | |
510 => "Not Extended", | |
511 => "Network Authentication Required", | |
_ => null, | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text.Json.Serialization; | |
namespace YOUR_NAMESPACE_HERE; | |
/// <remarks> | |
/// Copied from aspnetcore repo, so we don't need to bring in the ASP.NET Core framework. | |
/// License: MIT | |
/// Retrieved On: 2024-04-17 | |
/// Retrieved From: https://github.com/dotnet/aspnetcore/blob/7412c71cf881392233b5652310ab2b9e7fdf71eb/src/Http/Http.Abstractions/src/ProblemDetails/ProblemDetails.cs | |
/// </remarks> | |
public class ProblemDetails | |
{ | |
/// <summary> | |
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when | |
/// dereferenced, it provides human-readable documentation for the problem type | |
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be | |
/// "about:blank". | |
/// </summary> | |
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | |
[JsonPropertyOrder(-5)] | |
[JsonPropertyName("type")] | |
public string? Type { get; set; } | |
/// <summary> | |
/// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence | |
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation; | |
/// see[RFC7231], Section 3.4). | |
/// </summary> | |
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | |
[JsonPropertyOrder(-4)] | |
[JsonPropertyName("title")] | |
public string? Title { get; set; } | |
/// <summary> | |
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. | |
/// </summary> | |
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | |
[JsonPropertyOrder(-3)] | |
[JsonPropertyName("status")] | |
public int? Status { get; set; } | |
/// <summary> | |
/// A human-readable explanation specific to this occurrence of the problem. | |
/// </summary> | |
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | |
[JsonPropertyOrder(-2)] | |
[JsonPropertyName("detail")] | |
public string? Detail { get; set; } | |
/// <summary> | |
/// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. | |
/// </summary> | |
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] | |
[JsonPropertyOrder(-1)] | |
[JsonPropertyName("instance")] | |
public string? Instance { get; set; } | |
/// <summary> | |
/// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members. | |
/// <para> | |
/// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as | |
/// other members of a problem type. | |
/// </para> | |
/// </summary> | |
/// <remarks> | |
/// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters. | |
/// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. | |
/// </remarks> | |
[JsonExtensionData] | |
public IDictionary<string, object?> Extensions { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Diagnostics.CodeAnalysis; | |
using System.Net; | |
using System.Runtime.CompilerServices; | |
namespace YOUR_NAMESPACE_HERE; | |
internal static class ThrowHelper | |
{ | |
public static void ThrowIfNull<T>([NotNull] T? value, [CallerArgumentExpression(nameof(value))] string argumentName = "") | |
{ | |
if (value is null) | |
ThrowArgumentNullException(argumentName); | |
} | |
[DoesNotReturn] | |
public static void ThrowArgumentNullException(string argumentName) | |
{ | |
throw new ArgumentNullException(argumentName); | |
} | |
public static void ThrowIfSuccessfulStatusCode(HttpStatusCode statusCode, [CallerArgumentExpression(nameof(statusCode))] string argumentName = "") | |
{ | |
if (statusCode.IsSuccessfulStatusCode()) | |
ThrowSuccessfulStatusCode(statusCode, argumentName); | |
} | |
public static void ThrowIfUnsuccessfulStatusCode(HttpStatusCode statusCode, [CallerArgumentExpression(nameof(statusCode))] string argumentName = "") | |
{ | |
if (statusCode.IsSuccessfulStatusCode() is false) | |
ThrowUnsuccessfulStatusCode(statusCode, argumentName); | |
} | |
private static void ThrowSuccessfulStatusCode(HttpStatusCode statusCode, string argumentName) | |
{ | |
throw new ArgumentOutOfRangeException(argumentName, statusCode, $"Expected unsuccessful status code; received {statusCode}"); | |
} | |
private static void ThrowUnsuccessfulStatusCode(HttpStatusCode statusCode, string argumentName) | |
{ | |
throw new ArgumentOutOfRangeException(argumentName, statusCode, $"Expected successful status code; received {statusCode}"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment