Skip to content

Instantly share code, notes, and snippets.

@binarycow
Created April 19, 2024 13:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save binarycow/2727bcf0b9e12598e910ea0fe6b74045 to your computer and use it in GitHub Desktop.
Save binarycow/2727bcf0b9e12598e910ea0fe6b74045 to your computer and use it in GitHub Desktop.
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);
}
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);
}
}
}
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);
}
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;
}
}
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,
};
}
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);
}
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