diff --git a/.editorconfig b/.editorconfig index 8f2ff4c..015138a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,7 @@ indent_style = space tab_width = 4 # New line preferences -end_of_line = crlf +end_of_line = lf insert_final_newline = true #### .NET Coding Conventions #### diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..12dfd0c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf +*.cs text eol=lf +*.csproj text eol=lf +*.props text eol=lf +*.targets text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf +*.md text eol=lf +*.sh text eol=lf +*.editorconfig text eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 682a699..d0c14f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,8 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: '11.0.x' + include-prerelease: true - name: Restore run: dotnet restore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 859f52f..004489a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,8 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.x' + dotnet-version: '11.0.x' + include-prerelease: true - name: Verify version alignment run: | diff --git a/.gitignore b/.gitignore index 0e91e7e..d85bdad 100644 --- a/.gitignore +++ b/.gitignore @@ -761,4 +761,5 @@ $RECYCLE.BIN/ *.db-shm *.db-wal +plans/ diff --git a/CSharpEssentials.Any/Any.cs b/CSharpEssentials.Any/Any.cs index ba0c9ae..34f6770 100644 --- a/CSharpEssentials.Any/Any.cs +++ b/CSharpEssentials.Any/Any.cs @@ -51,17 +51,12 @@ public static class Any internal static (int index, object? value) Deserialize(Dictionary typeMap, int index, object? value) { - #if NET6_0_OR_GREATER - - ArgumentNullException.ThrowIfNull(value); - - #else - - if (value is null) - - throw new ArgumentNullException(nameof(value)); - - #endif +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(value); +#else + if (value is null) + throw new ArgumentNullException(nameof(value)); +#endif if (!typeMap.TryGetValue(index, out Type? type)) throw new InvalidOperationException($"{index} is not valid index for Any<>"); diff --git a/CSharpEssentials.Any/AnyActionResult.cs b/CSharpEssentials.Any/AnyActionResult.cs index 9613d0b..a942cf3 100644 --- a/CSharpEssentials.Any/AnyActionResult.cs +++ b/CSharpEssentials.Any/AnyActionResult.cs @@ -10,4 +10,4 @@ public readonly record struct AnyActionResult(AnyActionStatus Status, T { public static implicit operator AnyActionResult(TResult? result) => new(AnyActionStatus.Executed, result); public static implicit operator AnyActionResult(AnyActionStatus status) => new(status, default); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Any/AnyT2.cs b/CSharpEssentials.Any/AnyT2.cs index e933de1..d8130b4 100644 --- a/CSharpEssentials.Any/AnyT2.cs +++ b/CSharpEssentials.Any/AnyT2.cs @@ -1,3 +1,4 @@ + using System.Text.Json.Serialization; using CSharpEssentials.Json; @@ -34,7 +35,7 @@ public AnyActionStatus Switch( Action? first = null, Action? second = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); switch (Index) { @@ -53,7 +54,7 @@ public AnyActionResult Match( Func? first = null, Func? second = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); return Index switch { diff --git a/CSharpEssentials.Any/AnyT3.cs b/CSharpEssentials.Any/AnyT3.cs index d9d1ee7..c3fb74b 100644 --- a/CSharpEssentials.Any/AnyT3.cs +++ b/CSharpEssentials.Any/AnyT3.cs @@ -1,3 +1,4 @@ + using System.Text.Json.Serialization; using CSharpEssentials.Json; @@ -41,7 +42,7 @@ public AnyActionStatus Switch( Action? second = null, Action? third = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); switch (Index) { @@ -64,7 +65,7 @@ public AnyActionResult Match( Func? second = null, Func? third = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); return Index switch { diff --git a/CSharpEssentials.Any/AnyT4.cs b/CSharpEssentials.Any/AnyT4.cs index bf6a388..ea64e19 100644 --- a/CSharpEssentials.Any/AnyT4.cs +++ b/CSharpEssentials.Any/AnyT4.cs @@ -1,3 +1,4 @@ + using System.Text.Json.Serialization; using CSharpEssentials.Json; @@ -48,7 +49,7 @@ public AnyActionStatus Switch( Action? third = null, Action? fourth = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); switch (Index) { @@ -76,7 +77,7 @@ public AnyActionResult Match( Func? third = null, Func? fourth = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); return Index switch { diff --git a/CSharpEssentials.Any/AnyT5.cs b/CSharpEssentials.Any/AnyT5.cs index a5c89f9..1ea2379 100644 --- a/CSharpEssentials.Any/AnyT5.cs +++ b/CSharpEssentials.Any/AnyT5.cs @@ -1,3 +1,4 @@ + using System.Text.Json.Serialization; using CSharpEssentials.Json; @@ -55,7 +56,7 @@ public AnyActionStatus Switch( Action? fourth = null, Action? fifth = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); switch (Index) { @@ -86,7 +87,7 @@ public AnyActionResult Match( Func? fourth = null, Func? fifth = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); return Index switch { diff --git a/CSharpEssentials.Any/AnyT6.cs b/CSharpEssentials.Any/AnyT6.cs index fb78650..72bf006 100644 --- a/CSharpEssentials.Any/AnyT6.cs +++ b/CSharpEssentials.Any/AnyT6.cs @@ -1,3 +1,4 @@ + using System.Text.Json.Serialization; using CSharpEssentials.Json; @@ -63,7 +64,7 @@ public AnyActionStatus Switch( Action? fifth = null, Action? sixth = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); switch (Index) { @@ -98,7 +99,7 @@ public AnyActionResult Match( Func? fifth = null, Func? sixth = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); return Index switch { diff --git a/CSharpEssentials.Any/AnyT7.cs b/CSharpEssentials.Any/AnyT7.cs index 09617cf..70ba569 100644 --- a/CSharpEssentials.Any/AnyT7.cs +++ b/CSharpEssentials.Any/AnyT7.cs @@ -1,3 +1,4 @@ + using System.Text.Json.Serialization; using CSharpEssentials.Json; @@ -70,7 +71,7 @@ public AnyActionStatus Switch( Action? sixth = null, Action? seventh = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); switch (Index) { @@ -109,7 +110,7 @@ public AnyActionResult Match( Func? sixth = null, Func? seventh = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); return Index switch { diff --git a/CSharpEssentials.Any/AnyT8.cs b/CSharpEssentials.Any/AnyT8.cs index 56ace71..b21915a 100644 --- a/CSharpEssentials.Any/AnyT8.cs +++ b/CSharpEssentials.Any/AnyT8.cs @@ -1,3 +1,4 @@ + using System.Text.Json.Serialization; using CSharpEssentials.Json; @@ -77,7 +78,7 @@ public AnyActionStatus Switch( Action? seventh = null, Action? eighth = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); switch (Index) { @@ -120,7 +121,7 @@ public AnyActionResult Match( Func? seventh = null, Func? eighth = null) { -if (Value is null) + if (Value is null) throw new InvalidOperationException("Value cannot be null"); return Index switch { diff --git a/CSharpEssentials.Any/CSharpEssentials.Any.csproj b/CSharpEssentials.Any/CSharpEssentials.Any.csproj index 88c077c..1f21d6a 100644 --- a/CSharpEssentials.Any/CSharpEssentials.Any.csproj +++ b/CSharpEssentials.Any/CSharpEssentials.Any.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 CSharpEssentials.Any diff --git a/CSharpEssentials.AspNetCore/CSharpEssentials.AspNetCore.csproj b/CSharpEssentials.AspNetCore/CSharpEssentials.AspNetCore.csproj index 9fe537d..24f244e 100644 --- a/CSharpEssentials.AspNetCore/CSharpEssentials.AspNetCore.csproj +++ b/CSharpEssentials.AspNetCore/CSharpEssentials.AspNetCore.csproj @@ -2,7 +2,7 @@ - net9.0;net8.0 + net11.0;net10.0;net9.0;net8.0 CSharpEssentials.AspNetCore CSharp Essentials AspNetCore @@ -49,7 +49,7 @@ - + diff --git a/CSharpEssentials.AspNetCore/ProblemDetails/EnhancedProblemDetails.cs b/CSharpEssentials.AspNetCore/ProblemDetails/EnhancedProblemDetails.cs index dc69bae..8420070 100644 --- a/CSharpEssentials.AspNetCore/ProblemDetails/EnhancedProblemDetails.cs +++ b/CSharpEssentials.AspNetCore/ProblemDetails/EnhancedProblemDetails.cs @@ -14,8 +14,8 @@ public sealed class EnhancedProblemDetails : ProblemDetails #endif [JsonPropertyName("errorCodes")] - public HashSet ErrorCodes { get; set; } = new HashSet(); + public HashSet ErrorCodes { get; set; } = []; [JsonPropertyName("errorMessages")] - public HashSet ErrorMessages { get; set; } = new HashSet(); + public HashSet ErrorMessages { get; set; } = []; } diff --git a/CSharpEssentials.AspNetCore/ProblemDetails/Extensions.cs b/CSharpEssentials.AspNetCore/ProblemDetails/Extensions.cs index ae95fc6..4c2610d 100644 --- a/CSharpEssentials.AspNetCore/ProblemDetails/Extensions.cs +++ b/CSharpEssentials.AspNetCore/ProblemDetails/Extensions.cs @@ -1,10 +1,10 @@ +using CSharpEssentials.Core; using CSharpEssentials.Errors; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; -using CSharpEssentials.Core; namespace CSharpEssentials.AspNetCore; @@ -93,8 +93,8 @@ public static IActionResult ToActionResult(this ResultPattern.Interfaces.IResult if (result.IsSuccess) throw new InvalidOperationException("Cannot convert a successful result to an action result"); return result.Errors.ToActionResult(httpContext, extensions, statusCode); - } + /// /// Converts an to an . /// @@ -181,6 +181,7 @@ public static EnhancedProblemDetails ToProblemDetails(this ResultPattern.Interfa throw new InvalidOperationException("Cannot convert a successful result to a problem details"); return result.Errors.ToProblemDetails(extensions, statusCode); } + /// /// Converts an to a object. /// @@ -202,12 +203,17 @@ public static EnhancedProblemDetails ToProblemDetails(this ResultPattern.Interfa /// public static EnhancedProblemDetails ToProblemDetails(this Error[] errors, ErrorMetadata? extensions = null, int? statusCode = null) { - ErrorMetadata metadata = extensions.IsNotNull() ? new ErrorMetadata(extensions) : ErrorMetadata.CreateEmpty(); + ErrorMetadata metadata = []; + if (extensions.IsNotNull()) + foreach (KeyValuePair item in extensions) + metadata[item.Key] = item.Value; Error error = errors.MaxBy(e => e.Type.ToHttpStatusCode()); statusCode ??= error.Type.ToHttpStatusCode(); +#pragma warning disable IDE0028 var errorCodes = errors.Select(e => e.Code).ToHashSet(); var errorMessages = errors.Select(e => e.Description).ToHashSet(); +#pragma warning restore IDE0028 var problemDetails = new EnhancedProblemDetails { diff --git a/CSharpEssentials.AspNetCore/ProblemDetails/ValidateModelAttribute.cs b/CSharpEssentials.AspNetCore/ProblemDetails/ValidateModelAttribute.cs index edc9c12..dcfba51 100644 --- a/CSharpEssentials.AspNetCore/ProblemDetails/ValidateModelAttribute.cs +++ b/CSharpEssentials.AspNetCore/ProblemDetails/ValidateModelAttribute.cs @@ -11,10 +11,9 @@ public override void OnActionExecuting(ActionExecutingContext context) { if (context.ModelState.IsValid) return; - Error[] errors = context.ModelState + Error[] errors = [.. context.ModelState .Where(arg => arg.Value != null) - .SelectMany(state => state.Value!.Errors.Select(x => Error.Validation($"validation.{state.Key}", x.ErrorMessage))) - .ToArray(); + .SelectMany(state => state.Value!.Errors.Select(x => Error.Validation($"validation.{state.Key}", x.ErrorMessage)))]; context.Result = errors.ToActionResult(context.HttpContext); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.AspNetCore/ResultEndpointFilter/IResultErrorMapper.cs b/CSharpEssentials.AspNetCore/ResultEndpointFilter/IResultErrorMapper.cs new file mode 100644 index 0000000..d1959dd --- /dev/null +++ b/CSharpEssentials.AspNetCore/ResultEndpointFilter/IResultErrorMapper.cs @@ -0,0 +1,8 @@ +using CSharpEssentials.Errors; + +namespace CSharpEssentials.AspNetCore; + +public interface IResultErrorMapper +{ + Microsoft.AspNetCore.Http.IResult Map(Error[] errors); +} diff --git a/CSharpEssentials.AspNetCore/ResultEndpointFilter/ResultEndpointFilter.cs b/CSharpEssentials.AspNetCore/ResultEndpointFilter/ResultEndpointFilter.cs new file mode 100644 index 0000000..d7bd6ab --- /dev/null +++ b/CSharpEssentials.AspNetCore/ResultEndpointFilter/ResultEndpointFilter.cs @@ -0,0 +1,43 @@ + +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; +using Microsoft.AspNetCore.Http; + +namespace CSharpEssentials.AspNetCore; + +public sealed class ResultEndpointFilter(IResultErrorMapper? mapper = null) : IEndpointFilter +{ + public async ValueTask InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) + { + object? result = await next(context); + if (result is null) + return result; + + Type resultType = result.GetType(); + if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Result<>)) + { + bool isSuccess = GetPropertyValue(result, resultType, nameof(Result.IsSuccess)); + Error[] errors = GetPropertyValue(result, resultType, nameof(Result.Errors)); + + if (isSuccess) + { + object? value = resultType.GetProperty("Value")?.GetValue(result); + return TypedResults.Ok(value); + } + + return mapper?.Map(errors) ?? Results.BadRequest(errors); + } + + if (result is CSharpEssentials.ResultPattern.Interfaces.IResult r) + { + return r.IsSuccess + ? Results.Ok() + : mapper?.Map(r.Errors) ?? Results.BadRequest(r.Errors); + } + + return result; + } + + private static T GetPropertyValue(object instance, Type type, string propertyName) + => (T)type.GetProperty(propertyName)!.GetValue(instance)!; +} diff --git a/CSharpEssentials.AspNetCore/Swagger/ConfigureSwaggerOptions.cs b/CSharpEssentials.AspNetCore/Swagger/ConfigureSwaggerOptions.cs index af575df..a51c7f3 100644 --- a/CSharpEssentials.AspNetCore/Swagger/ConfigureSwaggerOptions.cs +++ b/CSharpEssentials.AspNetCore/Swagger/ConfigureSwaggerOptions.cs @@ -10,6 +10,7 @@ using Swashbuckle.AspNetCore.SwaggerGen; namespace CSharpEssentials.AspNetCore; + public sealed class DefaultConfigureSwaggerOptions( IServiceProvider serviceProvider, IHostEnvironment environment, @@ -89,4 +90,4 @@ private static string CreateDescription(string? description, ApiVersionDescripti ? $"{description} This API version has been deprecated" : description; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.AspNetCore/Swagger/Filters/EnumSchemaFilter.cs b/CSharpEssentials.AspNetCore/Swagger/Filters/EnumSchemaFilter.cs index 285584c..c7c79b7 100644 --- a/CSharpEssentials.AspNetCore/Swagger/Filters/EnumSchemaFilter.cs +++ b/CSharpEssentials.AspNetCore/Swagger/Filters/EnumSchemaFilter.cs @@ -1,9 +1,9 @@ using System.Reflection; +using CSharpEssentials.Core; using CSharpEssentials.Enums; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -using CSharpEssentials.Core; namespace CSharpEssentials.AspNetCore.Swagger.Filters; @@ -14,7 +14,7 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) bool isStringEnum = context.Type.IsEnum && context.Type.GetCustomAttribute() != null; if (isStringEnum.IsFalse()) return; - string[] values = Enum.GetNames(context.Type).Select(x => x.ToSnakeCase()).ToArray(); + string[] values = [.. Enum.GetNames(context.Type).Select(x => x.ToSnakeCase())]; var enumValues = values .Select(name => new OpenApiString(name)) @@ -25,4 +25,4 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) schema.Enum = enumValues; schema.Description = $"Possible values: {string.Join(", ", values)}"; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.AspNetCore/Swagger/Filters/ReApplyOptionalRouteParameterOperationFilter.cs b/CSharpEssentials.AspNetCore/Swagger/Filters/ReApplyOptionalRouteParameterOperationFilter.cs index b735bd6..a21a5ff 100644 --- a/CSharpEssentials.AspNetCore/Swagger/Filters/ReApplyOptionalRouteParameterOperationFilter.cs +++ b/CSharpEssentials.AspNetCore/Swagger/Filters/ReApplyOptionalRouteParameterOperationFilter.cs @@ -41,4 +41,4 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) #else private static Regex RouteRegex() => new Regex(@"{(?\w+)\?}", RegexOptions.Compiled); #endif -} \ No newline at end of file +} diff --git a/CSharpEssentials.AspNetCore/Swagger/SwashbuckleSchemaIdFactory.cs b/CSharpEssentials.AspNetCore/Swagger/SwashbuckleSchemaIdFactory.cs index c7ba165..81f8483 100644 --- a/CSharpEssentials.AspNetCore/Swagger/SwashbuckleSchemaIdFactory.cs +++ b/CSharpEssentials.AspNetCore/Swagger/SwashbuckleSchemaIdFactory.cs @@ -43,4 +43,4 @@ public string GetSchemaId(Type modelType) return $"{id}{(index >= 1 ? index.ToString(CultureInfo.InvariantCulture) : string.Empty)}"; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Clone/CSharpEssentials.Clone.csproj b/CSharpEssentials.Clone/CSharpEssentials.Clone.csproj index c0b2801..7d2afef 100644 --- a/CSharpEssentials.Clone/CSharpEssentials.Clone.csproj +++ b/CSharpEssentials.Clone/CSharpEssentials.Clone.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1;netstandard2.0 + net11.0;net10.0;net9.0;netstandard2.1;netstandard2.0 CSharpEssentials.Clone diff --git a/CSharpEssentials.Clone/ICloneable.cs b/CSharpEssentials.Clone/ICloneable.cs index 0c147bd..d61913a 100644 --- a/CSharpEssentials.Clone/ICloneable.cs +++ b/CSharpEssentials.Clone/ICloneable.cs @@ -3,4 +3,4 @@ namespace CSharpEssentials.Clone; public interface ICloneable { T Clone(); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Core/CSharpEssentials.Core.csproj b/CSharpEssentials.Core/CSharpEssentials.Core.csproj index 58f583b..c2283f7 100644 --- a/CSharpEssentials.Core/CSharpEssentials.Core.csproj +++ b/CSharpEssentials.Core/CSharpEssentials.Core.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1;netstandard2.0 + net11.0;net10.0;net9.0;netstandard2.1;netstandard2.0 CSharpEssentials.Core diff --git a/CSharpEssentials.Core/Constants/HttpCodes.cs b/CSharpEssentials.Core/Constants/HttpCodes.cs index 36dbc23..f8531a9 100644 --- a/CSharpEssentials.Core/Constants/HttpCodes.cs +++ b/CSharpEssentials.Core/Constants/HttpCodes.cs @@ -4,16 +4,51 @@ /// public struct HttpCodes { + // 2xx Success public const int Ok = 200; public const int Created = 201; public const int Accepted = 202; public const int NoContent = 204; + public const int PartialContent = 206; + public const int MultiStatus = 207; + + // 3xx Redirection + public const int MovedPermanently = 301; + public const int Found = 302; + public const int NotModified = 304; + public const int TemporaryRedirect = 307; + public const int PermanentRedirect = 308; + + // 4xx Client Errors public const int BadRequest = 400; public const int Unauthorized = 401; + public const int PaymentRequired = 402; public const int Forbidden = 403; public const int NotFound = 404; public const int MethodNotAllowed = 405; + public const int NotAcceptable = 406; + public const int RequestTimeout = 408; public const int Conflict = 409; + public const int Gone = 410; + public const int PreconditionFailed = 412; + public const int PayloadTooLarge = 413; + public const int UnsupportedMediaType = 415; + public const int UnprocessableEntity = 422; + public const int Locked = 423; + public const int FailedDependency = 424; + public const int TooEarly = 425; + public const int PreconditionRequired = 428; + public const int TooManyRequests = 429; + public const int RequestHeaderFieldsTooLarge = 431; + public const int UnavailableForLegalReasons = 451; + + // 5xx Server Errors public const int InternalServerError = 500; public const int NotImplemented = 501; -} \ No newline at end of file + public const int BadGateway = 502; + public const int ServiceUnavailable = 503; + public const int GatewayTimeout = 504; + public const int HttpVersionNotSupported = 505; + public const int InsufficientStorage = 507; + public const int NetworkAuthenticationRequired = 511; +} diff --git a/CSharpEssentials.Core/Extensions/EnumerableExtensionMembers.cs b/CSharpEssentials.Core/Extensions/EnumerableExtensionMembers.cs new file mode 100644 index 0000000..0a31f81 --- /dev/null +++ b/CSharpEssentials.Core/Extensions/EnumerableExtensionMembers.cs @@ -0,0 +1,11 @@ +#if NET10_0_OR_GREATER +namespace CSharpEssentials; + +public static class EnumerableExtensionMembers +{ + extension(IEnumerable source) + { + public bool IsEmpty => !source.Any(); + } +} +#endif diff --git a/CSharpEssentials.Core/Extensions/RandomItemsExtensions.cs b/CSharpEssentials.Core/Extensions/RandomItemsExtensions.cs index 46c47cc..d2e1a6f 100644 --- a/CSharpEssentials.Core/Extensions/RandomItemsExtensions.cs +++ b/CSharpEssentials.Core/Extensions/RandomItemsExtensions.cs @@ -1,4 +1,6 @@ -using System.Runtime.InteropServices; +#if NET5_0_OR_GREATER +using System.Runtime.InteropServices; +#endif using System.Security.Cryptography; namespace CSharpEssentials.Core; diff --git a/CSharpEssentials.Core/Extensions/StringExtensionMembers.cs b/CSharpEssentials.Core/Extensions/StringExtensionMembers.cs new file mode 100644 index 0000000..cd8dc77 --- /dev/null +++ b/CSharpEssentials.Core/Extensions/StringExtensionMembers.cs @@ -0,0 +1,24 @@ +#if NET10_0_OR_GREATER +namespace CSharpEssentials; + +public static class StringExtensionMembers +{ + extension(string str) + { + public bool IsPalindrome + { + get + { + if (string.IsNullOrEmpty(str)) + return true; + for (int i = 0, j = str.Length - 1; i < j; i++, j--) + { + if (str[i] != str[j]) + return false; + } + return true; + } + } + } +} +#endif diff --git a/CSharpEssentials.Core/Extensions/StringExtensions.cs b/CSharpEssentials.Core/Extensions/StringExtensions.cs index bbe2e15..245c892 100644 --- a/CSharpEssentials.Core/Extensions/StringExtensions.cs +++ b/CSharpEssentials.Core/Extensions/StringExtensions.cs @@ -34,7 +34,8 @@ private enum CaseType private static string ConvertCase(string input, CaseType caseType, CultureInfo? culture = null) { - if (input.Length == Zero) return string.Empty; + if (input.Length == Zero) + return string.Empty; ReadOnlySpan value = input.AsSpan(); bool isFirstCharacter = true; @@ -155,4 +156,4 @@ private static bool IsSpecialCharacter(UnicodeCategory category) => category is not UnicodeCategory.UppercaseLetter and not UnicodeCategory.LowercaseLetter and not UnicodeCategory.DecimalDigitNumber; -} \ No newline at end of file +} diff --git a/CSharpEssentials.Core/Guids/Guider.cs b/CSharpEssentials.Core/Guids/Guider.cs index 4d2cf01..585185b 100644 --- a/CSharpEssentials.Core/Guids/Guider.cs +++ b/CSharpEssentials.Core/Guids/Guider.cs @@ -1,6 +1,8 @@ using System.Buffers.Text; using System.Runtime.CompilerServices; +#if !NETSTANDARD using System.Runtime.InteropServices; +#endif namespace CSharpEssentials.Core; @@ -79,4 +81,4 @@ public static Guid NewGuid() #else => Guid.NewGuid(); #endif -} \ No newline at end of file +} diff --git a/CSharpEssentials.Entity/CSharpEssentials.Entity.csproj b/CSharpEssentials.Entity/CSharpEssentials.Entity.csproj index aaeea5a..b1aac51 100644 --- a/CSharpEssentials.Entity/CSharpEssentials.Entity.csproj +++ b/CSharpEssentials.Entity/CSharpEssentials.Entity.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 CSharpEssentials.Entity diff --git a/CSharpEssentials.Entity/EntityBase.cs b/CSharpEssentials.Entity/EntityBase.cs index 6b03e29..52aa9d3 100644 --- a/CSharpEssentials.Entity/EntityBase.cs +++ b/CSharpEssentials.Entity/EntityBase.cs @@ -1,4 +1,3 @@ - using CSharpEssentials.Entity.Interfaces; namespace CSharpEssentials.Entity; @@ -8,21 +7,29 @@ namespace CSharpEssentials.Entity; /// public abstract class EntityBase : IEntityBase { -#if NET8_0_OR_GREATER private readonly List _domainEvents = []; + +#if NET9_0_OR_GREATER + public DateTimeOffset CreatedAt + { + get; + private set => field = value > DateTimeOffset.MinValue ? value : throw new ArgumentOutOfRangeException(nameof(value)); + } #else - private readonly List _domainEvents = new List(0); + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0032:Use auto property", Justification = "Property validation requires explicit backing field on targets prior to .NET 9")] + private DateTimeOffset _createdAt; + public DateTimeOffset CreatedAt + { + get => _createdAt; + private set => _createdAt = value > DateTimeOffset.MinValue ? value : throw new ArgumentOutOfRangeException(nameof(value)); + } #endif - - - public DateTimeOffset CreatedAt { get; private set; } public string? CreatedBy { get; private set; } public DateTimeOffset? UpdatedAt { get; private set; } public string? UpdatedBy { get; private set; } - public IReadOnlyList DomainEvents => _domainEvents.ToList().AsReadOnly(); public void ClearDomainEvents() => _domainEvents.Clear(); diff --git a/CSharpEssentials.Entity/Interfaces/IDomainEvent.cs b/CSharpEssentials.Entity/Interfaces/IDomainEvent.cs index 0c4d9e8..341a870 100644 --- a/CSharpEssentials.Entity/Interfaces/IDomainEvent.cs +++ b/CSharpEssentials.Entity/Interfaces/IDomainEvent.cs @@ -1,4 +1,4 @@ namespace CSharpEssentials.Entity.Interfaces; -public interface IDomainEvent; \ No newline at end of file +public interface IDomainEvent; diff --git a/CSharpEssentials.Entity/Interfaces/IEntityBase.cs b/CSharpEssentials.Entity/Interfaces/IEntityBase.cs index c7ffab9..6fc23da 100644 --- a/CSharpEssentials.Entity/Interfaces/IEntityBase.cs +++ b/CSharpEssentials.Entity/Interfaces/IEntityBase.cs @@ -12,4 +12,4 @@ public interface IEntityBase : IEntityBase public interface ISoftDeletableEntityBase : IEntityBase, ISoftDeletableEntityBase where TId : IEquatable { -} \ No newline at end of file +} diff --git a/CSharpEssentials.Entity/SoftDeletableEntityBaseTId.cs b/CSharpEssentials.Entity/SoftDeletableEntityBaseTId.cs index b594220..92faae2 100644 --- a/CSharpEssentials.Entity/SoftDeletableEntityBaseTId.cs +++ b/CSharpEssentials.Entity/SoftDeletableEntityBaseTId.cs @@ -9,4 +9,4 @@ public abstract class SoftDeletableEntityBase : SoftDeletableEntityBase, IS where TId : IEquatable { public TId? Id { get; protected set; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.EntityFrameworkCore/Abstractions/BaseDbContext.cs b/CSharpEssentials.EntityFrameworkCore/Abstractions/BaseDbContext.cs index cf0dcec..24711f0 100644 --- a/CSharpEssentials.EntityFrameworkCore/Abstractions/BaseDbContext.cs +++ b/CSharpEssentials.EntityFrameworkCore/Abstractions/BaseDbContext.cs @@ -1,8 +1,7 @@ - +using CSharpEssentials.Core; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using CSharpEssentials.Core; namespace CSharpEssentials.EntityFrameworkCore; @@ -19,17 +18,26 @@ protected BaseDbContext( IServiceProvider serviceProvider = serviceScopeFactory.CreateScope().ServiceProvider; Logger = serviceProvider.GetRequiredService>(); ServiceProvider = serviceProvider; - Logger.LogInformation("Context {DbContextInstanceId} created", _instanceId); + LogContextCreated(_instanceId); } ~BaseDbContext() { - Logger.LogInformation("Context {DbContextInstanceId} destructed", _instanceId); + LogContextDestructed(_instanceId); } public override void Dispose() { - Logger.LogInformation("Context {DbContextInstanceId} disposed", _instanceId); + LogContextDisposed(_instanceId); base.Dispose(); } -} \ No newline at end of file + + [LoggerMessage(Level = LogLevel.Information, Message = "Context {DbContextInstanceId} created")] + private partial void LogContextCreated(Guid dbContextInstanceId); + + [LoggerMessage(Level = LogLevel.Information, Message = "Context {DbContextInstanceId} destructed")] + private partial void LogContextDestructed(Guid dbContextInstanceId); + + [LoggerMessage(Level = LogLevel.Information, Message = "Context {DbContextInstanceId} disposed")] + private partial void LogContextDisposed(Guid dbContextInstanceId); +} diff --git a/CSharpEssentials.EntityFrameworkCore/CSharpEssentials.EntityFrameworkCore.csproj b/CSharpEssentials.EntityFrameworkCore/CSharpEssentials.EntityFrameworkCore.csproj index 7c356bc..778330c 100644 --- a/CSharpEssentials.EntityFrameworkCore/CSharpEssentials.EntityFrameworkCore.csproj +++ b/CSharpEssentials.EntityFrameworkCore/CSharpEssentials.EntityFrameworkCore.csproj @@ -2,7 +2,7 @@ - net9.0;net8.0 + net10.0;net9.0;net8.0 CSharpEssentials.EntityFrameworkCore CSharp Essentials EntityFrameworkCore @@ -43,7 +43,7 @@ - + @@ -75,6 +75,10 @@ Condition="'$(UseProjectReferences)' != 'true'" /> + + \ No newline at end of file diff --git a/CSharpEssentials.EntityFrameworkCore/Converters/EnumToStringConverter.cs b/CSharpEssentials.EntityFrameworkCore/Converters/EnumToStringConverter.cs index bca179b..db2d03f 100644 --- a/CSharpEssentials.EntityFrameworkCore/Converters/EnumToStringConverter.cs +++ b/CSharpEssentials.EntityFrameworkCore/Converters/EnumToStringConverter.cs @@ -1,5 +1,5 @@ -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using CSharpEssentials.Core; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace CSharpEssentials.EntityFrameworkCore.Converters; @@ -16,4 +16,4 @@ public EnumToFormattedStringConverter() ) { } -} \ No newline at end of file +} diff --git a/CSharpEssentials.EntityFrameworkCore/Extensions/DbContextExtensionMethods.cs b/CSharpEssentials.EntityFrameworkCore/Extensions/DbContextExtensionMethods.cs index fcbe8b9..925c1e2 100644 --- a/CSharpEssentials.EntityFrameworkCore/Extensions/DbContextExtensionMethods.cs +++ b/CSharpEssentials.EntityFrameworkCore/Extensions/DbContextExtensionMethods.cs @@ -1,6 +1,8 @@ -using System.Linq.Expressions; + using CSharpEssentials.Entity; using CSharpEssentials.Entity.Interfaces; +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; using Microsoft.EntityFrameworkCore; namespace CSharpEssentials.EntityFrameworkCore; @@ -15,17 +17,17 @@ public static IQueryable AsNoTracking( public static void HardDelete(this DbContext context, TEntity? entity) where TEntity : class, ISoftDeletableEntityBase { - #if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(entity); + ArgumentNullException.ThrowIfNull(entity); - #else +#else if (entity is null) throw new ArgumentNullException(nameof(entity)); - #endif +#endif entity.MarkAsHardDeleted(); context.Remove(entity); } @@ -33,7 +35,7 @@ public static void HardDelete(this DbContext context, TEntity? entity) public static void HardDelete(this DbContext context, IEnumerable entities) where TEntity : class, ISoftDeletableEntityBase { - TEntity[] records = entities as TEntity[] ?? entities.ToArray(); + TEntity[] records = entities as TEntity[] ?? [.. entities]; records.HardDelete(); context.Set().RemoveRange(records); } @@ -41,17 +43,17 @@ public static void HardDelete(this DbContext context, IEnumerable(this DbSet context, TEntity? entity) where TEntity : class, ISoftDeletableEntityBase { - #if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(entity); + ArgumentNullException.ThrowIfNull(entity); - #else +#else if (entity is null) throw new ArgumentNullException(nameof(entity)); - #endif +#endif entity.MarkAsHardDeleted(); context.Remove(entity); } @@ -59,7 +61,7 @@ public static void HardDelete(this DbSet context, TEntity? ent public static void Delete(this DbSet context, IEnumerable entities) where TEntity : class, ISoftDeletableEntityBase { - TEntity[] entityBases = entities as TEntity[] ?? entities.ToArray(); + TEntity[] entityBases = entities as TEntity[] ?? [.. entities]; entityBases.HardDelete(); context.RemoveRange(entityBases); } @@ -103,25 +105,22 @@ public static async Task MigrateDataAsync( options.Query(dbSet) .ToListAsync(cancellationToken); - TEntity[] theyWillBeDeleted = entities + TEntity[] theyWillBeDeleted = [.. entities .Where(entity => dataList - .All(item => !dataKeySelector(item).Equals(entityKeySelector(entity)))) - .ToArray(); + .All(item => !dataKeySelector(item).Equals(entityKeySelector(entity))))]; if (options.HardDeleteMode && theyWillBeDeleted.Length != 0) theyWillBeDeleted.OfType().HardDelete(); - TEntity[] theyWillBeUpdated = entities + TEntity[] theyWillBeUpdated = [.. entities .Join(dataList, entityKeySelector, dataKeySelector, (entity, item) => new { entity, item }) .Where(obj => options.IsUpdatedFunc(obj.entity, obj.item)) - .Select(obj => options.UpdateFunc(obj.entity, obj.item)) - .ToArray(); + .Select(obj => options.UpdateFunc(obj.entity, obj.item))]; - TEntity[] theyWillBeAdded = dataList + TEntity[] theyWillBeAdded = [.. dataList .Where(item => entities .All(entity => !entityKeySelector(entity).Equals(dataKeySelector(item)))) - .Select(options.Converter) - .ToArray(); + .Select(options.Converter)]; if (theyWillBeDeleted.Length != 0) dbSet.RemoveRange(theyWillBeDeleted); @@ -134,4 +133,72 @@ public static async Task MigrateDataAsync( await dbContext.SaveChangesAsync(cancellationToken); } -} \ No newline at end of file + + public static async Task> FirstOrDefaultAsResultAsync( + this IQueryable source, + Error? notFoundError = null, + CancellationToken cancellationToken = default) + where T : class + { + T? entity = await source.FirstOrDefaultAsync(cancellationToken); + if (entity is null) + return notFoundError ?? Error.NotFound(); + return entity; + } + + public static async Task> SingleOrDefaultAsResultAsync( + this IQueryable source, + Error? notFoundError = null, + CancellationToken cancellationToken = default) + where T : class + { + T? entity = await source.SingleOrDefaultAsync(cancellationToken); + if (entity is null) + return notFoundError ?? Error.NotFound(); + return entity; + } + + public static async Task> FindAsResultAsync( + this DbSet source, + object?[]? keyValues, + Error? notFoundError = null, + CancellationToken cancellationToken = default) + where T : class + { + T? entity = await source.FindAsync(keyValues, cancellationToken); + if (entity is null) + return notFoundError ?? Error.NotFound(); + return entity; + } + + public static async Task SaveChangesAsResultAsync( + this DbContext context, + CancellationToken cancellationToken = default) + { + try + { + await context.SaveChangesAsync(cancellationToken); + return Result.Success(); + } + catch (DbUpdateException ex) + { + return Error.Exception(ex, ErrorType.Unknown); + } + } + + public static async Task> SaveChangesAsResultAsync( + this DbContext context, + bool acceptAllChangesOnSuccess, + CancellationToken cancellationToken = default) + { + try + { + int count = await context.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + return count; + } + catch (DbUpdateException ex) + { + return Error.Exception(ex, ErrorType.Unknown); + } + } +} diff --git a/CSharpEssentials.EntityFrameworkCore/Extensions/EntityBaseExtensions.cs b/CSharpEssentials.EntityFrameworkCore/Extensions/EntityBaseExtensions.cs index 739d8a0..044e108 100644 --- a/CSharpEssentials.EntityFrameworkCore/Extensions/EntityBaseExtensions.cs +++ b/CSharpEssentials.EntityFrameworkCore/Extensions/EntityBaseExtensions.cs @@ -80,15 +80,14 @@ public static void AddQueryFilter(this EntityTypeBuilder entityTypeBuilder, E public static void ApplySoftDeleteQueryFilter(this ModelBuilder modelBuilder) { Type entityBaseType = typeof(ISoftDeletableBase); - IMutableEntityType[] entities = modelBuilder.Model + IMutableEntityType[] entities = [.. modelBuilder.Model .GetEntityTypes() .Where(entityType => entityBaseType.IsAssignableFrom(entityType.ClrType)) - .Where(x => x.BaseType is null) - .ToArray(); + .Where(x => x.BaseType is null)]; - foreach (IMutableEntityType? entityType in entities) + foreach (IMutableEntityType entityType in entities) modelBuilder .Entity(entityType.ClrType) .AddQueryFilter(e => !e.IsDeleted); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.EntityFrameworkCore/Extensions/ModelConfigurationExtensions.cs b/CSharpEssentials.EntityFrameworkCore/Extensions/ModelConfigurationExtensions.cs index c07413d..db4def0 100644 --- a/CSharpEssentials.EntityFrameworkCore/Extensions/ModelConfigurationExtensions.cs +++ b/CSharpEssentials.EntityFrameworkCore/Extensions/ModelConfigurationExtensions.cs @@ -1,8 +1,8 @@ using System.Reflection; +using CSharpEssentials.Core; using CSharpEssentials.EntityFrameworkCore.Converters; using CSharpEssentials.Enums; using Microsoft.EntityFrameworkCore; -using CSharpEssentials.Core; namespace CSharpEssentials.EntityFrameworkCore; @@ -26,4 +26,4 @@ public static void ConfigureEnumConventions( .HaveMaxLength(enumMaxLength); } } -} \ No newline at end of file +} diff --git a/CSharpEssentials.EntityFrameworkCore/Extensions/PropertyBuilderExtensions.cs b/CSharpEssentials.EntityFrameworkCore/Extensions/PropertyBuilderExtensions.cs index fc0a5b6..d7f5ec3 100644 --- a/CSharpEssentials.EntityFrameworkCore/Extensions/PropertyBuilderExtensions.cs +++ b/CSharpEssentials.EntityFrameworkCore/Extensions/PropertyBuilderExtensions.cs @@ -23,4 +23,4 @@ public static PropertyBuilder HasJsonConversion(this Prope v => v.ConvertFromJson(jsonOptions) ?? default!) .HasColumnType(columnType); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.EntityFrameworkCore/Interceptors/Audit/AuditInterceptor.cs b/CSharpEssentials.EntityFrameworkCore/Interceptors/Audit/AuditInterceptor.cs index 768d4ca..4e2fa39 100644 --- a/CSharpEssentials.EntityFrameworkCore/Interceptors/Audit/AuditInterceptor.cs +++ b/CSharpEssentials.EntityFrameworkCore/Interceptors/Audit/AuditInterceptor.cs @@ -46,20 +46,14 @@ private void UpdateAuditableEntities(DbContext context) foreach (EntityEntry entry in context.ChangeTracker.Entries()) { - switch (entry.State) + if (entry.State == EntityState.Added && entry.Entity is ICreationAudit creationAudit) + creationAudit.SetCreatedInfo(now, userId); + else if (entry.State == EntityState.Modified && entry.Entity is IModificationAudit modificationAudit) + modificationAudit.SetUpdatedInfo(now, userId); + else if (entry.State == EntityState.Deleted && entry.Entity is ISoftDeletable softDeletable && !softDeletable.IsHardDeleted) { - case EntityState.Added when entry.Entity is ICreationAudit creationAudit: - creationAudit.SetCreatedInfo(now, userId); - break; - - case EntityState.Modified when entry.Entity is IModificationAudit modificationAudit: - modificationAudit.SetUpdatedInfo(now, userId); - break; - - case EntityState.Deleted when entry.Entity is ISoftDeletable softDeletable && !softDeletable.IsHardDeleted: - softDeletable.MarkAsDeleted(now, userId); - entry.State = EntityState.Modified; - break; + softDeletable.MarkAsDeleted(now, userId); + entry.State = EntityState.Modified; } } } diff --git a/CSharpEssentials.EntityFrameworkCore/Interceptors/DomainEvents/DomainEventInterceptor.cs b/CSharpEssentials.EntityFrameworkCore/Interceptors/DomainEvents/DomainEventInterceptor.cs index 804f211..be1a7c0 100644 --- a/CSharpEssentials.EntityFrameworkCore/Interceptors/DomainEvents/DomainEventInterceptor.cs +++ b/CSharpEssentials.EntityFrameworkCore/Interceptors/DomainEvents/DomainEventInterceptor.cs @@ -24,7 +24,7 @@ namespace CSharpEssentials.EntityFrameworkCore.Interceptors; /// Before-save events are always published directly via . /// /// -public sealed class DomainEventInterceptor( +public sealed partial class DomainEventInterceptor( ILogger logger, IServiceScopeFactory serviceScopeFactory) : SaveChangesInterceptor { @@ -118,7 +118,7 @@ private async Task DispatchAfterSaveEventsAsync(IDomainEvent[] events, Cancellat if (outbox is not null) { - logger.LogDebug("Storing {Count} domain events in outbox", events.Length); + LogStoringDomainEvents(events.Length); await outbox.StoreAsync(events, cancellationToken); } else @@ -137,9 +137,15 @@ private async Task PublishEventsDirectAsync(IServiceProvider provider, IDomainEv { IDomainEventPublisher publisher = provider.GetRequiredService(); - logger.LogDebug("Publishing {Count} domain events", events.Length); + LogPublishingDomainEvents(events.Length); foreach (IDomainEvent domainEvent in events) await publisher.PublishAsync(domainEvent, cancellationToken); } + + [LoggerMessage(Level = LogLevel.Debug, Message = "Storing {Count} domain events in outbox")] + private partial void LogStoringDomainEvents(int count); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Publishing {Count} domain events")] + private partial void LogPublishingDomainEvents(int count); } diff --git a/CSharpEssentials.EntityFrameworkCore/Pagination/Extensions.cs b/CSharpEssentials.EntityFrameworkCore/Pagination/Extensions.cs index dcc4f12..282eeff 100644 --- a/CSharpEssentials.EntityFrameworkCore/Pagination/Extensions.cs +++ b/CSharpEssentials.EntityFrameworkCore/Pagination/Extensions.cs @@ -1,10 +1,11 @@ using System.Collections.Concurrent; using System.Linq.Expressions; +using System.Reflection; using System.Runtime.CompilerServices; +using CSharpEssentials.Core; using CSharpEssentials.EntityFrameworkCore.Pagination.Requests; using CSharpEssentials.EntityFrameworkCore.Pagination.Responses; using Microsoft.EntityFrameworkCore; -using CSharpEssentials.Core; namespace CSharpEssentials.EntityFrameworkCore.Pagination; @@ -70,7 +71,7 @@ public static async Task> PaginateAsync.CompareTo), [typeof(TCursor)])!; + MethodInfo compareMethod = typeof(TCursor).GetMethod(nameof(IComparable<>.CompareTo), [typeof(TCursor)])!; Expression compareCall = Expression.Call(cursorSelector.Body, compareMethod, cursorConstant); Expression comparison = isAscending ? Expression.GreaterThan(compareCall, Expression.Constant(0)) diff --git a/CSharpEssentials.EntityFrameworkCore/Pagination/Responses/CursorPaginationResponse.cs b/CSharpEssentials.EntityFrameworkCore/Pagination/Responses/CursorPaginationResponse.cs index 44c8343..cfb984c 100644 --- a/CSharpEssentials.EntityFrameworkCore/Pagination/Responses/CursorPaginationResponse.cs +++ b/CSharpEssentials.EntityFrameworkCore/Pagination/Responses/CursorPaginationResponse.cs @@ -1,3 +1,3 @@ namespace CSharpEssentials.EntityFrameworkCore.Pagination.Responses; -public record CursorPaginationResponse(IReadOnlyList Items, TCursor? Next = default, bool HasMore = false); \ No newline at end of file +public record CursorPaginationResponse(IReadOnlyList Items, TCursor? Next = default, bool HasMore = false); diff --git a/CSharpEssentials.EntityFrameworkCore/Pagination/Responses/PaginationResponse.cs b/CSharpEssentials.EntityFrameworkCore/Pagination/Responses/PaginationResponse.cs index ea7c3f2..08b914c 100644 --- a/CSharpEssentials.EntityFrameworkCore/Pagination/Responses/PaginationResponse.cs +++ b/CSharpEssentials.EntityFrameworkCore/Pagination/Responses/PaginationResponse.cs @@ -1,5 +1,3 @@ -using System; - namespace CSharpEssentials.EntityFrameworkCore.Pagination.Responses; public record PaginationResponse( diff --git a/CSharpEssentials.Enums/CSharpEssentials.Enums.csproj b/CSharpEssentials.Enums/CSharpEssentials.Enums.csproj index a697938..36537dc 100644 --- a/CSharpEssentials.Enums/CSharpEssentials.Enums.csproj +++ b/CSharpEssentials.Enums/CSharpEssentials.Enums.csproj @@ -2,20 +2,21 @@ - net9.0;netstandard2.1;netstandard2.0 - + net11.0;net10.0;net9.0;netstandard2.1;netstandard2.0 + CSharpEssentials.Enums CSharpEssentials.Enums CSharpEssentials.Enums Enum utilities and attributes for enhanced enum functionality in C#. - Provides StringEnumAttribute and various enum helper methods for better enum handling and - serialization. + Provides StringEnumAttribute, compile-time enum extensions (ToOptimizedString, + ToSnakeCase, ToKebabCase, Parse, TryParse, IsDefined, GetNames, GetValues), + and a source generator for fast enum operations. Essential for working with enums in modern C# applications. - Enums,StringEnum,EnumAttribute,EnumUtils,Serialization,CSharpEssentials + Enums,StringEnum,EnumAttribute,EnumUtils,Serialization,SourceGenerator,CSharpEssentials icon.png Readme.MD @@ -24,8 +25,21 @@ true true portable + + + true + + + + + + + + + @@ -33,4 +47,22 @@ - \ No newline at end of file + + + + + + + + true + + + + + + + + diff --git a/CSharpEssentials.Enums/Generators/StringEnumGenerator.cs b/CSharpEssentials.Enums/Generators/StringEnumGenerator.cs new file mode 100644 index 0000000..bf03382 --- /dev/null +++ b/CSharpEssentials.Enums/Generators/StringEnumGenerator.cs @@ -0,0 +1,346 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CSharpEssentials.Enums; + +[Generator(LanguageNames.CSharp)] +public sealed class StringEnumGenerator : IIncrementalGenerator +{ + private const string AttributeName = "CSharpEssentials.Enums.StringEnumAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider enumSymbols = context.SyntaxProvider + .ForAttributeWithMetadataName( + AttributeName, + static (node, _) => node is EnumDeclarationSyntax, + static (ctx, _) => + (INamedTypeSymbol)ctx.TargetSymbol) + .Where(static symbol => symbol.DeclaredAccessibility != Accessibility.Private); + + context.RegisterSourceOutput(enumSymbols, static (spc, enumSymbol) => + { + string source = GenerateExtensionsClass(enumSymbol); + spc.AddSource($"{enumSymbol.Name}Extensions.g.cs", source); + }); + } + + private static string GenerateExtensionsClass(INamedTypeSymbol enumSymbol) + { + string ns = enumSymbol.ContainingNamespace.ToDisplayString(); + string name = enumSymbol.Name; + string fullName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + string accessibility = enumSymbol.DeclaredAccessibility == Accessibility.Public ? "public" : "internal"; + string underlyingType = enumSymbol.EnumUnderlyingType?.ToDisplayString() ?? "int"; + + IFieldSymbol[] members = [.. enumSymbol.GetMembers() + .OfType() + .Where(static f => f.ConstantValue is not null)]; + + StringBuilder sb = new(); + sb.AppendLine("#nullable enable"); + if (!string.IsNullOrEmpty(ns)) + { + sb.AppendLine($"namespace {ns};"); + } + + sb.AppendLine($"{accessibility} static class {name}Extensions"); + sb.AppendLine("{"); + + GenerateConstants(sb, members); + GenerateToOptimizedString(sb, members, fullName); + GenerateToSnakeCase(sb, members, fullName); + GenerateToKebabCase(sb, members, fullName); + GenerateToLowerCase(sb, members, fullName); + GenerateToUpperCase(sb, members, fullName); + GenerateIsDefined(sb, members, fullName); + GenerateTryParse(sb, members, fullName, underlyingType); + GenerateParse(sb, fullName); + GenerateGetNames(sb, members, fullName); + GenerateGetValues(sb, members, fullName); + GenerateAsUnderlyingType(sb, fullName, underlyingType); + GenerateFallbackHelpers(sb); + + sb.AppendLine("}"); + return sb.ToString(); + } + + private static void GenerateConstants(StringBuilder sb, IFieldSymbol[] members) + { + foreach (IFieldSymbol member in members) + { + string snake = ToSnakeCase(member.Name); + string kebab = ToKebabCase(member.Name); + sb.AppendLine($" public const string {member.Name}SnakeCase = \"{snake}\";"); + sb.AppendLine($" public const string {member.Name}KebabCase = \"{kebab}\";"); + } + + if (members.Length > 0) + { + sb.AppendLine(); + } + } + + private static void GenerateToOptimizedString(StringBuilder sb, IFieldSymbol[] members, string fullName) + { + sb.AppendLine($" public static string ToOptimizedString(this {fullName} value) => value switch"); + sb.AppendLine(" {"); + foreach (IFieldSymbol member in members) + { + sb.AppendLine($" {fullName}.{member.Name} => nameof({fullName}.{member.Name}),"); + } + sb.AppendLine($" _ => value.ToString()"); + sb.AppendLine(" };"); + sb.AppendLine(); + } + + private static void GenerateToSnakeCase(StringBuilder sb, IFieldSymbol[] members, string fullName) + { + sb.AppendLine($" public static string ToSnakeCase(this {fullName} value) => value switch"); + sb.AppendLine(" {"); + foreach (IFieldSymbol member in members) + { + string snake = ToSnakeCase(member.Name); + sb.AppendLine($" {fullName}.{member.Name} => \"{snake}\","); + } + sb.AppendLine($" _ => ToSnakeCaseFallback(value.ToOptimizedString())"); + sb.AppendLine(" };"); + sb.AppendLine(); + } + + private static void GenerateToKebabCase(StringBuilder sb, IFieldSymbol[] members, string fullName) + { + sb.AppendLine($" public static string ToKebabCase(this {fullName} value) => value switch"); + sb.AppendLine(" {"); + foreach (IFieldSymbol member in members) + { + string kebab = ToKebabCase(member.Name); + sb.AppendLine($" {fullName}.{member.Name} => \"{kebab}\","); + } + sb.AppendLine($" _ => ToKebabCaseFallback(value.ToOptimizedString())"); + sb.AppendLine(" };"); + sb.AppendLine(); + } + + private static void GenerateToLowerCase(StringBuilder sb, IFieldSymbol[] members, string fullName) + { + sb.AppendLine($" public static string ToLowerCase(this {fullName} value) => value switch"); + sb.AppendLine(" {"); + foreach (IFieldSymbol member in members) + { + sb.AppendLine($" {fullName}.{member.Name} => nameof({fullName}.{member.Name}).ToLowerInvariant(),"); + } + sb.AppendLine($" _ => value.ToString().ToLowerInvariant()"); + sb.AppendLine(" };"); + sb.AppendLine(); + } + + private static void GenerateToUpperCase(StringBuilder sb, IFieldSymbol[] members, string fullName) + { + sb.AppendLine($" public static string ToUpperCase(this {fullName} value) => value switch"); + sb.AppendLine(" {"); + foreach (IFieldSymbol member in members) + { + sb.AppendLine($" {fullName}.{member.Name} => nameof({fullName}.{member.Name}).ToUpperInvariant(),"); + } + sb.AppendLine($" _ => value.ToString().ToUpperInvariant()"); + sb.AppendLine(" };"); + sb.AppendLine(); + } + + private static void GenerateIsDefined(StringBuilder sb, IFieldSymbol[] members, string fullName) + { + sb.AppendLine($" public static bool IsDefined(string name) => name switch"); + sb.AppendLine(" {"); + foreach (IFieldSymbol member in members) + { + sb.AppendLine($" nameof({fullName}.{member.Name}) => true,"); + } + sb.AppendLine(" _ => false"); + sb.AppendLine(" };"); + sb.AppendLine(); + } + + private static void GenerateTryParse(StringBuilder sb, IFieldSymbol[] members, string fullName, string underlyingType) + { + sb.AppendLine($" public static bool TryParse(string? name, out {fullName} value)"); + sb.AppendLine(" {"); + sb.AppendLine(" switch (name)"); + sb.AppendLine(" {"); + foreach (IFieldSymbol member in members) + { + sb.AppendLine($" case string s when s.Equals(nameof({fullName}.{member.Name}), global::System.StringComparison.Ordinal):"); + sb.AppendLine($" value = {fullName}.{member.Name};"); + sb.AppendLine(" return true;"); + } + sb.AppendLine($" case string s when {underlyingType}.TryParse(s, out var numericValue):"); + sb.AppendLine($" value = ({fullName})numericValue;"); + sb.AppendLine(" return true;"); + sb.AppendLine(" default:"); + sb.AppendLine($" value = ({fullName})0;"); + sb.AppendLine(" return false;"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + private static void GenerateParse(StringBuilder sb, string fullName) + { + sb.AppendLine($" public static {fullName} Parse(string? name) =>"); + sb.AppendLine(" TryParse(name, out var value) ? value : ThrowValueNotFound(name);"); + sb.AppendLine(); + sb.AppendLine($" private static {fullName} ThrowValueNotFound(string? name) =>"); + sb.AppendLine(" throw new global::System.ArgumentException($\"Requested value '{name}' was not found.\");"); + sb.AppendLine(); + } + + private static void GenerateGetNames(StringBuilder sb, IFieldSymbol[] members, string fullName) + { + sb.AppendLine(" public static string[] GetNames() =>"); + sb.Append(" ["); + for (int i = 0; i < members.Length; i++) + { + if (i > 0) + sb.Append(", "); + sb.Append($"nameof({fullName}.{members[i].Name})"); + } + sb.AppendLine("];"); + sb.AppendLine(); + } + + private static void GenerateGetValues(StringBuilder sb, IFieldSymbol[] members, string fullName) + { + sb.AppendLine($" public static {fullName}[] GetValues() =>"); + sb.Append(" ["); + for (int i = 0; i < members.Length; i++) + { + if (i > 0) + sb.Append(", "); + sb.Append($"{fullName}.{members[i].Name}"); + } + sb.AppendLine("];"); + sb.AppendLine(); + } + + private static void GenerateAsUnderlyingType(StringBuilder sb, string fullName, string underlyingType) + { + sb.AppendLine($" public static {underlyingType} AsUnderlyingType(this {fullName} value) => ({underlyingType})value;"); + sb.AppendLine(); + } + + private static void GenerateFallbackHelpers(StringBuilder sb) + { + sb.AppendLine(" private static string ToSnakeCaseFallback(string input)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (string.IsNullOrEmpty(input)) return input;"); + sb.AppendLine(" global::System.Text.StringBuilder sb = new(input.Length + 4);"); + sb.AppendLine(" global::System.Globalization.UnicodeCategory previous = global::System.Globalization.UnicodeCategory.OtherSymbol;"); + sb.AppendLine(" bool isFirst = true;"); + sb.AppendLine(" for (int i = 0; i < input.Length; i++)"); + sb.AppendLine(" {"); + sb.AppendLine(" char c = input[i];"); + sb.AppendLine(" global::System.Globalization.UnicodeCategory current = char.GetUnicodeCategory(c);"); + sb.AppendLine(" bool insertSeparator = CheckCategory(previous, current);"); + sb.AppendLine(" bool isSpecial = IsSpecialCharacter(current);"); + sb.AppendLine(" if (!isSpecial)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (insertSeparator && !isFirst) sb.Append('_');"); + sb.AppendLine(" sb.Append(char.ToLowerInvariant(c));"); + sb.AppendLine(" isFirst = false;"); + sb.AppendLine(" }"); + sb.AppendLine(" previous = current;"); + sb.AppendLine(" }"); + sb.AppendLine(" return sb.ToString();"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" private static string ToKebabCaseFallback(string input)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (string.IsNullOrEmpty(input)) return input;"); + sb.AppendLine(" global::System.Text.StringBuilder sb = new(input.Length + 4);"); + sb.AppendLine(" global::System.Globalization.UnicodeCategory previous = global::System.Globalization.UnicodeCategory.OtherSymbol;"); + sb.AppendLine(" bool isFirst = true;"); + sb.AppendLine(" for (int i = 0; i < input.Length; i++)"); + sb.AppendLine(" {"); + sb.AppendLine(" char c = input[i];"); + sb.AppendLine(" global::System.Globalization.UnicodeCategory current = char.GetUnicodeCategory(c);"); + sb.AppendLine(" bool insertSeparator = CheckCategory(previous, current);"); + sb.AppendLine(" bool isSpecial = IsSpecialCharacter(current);"); + sb.AppendLine(" if (!isSpecial)"); + sb.AppendLine(" {"); + sb.AppendLine(" if (insertSeparator && !isFirst) sb.Append('-');"); + sb.AppendLine(" sb.Append(char.ToLowerInvariant(c));"); + sb.AppendLine(" isFirst = false;"); + sb.AppendLine(" }"); + sb.AppendLine(" previous = current;"); + sb.AppendLine(" }"); + sb.AppendLine(" return sb.ToString();"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" private static bool CheckCategory(global::System.Globalization.UnicodeCategory previous, global::System.Globalization.UnicodeCategory current) =>"); + sb.AppendLine(" previous != current && (current is global::System.Globalization.UnicodeCategory.UppercaseLetter || current is global::System.Globalization.UnicodeCategory.DecimalDigitNumber || IsSpecialCharacter(previous) && !IsSpecialCharacter(current));"); + sb.AppendLine(); + sb.AppendLine(" private static bool IsSpecialCharacter(global::System.Globalization.UnicodeCategory category) =>"); + sb.AppendLine(" category is not global::System.Globalization.UnicodeCategory.UppercaseLetter"); + sb.AppendLine(" and not global::System.Globalization.UnicodeCategory.LowercaseLetter"); + sb.AppendLine(" and not global::System.Globalization.UnicodeCategory.DecimalDigitNumber;"); + } + + private static string ToSnakeCase(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + StringBuilder sb = new(input.Length + 4); + System.Globalization.UnicodeCategory previous = System.Globalization.UnicodeCategory.OtherSymbol; + bool isFirst = true; + for (int i = 0; i < input.Length; i++) + { + char c = input[i]; + System.Globalization.UnicodeCategory current = char.GetUnicodeCategory(c); + bool insertSeparator = CheckCategory(previous, current); + bool isSpecial = IsSpecialCharacter(current); + if (!isSpecial) + { + if (insertSeparator && !isFirst) + sb.Append('_'); + sb.Append(char.ToLowerInvariant(c)); + isFirst = false; + } + previous = current; + } + return sb.ToString(); + } + + private static string ToKebabCase(string input) + { + if (string.IsNullOrEmpty(input)) + return input; + StringBuilder sb = new(input.Length + 4); + System.Globalization.UnicodeCategory previous = System.Globalization.UnicodeCategory.OtherSymbol; + bool isFirst = true; + for (int i = 0; i < input.Length; i++) + { + char c = input[i]; + System.Globalization.UnicodeCategory current = char.GetUnicodeCategory(c); + bool insertSeparator = CheckCategory(previous, current); + bool isSpecial = IsSpecialCharacter(current); + if (!isSpecial) + { + if (insertSeparator && !isFirst) + sb.Append('-'); + sb.Append(char.ToLowerInvariant(c)); + isFirst = false; + } + previous = current; + } + return sb.ToString(); + } + + private static bool CheckCategory(System.Globalization.UnicodeCategory previous, System.Globalization.UnicodeCategory current) => + previous != current && (current is System.Globalization.UnicodeCategory.UppercaseLetter || current is System.Globalization.UnicodeCategory.DecimalDigitNumber || IsSpecialCharacter(previous) && !IsSpecialCharacter(current)); + + private static bool IsSpecialCharacter(System.Globalization.UnicodeCategory category) => + category is not System.Globalization.UnicodeCategory.UppercaseLetter + and not System.Globalization.UnicodeCategory.LowercaseLetter + and not System.Globalization.UnicodeCategory.DecimalDigitNumber; +} diff --git a/CSharpEssentials.Enums/Readme.MD b/CSharpEssentials.Enums/Readme.MD index 7c2aaba..d53a518 100644 --- a/CSharpEssentials.Enums/Readme.MD +++ b/CSharpEssentials.Enums/Readme.MD @@ -1,10 +1,11 @@ # CSharpEssentials.Enums -`CSharpEssentials.Enums` is a minimal utility package defining metadata attributes for Enum handling. It is primarily designed to work in conjunction with **CSharpEssentials.Json** to control serialization behaviors. +`CSharpEssentials.Enums` provides enum metadata attributes and a built-in source generator for fast, compile-time enum operations. It is designed to work with **CSharpEssentials.Json** and **CSharpEssentials.EntityFrameworkCore** for serialization and storage scenarios. ## 🚀 Features - **StringEnumAttribute**: A marker attribute to explicitly indicate that an `Enum` should be serialized as a string rather than an integer. +- **Built-in Source Generator**: Apply `[StringEnum]` to enums and the generator produces compile-time optimized extension methods and constants. ## 📦 Installation @@ -28,5 +29,39 @@ public enum UserStatus } ``` +The source generator automatically creates `{Enum}Extensions` with: + +| Member | Description | +|--------|-------------| +| `ToOptimizedString()` | Fast `switch`-based string conversion | +| `ToSnakeCase()` | Compile-time snake_case string | +| `ToKebabCase()` | Compile-time kebab-case string | +| `ToLowerCase()` | Compile-time lower-case string | +| `ToUpperCase()` | Compile-time upper-case string | +| `{Member}SnakeCase` | Public constant for snake_case | +| `{Member}KebabCase` | Public constant for kebab-case | +| `IsDefined(string)` | Fast name lookup | +| `TryParse(string, out Enum)` | Fast parsing with numeric fallback | +| `Parse(string)` | Fast parsing (throws on failure) | +| `GetNames()` | Array of enum member names | +| `GetValues()` | Array of enum values | +| `AsUnderlyingType()` | Cast to backing numeric type | + +### Example + +```csharp +UserStatus s = UserStatus.Active; + +s.ToOptimizedString(); // "Active" +s.ToSnakeCase(); // "active" +s.ToKebabCase(); // "active" + +UserStatusExtensions.ActiveSnakeCase; // "active" +UserStatusExtensions.InactiveKebabCase; // "inactive" + +UserStatusExtensions.IsDefined("Active"); // true +UserStatusExtensions.TryParse("Inactive", out var v); // true, v = Inactive +``` + ### Integration -While this package only provides the attribute, it is intended to be used with serialization libraries (like `CSharpEssentials.Json`) that look for this marker to automatically apply string conversion logic. +This package is intended to be used with serialization libraries (like `CSharpEssentials.Json`) and ORMs (like `CSharpEssentials.EntityFrameworkCore`) that look for this marker to automatically apply string conversion logic. diff --git a/CSharpEssentials.Enums/StringEnumAttribute.cs b/CSharpEssentials.Enums/StringEnumAttribute.cs index 87fc711..bd3f3d2 100644 --- a/CSharpEssentials.Enums/StringEnumAttribute.cs +++ b/CSharpEssentials.Enums/StringEnumAttribute.cs @@ -1,4 +1,4 @@ namespace CSharpEssentials.Enums; [AttributeUsage(AttributeTargets.Enum, AllowMultiple = false, Inherited = false)] -public sealed class StringEnumAttribute : Attribute; \ No newline at end of file +public sealed class StringEnumAttribute : Attribute; diff --git a/CSharpEssentials.Errors/CSharpEssentials.Errors.csproj b/CSharpEssentials.Errors/CSharpEssentials.Errors.csproj index be9ac1c..617b781 100644 --- a/CSharpEssentials.Errors/CSharpEssentials.Errors.csproj +++ b/CSharpEssentials.Errors/CSharpEssentials.Errors.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 CSharpEssentials.Errors diff --git a/CSharpEssentials.Errors/Error.cs b/CSharpEssentials.Errors/Error.cs index f7b74bb..4148f3c 100644 --- a/CSharpEssentials.Errors/Error.cs +++ b/CSharpEssentials.Errors/Error.cs @@ -7,8 +7,8 @@ namespace CSharpEssentials.Errors; [JsonConstructor] private Error(string code, string description, ErrorType type, ErrorMetadata? metadata) { - Code = code; - Description = description; + Code = code ?? throw new ArgumentNullException(nameof(code)); + Description = description ?? throw new ArgumentNullException(nameof(description)); Type = type; NumericType = type.ToIntType(); Metadata = metadata; @@ -17,12 +17,12 @@ private Error(string code, string description, ErrorType type, ErrorMetadata? me /// /// Gets the unique error code. /// - public string Code { get; } + public string Code { get; init; } /// /// Gets the error description. /// - public string Description { get; } + public string Description { get; init; } /// /// Gets the error type. @@ -39,13 +39,13 @@ private Error(string code, string description, ErrorType type, ErrorMetadata? me /// public ErrorMetadata? Metadata { get; } - public static Error[] CreateMany(params IEnumerable errors) => errors.ToArray(); + public static Error[] CreateMany(params IEnumerable errors) => [.. errors]; /// /// Creates an from a code and description. /// /// /// - public static Error[] CreateMany(params IEnumerable> errors) => errors.SelectMany(x => x).ToArray(); + public static Error[] CreateMany(params IEnumerable> errors) => [.. errors.SelectMany(x => x)]; /// diff --git a/CSharpEssentials.Errors/ErrorMetadata.cs b/CSharpEssentials.Errors/ErrorMetadata.cs index db60da7..0315dca 100644 --- a/CSharpEssentials.Errors/ErrorMetadata.cs +++ b/CSharpEssentials.Errors/ErrorMetadata.cs @@ -1,4 +1,6 @@ +#if NET6_0_OR_GREATER using System.Globalization; +#endif using System.Text; namespace CSharpEssentials.Errors; @@ -9,18 +11,10 @@ public ErrorMetadata() { } public ErrorMetadata(IDictionary dictionary) : base(dictionary) { } public ErrorMetadata(params IEnumerable> collection) : base(collection) { } -#if NET8_0_OR_GREATER public ErrorMetadata(KeyValuePair keyValuePair) : this([keyValuePair]) { } -#else - public ErrorMetadata(KeyValuePair keyValuePair) : this(new[] { keyValuePair }) { } -#endif public ErrorMetadata(string key, object? value) : this(new KeyValuePair(key, value)) { } -#if NET8_0_OR_GREATER public static ErrorMetadata CreateEmpty() => []; -#else - public static ErrorMetadata CreateEmpty() => new ErrorMetadata(); -#endif public static ErrorMetadata CreateWithStackTrace() => new("stackTrace", Environment.StackTrace); public static ErrorMetadata CreateWithException(Exception ex) => new("exception", ex); public static ErrorMetadata CreateWithExceptionDetailed(Exception exception) diff --git a/CSharpEssentials.Errors/ErrorType.cs b/CSharpEssentials.Errors/ErrorType.cs index 26516cd..f5a082e 100644 --- a/CSharpEssentials.Errors/ErrorType.cs +++ b/CSharpEssentials.Errors/ErrorType.cs @@ -16,4 +16,4 @@ public enum ErrorType Unauthorized, Forbidden, Unknown -} \ No newline at end of file +} diff --git a/CSharpEssentials.Errors/Exceptions/DomainException.cs b/CSharpEssentials.Errors/Exceptions/DomainException.cs index bae62c2..ddd7b23 100644 --- a/CSharpEssentials.Errors/Exceptions/DomainException.cs +++ b/CSharpEssentials.Errors/Exceptions/DomainException.cs @@ -4,11 +4,8 @@ namespace CSharpEssentials.Exceptions; public class DomainException : Exception { - public DomainException(Error error) - : base(error.Description) - { - Error = error; - } + public DomainException(Error error) + : base(error.Description) => Error = error; public Error Error { get; } } diff --git a/CSharpEssentials.Errors/Exceptions/EnhancedValidationException.cs b/CSharpEssentials.Errors/Exceptions/EnhancedValidationException.cs index 4f3dfbd..b410365 100644 --- a/CSharpEssentials.Errors/Exceptions/EnhancedValidationException.cs +++ b/CSharpEssentials.Errors/Exceptions/EnhancedValidationException.cs @@ -4,11 +4,8 @@ namespace CSharpEssentials.Exceptions; public class EnhancedValidationException : Exception { - public EnhancedValidationException(Error[] errors) - : base($"Validation failed with {errors.Length} errors") - { - Errors = errors ?? throw new ArgumentNullException(nameof(errors)); - } + public EnhancedValidationException(Error[] errors) + : base($"Validation failed with {errors.Length} errors") => Errors = errors ?? throw new ArgumentNullException(nameof(errors)); public Error[] Errors { get; } } diff --git a/CSharpEssentials.GcpSecretManager/CSharpEssentials.GcpSecretManager.csproj b/CSharpEssentials.GcpSecretManager/CSharpEssentials.GcpSecretManager.csproj index cda7c06..bdaaffb 100644 --- a/CSharpEssentials.GcpSecretManager/CSharpEssentials.GcpSecretManager.csproj +++ b/CSharpEssentials.GcpSecretManager/CSharpEssentials.GcpSecretManager.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 True diff --git a/CSharpEssentials.GcpSecretManager/Configuration/GoogleSecretManagerConfig.cs b/CSharpEssentials.GcpSecretManager/Configuration/GoogleSecretManagerConfig.cs index 54068f4..735093c 100644 --- a/CSharpEssentials.GcpSecretManager/Configuration/GoogleSecretManagerConfig.cs +++ b/CSharpEssentials.GcpSecretManager/Configuration/GoogleSecretManagerConfig.cs @@ -6,6 +6,6 @@ internal sealed class GoogleSecretManagerConfig #if NET8_0_OR_GREATER public List Projects { get; set; } = []; #else - public List Projects { get; set; } = new List(); + public List Projects { get; set; } = []; #endif -} \ No newline at end of file +} diff --git a/CSharpEssentials.GcpSecretManager/Configuration/ISecretManagerConfigurationLoader.cs b/CSharpEssentials.GcpSecretManager/Configuration/ISecretManagerConfigurationLoader.cs index b75e892..0d7b678 100644 --- a/CSharpEssentials.GcpSecretManager/Configuration/ISecretManagerConfigurationLoader.cs +++ b/CSharpEssentials.GcpSecretManager/Configuration/ISecretManagerConfigurationLoader.cs @@ -41,28 +41,18 @@ public string GetKey(string keyId) public bool ShouldLoadSecret(Secret secret, ProjectSecretConfiguration projectConfig) { - #if NET6_0_OR_GREATER - - ArgumentNullException.ThrowIfNull(secret); - - #else - - if (secret is null) - - throw new ArgumentNullException(nameof(secret)); - - #endif - #if NET6_0_OR_GREATER - - ArgumentNullException.ThrowIfNull(projectConfig); - - #else - - if (projectConfig is null) - - throw new ArgumentNullException(nameof(projectConfig)); - - #endif +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(secret); +#else + if (secret is null) + throw new ArgumentNullException(nameof(secret)); +#endif +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(projectConfig); +#else + if (projectConfig is null) + throw new ArgumentNullException(nameof(projectConfig)); +#endif if (projectConfig.PrefixFilters.Count == 0 && projectConfig.SecretIds.Count == 0) return true; @@ -71,4 +61,4 @@ public bool ShouldLoadSecret(Secret secret, ProjectSecretConfiguration projectCo return projectConfig.SecretIds.Contains(secretId) || projectConfig.PrefixFilters.Any(secretId.StartsWith); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.GcpSecretManager/Configuration/SecretManagerConfigurationOptions.cs b/CSharpEssentials.GcpSecretManager/Configuration/SecretManagerConfigurationOptions.cs index 7cb36b6..d104005 100644 --- a/CSharpEssentials.GcpSecretManager/Configuration/SecretManagerConfigurationOptions.cs +++ b/CSharpEssentials.GcpSecretManager/Configuration/SecretManagerConfigurationOptions.cs @@ -12,11 +12,7 @@ public sealed class SecretManagerConfigurationOptions /// public string? CredentialsPath { get; init; } -#if NET8_0_OR_GREATER private readonly List _projects = []; -#else - private readonly List _projects = new List(); -#endif /// /// Gets the list of project configurations. @@ -54,17 +50,12 @@ public sealed class SecretManagerConfigurationOptions /// The project configuration to add. public void AddProject(ProjectSecretConfiguration project) { - #if NET6_0_OR_GREATER - - ArgumentNullException.ThrowIfNull(project); - - #else - - if (project is null) - - throw new ArgumentNullException(nameof(project)); - - #endif +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(project); +#else + if (project is null) + throw new ArgumentNullException(nameof(project)); +#endif _projects.Add(project); } @@ -94,4 +85,4 @@ internal void LoadFromConfiguration(IConfiguration configuration) AddProject(project); } } -} \ No newline at end of file +} diff --git a/CSharpEssentials.GcpSecretManager/Configuration/SecretManagerConfigurationProvider.cs b/CSharpEssentials.GcpSecretManager/Configuration/SecretManagerConfigurationProvider.cs index 6451296..a58697e 100644 --- a/CSharpEssentials.GcpSecretManager/Configuration/SecretManagerConfigurationProvider.cs +++ b/CSharpEssentials.GcpSecretManager/Configuration/SecretManagerConfigurationProvider.cs @@ -20,8 +20,7 @@ SecretManagerConfigurationOptions options private const char _separator = ':'; private static readonly AsyncRetryPolicy _retryPolicy = Policy - .Handle(ex => ex.StatusCode == StatusCode.ResourceExhausted || - ex.StatusCode == StatusCode.Unavailable) + .Handle(ex => ex.StatusCode is StatusCode.ResourceExhausted or StatusCode.Unavailable) .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); private readonly ConcurrentDictionary _data = new(); @@ -65,16 +64,16 @@ public async Task LoadAsync() ListSecretsAsync(context, parent)).ConfigureAwait(false); if (secrets.Count == 0) - return new Dictionary(); + return []; var filteredSecrets = secrets .Where(secret => loader.ShouldLoadSecret(secret, context.Config)) .ToList(); if (filteredSecrets.Count == 0) - return new Dictionary(); + return []; - Dictionary resultDict = new(StringComparer.Ordinal); + Dictionary resultDict = []; for (int i = 0; i < filteredSecrets.Count; i += options.BatchSize) { @@ -90,7 +89,7 @@ public async Task LoadAsync() catch (Exception ex) { await Console.Error.WriteLineAsync($"Error loading secrets for {parent}: {ex}"); - return new Dictionary(StringComparer.Ordinal); + return []; } } @@ -166,7 +165,7 @@ private async Task> ListSecretsAsync(ProjectSecretLoadContext conte catch (Exception ex) { await Console.Error.WriteLineAsync($"Error listing secrets: {ex.Message}"); - return new List(); + return []; } } @@ -187,6 +186,7 @@ private async Task LoadSecretValueAsync( private static void FlattenJson(IDictionary data, JsonElement element, string parentPath) { +#pragma warning disable IDE0010 switch (element.ValueKind) { case JsonValueKind.Object: @@ -216,5 +216,6 @@ private static void FlattenJson(IDictionary data, JsonElement e data[parentPath] = element.ToString(); break; } +#pragma warning restore IDE0010 } -} \ No newline at end of file +} diff --git a/CSharpEssentials.GcpSecretManager/Extensions/SecretManagerConfigurationProviderExtensions.cs b/CSharpEssentials.GcpSecretManager/Extensions/SecretManagerConfigurationProviderExtensions.cs index 5ffaf46..3ff172e 100644 --- a/CSharpEssentials.GcpSecretManager/Extensions/SecretManagerConfigurationProviderExtensions.cs +++ b/CSharpEssentials.GcpSecretManager/Extensions/SecretManagerConfigurationProviderExtensions.cs @@ -29,4 +29,4 @@ public static IConfigurationManager AddGcpSecretManager( configuration.Add(new SecretManagerConfigurationSource(configurationOptions)); return configuration; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.GcpSecretManager/Infrastructure/SecretManagerPaths.cs b/CSharpEssentials.GcpSecretManager/Infrastructure/SecretManagerPaths.cs index 7b3e4b0..91eff8b 100644 --- a/CSharpEssentials.GcpSecretManager/Infrastructure/SecretManagerPaths.cs +++ b/CSharpEssentials.GcpSecretManager/Infrastructure/SecretManagerPaths.cs @@ -11,4 +11,4 @@ internal static string BuildSecretPath(string projectId, string? region, string => string.IsNullOrEmpty(region) ? $"projects/{projectId}/secrets/{secretId}/versions/latest" : $"projects/{projectId}/locations/{region}/secrets/{secretId}/versions/latest"; -} \ No newline at end of file +} diff --git a/CSharpEssentials.GcpSecretManager/Infrastructure/ServiceClientHelper.cs b/CSharpEssentials.GcpSecretManager/Infrastructure/ServiceClientHelper.cs index 2cbc460..44483ba 100644 --- a/CSharpEssentials.GcpSecretManager/Infrastructure/ServiceClientHelper.cs +++ b/CSharpEssentials.GcpSecretManager/Infrastructure/ServiceClientHelper.cs @@ -16,8 +16,7 @@ internal interface IServiceClientHelper internal sealed class ServiceClientHelper : IServiceClientHelper { private static readonly AsyncRetryPolicy RetryPolicy = Policy - .Handle(ex => ex.StatusCode == StatusCode.ResourceExhausted || - ex.StatusCode == StatusCode.Unavailable) + .Handle(ex => ex.StatusCode is StatusCode.ResourceExhausted or StatusCode.Unavailable) .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); @@ -29,7 +28,8 @@ public SecretManagerServiceClient Create(string credentialsPath) #if NET6_0_OR_GREATER ArgumentException.ThrowIfNullOrEmpty(credentialsPath); #else - if (string.IsNullOrEmpty(credentialsPath)) throw new ArgumentException("Value cannot be null or empty.", nameof(credentialsPath)); + if (string.IsNullOrEmpty(credentialsPath)) + throw new ArgumentException("Value cannot be null or empty.", nameof(credentialsPath)); #endif var clientBuilder = new SecretManagerServiceClientBuilder @@ -66,4 +66,4 @@ public SecretManagerServiceClient CreateWithRegion(string credentialsPath, strin return RetryPolicy.ExecuteAsync(() => clientBuilder.BuildAsync()).GetAwaiter().GetResult(); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.GcpSecretManager/Models/ProjectSecretConfiguration.cs b/CSharpEssentials.GcpSecretManager/Models/ProjectSecretConfiguration.cs index ceb3943..b8ff197 100644 --- a/CSharpEssentials.GcpSecretManager/Models/ProjectSecretConfiguration.cs +++ b/CSharpEssentials.GcpSecretManager/Models/ProjectSecretConfiguration.cs @@ -1,4 +1,3 @@ - namespace CSharpEssentials.GcpSecretManager; /// @@ -24,40 +23,24 @@ public sealed record ProjectSecretConfiguration /// Gets the list of prefix filters for secret IDs. /// Only secrets with IDs starting with these prefixes will be loaded. /// -#if NET8_0_OR_GREATER public IReadOnlyList PrefixFilters { get; init; } = []; -#else - public IReadOnlyList PrefixFilters { get; init; } = Array.Empty(); -#endif /// /// Gets the list of specific secret IDs to load. /// Only these secrets will be loaded if specified. /// -#if NET8_0_OR_GREATER public IReadOnlyList SecretIds { get; init; } = []; -#else - public IReadOnlyList SecretIds { get; init; } = Array.Empty(); -#endif /// /// Gets the list of specific secret IDs that should be treated as raw strings (not parsed as JSON). /// -#if NET8_0_OR_GREATER public IReadOnlyList RawSecretIds { get; init; } = []; -#else - public IReadOnlyList RawSecretIds { get; init; } = Array.Empty(); -#endif /// /// Gets the list of prefixes for secrets that should be treated as raw strings (not parsed as JSON). /// Any secret with an ID starting with these prefixes will be treated as raw. /// -#if NET8_0_OR_GREATER public IReadOnlyList RawSecretPrefixes { get; init; } = []; -#else - public IReadOnlyList RawSecretPrefixes { get; init; } = Array.Empty(); -#endif /// /// Checks if a secret should be treated as raw string based on its ID. @@ -69,4 +52,4 @@ public bool IsRawSecret(string secretId) return RawSecretIds.Contains(secretId) || RawSecretPrefixes.Any(prefix => secretId.StartsWith(prefix, StringComparison.Ordinal)); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.GcpSecretManager/Models/ProjectSecretLoadContext.cs b/CSharpEssentials.GcpSecretManager/Models/ProjectSecretLoadContext.cs index 5aa357f..6be56b0 100644 --- a/CSharpEssentials.GcpSecretManager/Models/ProjectSecretLoadContext.cs +++ b/CSharpEssentials.GcpSecretManager/Models/ProjectSecretLoadContext.cs @@ -6,4 +6,4 @@ namespace CSharpEssentials.GcpSecretManager.Models.Internal; internal sealed record ProjectSecretLoadContext( SecretManagerServiceClient Client, ProjectName ProjectName, - ProjectSecretConfiguration Config); \ No newline at end of file + ProjectSecretConfiguration Config); diff --git a/CSharpEssentials.GcpSecretManager/Models/SecretLoadResult.cs b/CSharpEssentials.GcpSecretManager/Models/SecretLoadResult.cs index c316390..1258d16 100644 --- a/CSharpEssentials.GcpSecretManager/Models/SecretLoadResult.cs +++ b/CSharpEssentials.GcpSecretManager/Models/SecretLoadResult.cs @@ -1,3 +1,3 @@ namespace CSharpEssentials.GcpSecretManager.Models.Internal; -internal sealed record SecretLoadResult(string Path, string Value, string Key); \ No newline at end of file +internal sealed record SecretLoadResult(string Path, string Value, string Key); diff --git a/CSharpEssentials.Http/CSharpEssentials.Http.csproj b/CSharpEssentials.Http/CSharpEssentials.Http.csproj new file mode 100644 index 0000000..7ace6bd --- /dev/null +++ b/CSharpEssentials.Http/CSharpEssentials.Http.csproj @@ -0,0 +1,39 @@ + + + + net11.0;net10.0;net9.0;netstandard2.1 + CSharpEssentials.Http + CSharp Essentials Http + CSharp Essentials Http + HttpClient extensions that bridge HTTP calls to the Result pattern. + Provides GetFromJsonAsResultAsync, PostAsJsonAsResultAsync, status code mapping, + and Polly resilience integration. + HttpClient,Result,Error,Http,Polly,Resilience,FunctionalProgramming,CSharpEssentials + icon.png + Readme.MD + true + true + portable + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CSharpEssentials.Http/HttpClientResilienceExtensions.cs b/CSharpEssentials.Http/HttpClientResilienceExtensions.cs new file mode 100644 index 0000000..98af28d --- /dev/null +++ b/CSharpEssentials.Http/HttpClientResilienceExtensions.cs @@ -0,0 +1,228 @@ +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; +using Polly; +using Polly.CircuitBreaker; +using Polly.Retry; +using Polly.Timeout; + +namespace CSharpEssentials.Http; + +public static class HttpClientResilienceExtensions +{ + public static ResiliencePipeline CreateRetryPipeline(int maxRetryAttempts = 3, TimeSpan? delay = null) + { + TimeSpan baseDelay = delay ?? TimeSpan.FromSeconds(1); + + return new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + MaxRetryAttempts = maxRetryAttempts, + Delay = baseDelay, + BackoffType = DelayBackoffType.Exponential, + ShouldHandle = new PredicateBuilder() + .Handle() + .Handle() + .Handle() + .Handle(ex => !ex.CancellationToken.IsCancellationRequested) + }) + .Build(); + } + + public static ResiliencePipeline> CreateRetryPipeline(int maxRetryAttempts = 3, TimeSpan? delay = null) + { + TimeSpan baseDelay = delay ?? TimeSpan.FromSeconds(1); + + return new ResiliencePipelineBuilder>() + .AddRetry(new RetryStrategyOptions> + { + MaxRetryAttempts = maxRetryAttempts, + Delay = baseDelay, + BackoffType = DelayBackoffType.Exponential, + ShouldHandle = new PredicateBuilder>() + .HandleResult(IsRetryable) + .Handle() + .Handle() + .Handle() + .Handle(ex => !ex.CancellationToken.IsCancellationRequested) + }) + .Build(); + } + + public static ResiliencePipeline CreateTimeoutPipeline(TimeSpan timeout) + { + return new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions + { + Timeout = timeout + }) + .Build(); + } + + public static ResiliencePipeline CreateCircuitBreakerPipeline(int minimumThroughput = 5, TimeSpan? samplingDuration = null, TimeSpan? breakDuration = null) + { + return new ResiliencePipelineBuilder() + .AddCircuitBreaker(new CircuitBreakerStrategyOptions + { + FailureRatio = 0.5, + MinimumThroughput = minimumThroughput, + SamplingDuration = samplingDuration ?? TimeSpan.FromMinutes(1), + BreakDuration = breakDuration ?? TimeSpan.FromSeconds(30), + ShouldHandle = new PredicateBuilder() + .Handle() + .Handle() + .Handle() + .Handle(ex => !ex.CancellationToken.IsCancellationRequested) + }) + .Build(); + } + + public static ResiliencePipeline> CreateCircuitBreakerPipeline(int minimumThroughput = 5, TimeSpan? samplingDuration = null, TimeSpan? breakDuration = null) + { + return new ResiliencePipelineBuilder>() + .AddCircuitBreaker(new CircuitBreakerStrategyOptions> + { + FailureRatio = 0.5, + MinimumThroughput = minimumThroughput, + SamplingDuration = samplingDuration ?? TimeSpan.FromMinutes(1), + BreakDuration = breakDuration ?? TimeSpan.FromSeconds(30), + ShouldHandle = new PredicateBuilder>() + .HandleResult(IsRetryable) + .Handle() + .Handle() + .Handle() + .Handle(ex => !ex.CancellationToken.IsCancellationRequested) + }) + .Build(); + } + + public static ResiliencePipeline CreateResiliencePipeline(int maxRetryAttempts = 3, TimeSpan? timeout = null, TimeSpan? retryDelay = null) + { + TimeSpan effectiveTimeout = timeout ?? TimeSpan.FromSeconds(30); + TimeSpan effectiveDelay = retryDelay ?? TimeSpan.FromSeconds(1); + + return new ResiliencePipelineBuilder() + .AddTimeout(new TimeoutStrategyOptions + { + Timeout = effectiveTimeout + }) + .AddRetry(new RetryStrategyOptions + { + MaxRetryAttempts = maxRetryAttempts, + Delay = effectiveDelay, + BackoffType = DelayBackoffType.Exponential, + ShouldHandle = new PredicateBuilder() + .Handle() + .Handle() + .Handle() + .Handle(ex => !ex.CancellationToken.IsCancellationRequested) + }) + .Build(); + } + + public static ResiliencePipeline> CreateResiliencePipeline(int maxRetryAttempts = 3, TimeSpan? timeout = null, TimeSpan? retryDelay = null) + { + TimeSpan effectiveTimeout = timeout ?? TimeSpan.FromSeconds(30); + TimeSpan effectiveDelay = retryDelay ?? TimeSpan.FromSeconds(1); + + return new ResiliencePipelineBuilder>() + .AddTimeout(new TimeoutStrategyOptions + { + Timeout = effectiveTimeout + }) + .AddRetry(new RetryStrategyOptions> + { + MaxRetryAttempts = maxRetryAttempts, + Delay = effectiveDelay, + BackoffType = DelayBackoffType.Exponential, + ShouldHandle = new PredicateBuilder>() + .HandleResult(r => IsRetryable(r)) + .Handle() + .Handle() + .Handle() + .Handle(ex => !ex.CancellationToken.IsCancellationRequested) + }) + .Build(); + } + + public static async Task ExecuteAsResultAsync( + this ResiliencePipeline pipeline, + Func> callback, + CancellationToken cancellationToken = default) + { + return await ExecuteResilienceAsync(async () => + await pipeline.ExecuteAsync(async token => await callback(token), cancellationToken)); + } + + public static Task> ExecuteAsResultAsync( + this ResiliencePipeline pipeline, + Func>> callback, + CancellationToken cancellationToken = default) + { + return Result.TryAsync( + () => pipeline.ExecuteAsync(async token => await callback(token), cancellationToken).AsTask(), + HandleException, + cancellationToken); + } + + public static Task> ExecuteAsResultAsync( + this ResiliencePipeline> pipeline, + Func>> callback, + CancellationToken cancellationToken = default) + { + return Result.TryAsync( + () => pipeline.ExecuteAsync(async token => await callback(token), cancellationToken).AsTask(), + HandleException, + cancellationToken); + } + + private static bool IsRetryable(Result result) + { + if (result.IsSuccess) + return false; + + ErrorType type = result.FirstError.Type; + return type is not ErrorType.Unauthorized + and not ErrorType.Forbidden + and not ErrorType.NotFound + and not ErrorType.Validation; + } + + private static Error HandleException(Exception ex) + { + if (ex is OperationCanceledException oce && oce.CancellationToken.IsCancellationRequested) + throw new OperationCanceledException(oce.Message, oce, oce.CancellationToken); + + if (ex is BrokenCircuitException) + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex).Throw(); + + return Error.Exception(ex, ErrorType.Unexpected); + } + + private static async Task ExecuteResilienceAsync(Func> action) + { + try + { + return await action(); + } + catch (TimeoutRejectedException ex) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + catch (HttpRequestException ex) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + catch (IOException ex) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + catch (TaskCanceledException ex) when (!ex.CancellationToken.IsCancellationRequested) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + catch (Exception ex) when (ex is not OperationCanceledException and not BrokenCircuitException) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + } +} diff --git a/CSharpEssentials.Http/HttpClientResultExtensions.cs b/CSharpEssentials.Http/HttpClientResultExtensions.cs new file mode 100644 index 0000000..48dc740 --- /dev/null +++ b/CSharpEssentials.Http/HttpClientResultExtensions.cs @@ -0,0 +1,195 @@ +using System.Net.Http.Json; +using System.Text.Json; +using CSharpEssentials.Errors; +using CSharpEssentials.Json; +using CSharpEssentials.ResultPattern; + +namespace CSharpEssentials.Http; + +public static class HttpClientResultExtensions +{ + public static Task> GetFromJsonAsResultAsync( + this HttpClient client, + Uri? requestUri, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + return Result.TryAsync( + async () => + { + HttpResponseMessage response = await client.GetAsync(requestUri, cancellationToken); + return await HandleResponseAsync(response, options, cancellationToken); + }, + HandleException, + cancellationToken); + } + + public static Task> PostAsJsonAsResultAsync( + this HttpClient client, + Uri? requestUri, + object value, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + return Result.TryAsync( + async () => + { + HttpResponseMessage response = await client.PostAsJsonAsync(requestUri, value, options ?? EnhancedJsonSerializerOptions.DefaultOptions, cancellationToken); + return await HandleResponseAsync(response, options, cancellationToken); + }, + HandleException, + cancellationToken); + } + + public static async Task PostAsResultAsync( + this HttpClient client, + Uri? requestUri, + HttpContent content, + CancellationToken cancellationToken = default) + { + return await ExecuteAsync(async () => + { + HttpResponseMessage response = await client.PostAsync(requestUri, content, cancellationToken); + return HandleResponse(response); + }); + } + + public static Task> PutAsJsonAsResultAsync( + this HttpClient client, + Uri? requestUri, + object value, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + return Result.TryAsync( + async () => + { + HttpResponseMessage response = await client.PutAsJsonAsync(requestUri, value, options ?? EnhancedJsonSerializerOptions.DefaultOptions, cancellationToken); + return await HandleResponseAsync(response, options, cancellationToken); + }, + HandleException, + cancellationToken); + } + + public static async Task PutAsResultAsync( + this HttpClient client, + Uri? requestUri, + HttpContent content, + CancellationToken cancellationToken = default) + { + return await ExecuteAsync(async () => + { + HttpResponseMessage response = await client.PutAsync(requestUri, content, cancellationToken); + return HandleResponse(response); + }); + } + + public static Task> PatchAsJsonAsResultAsync( + this HttpClient client, + Uri? requestUri, + object value, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + return Result.TryAsync( + async () => + { + using HttpContent content = JsonContent.Create(value, options: options ?? EnhancedJsonSerializerOptions.DefaultOptions); + HttpResponseMessage response = await client.PatchAsync(requestUri, content, cancellationToken); + return await HandleResponseAsync(response, options, cancellationToken); + }, + HandleException, + cancellationToken); + } + + public static async Task DeleteAsResultAsync( + this HttpClient client, + Uri? requestUri, + CancellationToken cancellationToken = default) + { + return await ExecuteAsync(async () => + { + HttpResponseMessage response = await client.DeleteAsync(requestUri, cancellationToken); + return HandleResponse(response); + }); + } + + public static async Task SendAsResultAsync( + this HttpClient client, + HttpRequestMessage request, + CancellationToken cancellationToken = default) + { + return await ExecuteAsync(async () => + { + HttpResponseMessage response = await client.SendAsync(request, cancellationToken); + return HandleResponse(response); + }); + } + + public static Task> SendAsResultAsync( + this HttpClient client, + HttpRequestMessage request, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + return Result.TryAsync( + async () => + { + HttpResponseMessage response = await client.SendAsync(request, cancellationToken); + return await HandleResponseAsync(response, options, cancellationToken); + }, + HandleException, + cancellationToken); + } + + private static Error HandleException(Exception ex) + { + if (ex is OperationCanceledException oce && oce.CancellationToken.IsCancellationRequested) + throw new OperationCanceledException(oce.Message, oce, oce.CancellationToken); + + return Error.Exception(ex, ErrorType.Unexpected); + } + + private static async Task ExecuteAsync(Func> action) + { + try + { + return await action(); + } + catch (HttpRequestException ex) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + catch (IOException ex) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + catch (TaskCanceledException ex) when (!ex.CancellationToken.IsCancellationRequested) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + } + + private static async Task> HandleResponseAsync(HttpResponseMessage response, JsonSerializerOptions? options, CancellationToken cancellationToken) + { + if (response.IsSuccessStatusCode) + { + T? value = await response.Content.ReadFromJsonAsync(options ?? EnhancedJsonSerializerOptions.DefaultOptions, cancellationToken); + if (value is null) + return Error.NotFound(description: "Response body was empty or could not be deserialized."); + return value; + } + + var error = HttpStatusCodeMapper.ToError(response.StatusCode); + return error; + } + + private static Result HandleResponse(HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + return Result.Success(); + + var error = HttpStatusCodeMapper.ToError(response.StatusCode); + return error; + } +} diff --git a/CSharpEssentials.Http/HttpContentExtensions.cs b/CSharpEssentials.Http/HttpContentExtensions.cs new file mode 100644 index 0000000..05c22a4 --- /dev/null +++ b/CSharpEssentials.Http/HttpContentExtensions.cs @@ -0,0 +1,61 @@ +using System.Net.Http.Json; +using System.Text.Json; +using CSharpEssentials.Errors; +using CSharpEssentials.Json; +using CSharpEssentials.ResultPattern; + +namespace CSharpEssentials.Http; + +public static class HttpContentExtensions +{ + public static async Task> ReadAsStringAsResultAsync(this HttpContent content, CancellationToken cancellationToken = default) + { + try + { +#if NETSTANDARD2_1 + string value = await content.ReadAsStringAsync(); +#else + string value = await content.ReadAsStringAsync(cancellationToken); +#endif + return value; + } + catch (OperationCanceledException oce) when (oce.CancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(oce.Message, oce, oce.CancellationToken); + } + catch (JsonException ex) + { + return Error.Exception(ex, ErrorType.Validation); + } + catch (Exception ex) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + } + + public static async Task> ReadFromJsonAsResultAsync( + this HttpContent content, + JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) + { + try + { + T? value = await content.ReadFromJsonAsync(options ?? EnhancedJsonSerializerOptions.DefaultOptions, cancellationToken); + if (value is null) + return Error.NotFound(description: "Response body was empty or could not be deserialized."); + return value; + } + catch (OperationCanceledException oce) when (oce.CancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(oce.Message, oce, oce.CancellationToken); + } + catch (JsonException ex) + { + return Error.Exception(ex, ErrorType.Validation); + } + catch (Exception ex) + { + return Error.Exception(ex, ErrorType.Unexpected); + } + } +} diff --git a/CSharpEssentials.Http/HttpRequestBuilder.cs b/CSharpEssentials.Http/HttpRequestBuilder.cs new file mode 100644 index 0000000..7ae4485 --- /dev/null +++ b/CSharpEssentials.Http/HttpRequestBuilder.cs @@ -0,0 +1,137 @@ +using System.Net.Http.Json; +using System.Text.Json; +using CSharpEssentials.Errors; +using CSharpEssentials.Json; +using CSharpEssentials.ResultPattern; + +namespace CSharpEssentials.Http; + +public sealed class HttpRequestBuilder +{ + private HttpMethod _method = HttpMethod.Get; + private Uri? _uri; + private readonly List<(string Key, string Value)> _headers = []; + private readonly List<(string Key, string Value)> _queryParameters = []; + private HttpContent? _content; + + private HttpRequestBuilder() { } + + public static HttpRequestBuilder Get(string uri) => new() { _method = HttpMethod.Get, _uri = new Uri(uri, UriKind.RelativeOrAbsolute) }; + public static HttpRequestBuilder Get(Uri uri) => new() { _method = HttpMethod.Get, _uri = uri }; + public static HttpRequestBuilder Post(string uri) => new() { _method = HttpMethod.Post, _uri = new Uri(uri, UriKind.RelativeOrAbsolute) }; + public static HttpRequestBuilder Post(Uri uri) => new() { _method = HttpMethod.Post, _uri = uri }; + public static HttpRequestBuilder Put(string uri) => new() { _method = HttpMethod.Put, _uri = new Uri(uri, UriKind.RelativeOrAbsolute) }; + public static HttpRequestBuilder Put(Uri uri) => new() { _method = HttpMethod.Put, _uri = uri }; + public static HttpRequestBuilder Patch(string uri) => new() { _method = HttpMethod.Patch, _uri = new Uri(uri, UriKind.RelativeOrAbsolute) }; + public static HttpRequestBuilder Patch(Uri uri) => new() { _method = HttpMethod.Patch, _uri = uri }; + public static HttpRequestBuilder Delete(string uri) => new() { _method = HttpMethod.Delete, _uri = new Uri(uri, UriKind.RelativeOrAbsolute) }; + public static HttpRequestBuilder Delete(Uri uri) => new() { _method = HttpMethod.Delete, _uri = uri }; + + public HttpRequestBuilder WithMethod(HttpMethod method) + { + _method = method; + return this; + } + + public HttpRequestBuilder WithUri(Uri uri) + { + _uri = uri; + return this; + } + + public HttpRequestBuilder WithUri(string uri) + { + _uri = new Uri(uri, UriKind.RelativeOrAbsolute); + return this; + } + + public HttpRequestBuilder WithHeader(string name, string value) + { + _headers.Add((name, value)); + return this; + } + + public HttpRequestBuilder WithHeaders(Dictionary headers) + { + foreach (KeyValuePair header in headers) + _headers.Add((header.Key, header.Value)); + return this; + } + + public HttpRequestBuilder WithQuery(string name, string value) + { + _queryParameters.Add((name, value)); + return this; + } + + public HttpRequestBuilder WithQuery(Dictionary parameters) + { + foreach (KeyValuePair parameter in parameters) + { + if (parameter.Value is not null) + _queryParameters.Add((parameter.Key, parameter.Value)); + } + return this; + } + + public HttpRequestBuilder WithContent(HttpContent content) + { + _content = content; + return this; + } + + public HttpRequestBuilder WithJsonContent(object value, JsonSerializerOptions? options = null) + { + _content = JsonContent.Create(value, options: options ?? EnhancedJsonSerializerOptions.DefaultOptions); + return this; + } + + public Result Build() + { + if (_uri is null) + return Error.Validation("HttpRequestBuilder.UriRequired", "URI must be set before building the request."); + + Result uriResult = _queryParameters.Count > 0 + ? _uri.WithQueryString(_queryParameters.ToDictionary(p => p.Key, p => (string?)p.Value)) + : _uri!; + + if (uriResult.IsFailure) + return uriResult.Errors; + + var request = new HttpRequestMessage(_method, uriResult.Value); + + foreach ((string key, string value) in _headers) + request.Headers.TryAddWithoutValidation(key, value); + + if (_content is not null) + request.Content = _content; + + return request; + } + + public async Task AsResultAsync(HttpClient client, CancellationToken cancellationToken = default) + { + if (client is null) + return Error.Validation("HttpRequestBuilder.ClientRequired", "HttpClient cannot be null."); + + Result buildResult = Build(); + if (buildResult.IsFailure) + return buildResult.Errors; + + using HttpRequestMessage request = buildResult.Value; + return await client.SendAsResultAsync(request, cancellationToken); + } + + public async Task> AsResultAsync(HttpClient client, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) + { + if (client is null) + return Error.Validation("HttpRequestBuilder.ClientRequired", "HttpClient cannot be null."); + + Result buildResult = Build(); + if (buildResult.IsFailure) + return buildResult.Errors; + + using HttpRequestMessage request = buildResult.Value; + return await client.SendAsResultAsync(request, options, cancellationToken); + } +} diff --git a/CSharpEssentials.Http/HttpStatusCodeMapper.cs b/CSharpEssentials.Http/HttpStatusCodeMapper.cs new file mode 100644 index 0000000..4dec626 --- /dev/null +++ b/CSharpEssentials.Http/HttpStatusCodeMapper.cs @@ -0,0 +1,68 @@ +using System.Net; +using CSharpEssentials.Errors; + +namespace CSharpEssentials.Http; + +public static class HttpStatusCodeMapper +{ +#pragma warning disable IDE0072 + public static ErrorType ToErrorType(HttpStatusCode statusCode) + => statusCode switch + { + HttpStatusCode.BadRequest => ErrorType.Validation, + HttpStatusCode.Unauthorized => ErrorType.Unauthorized, + HttpStatusCode.PaymentRequired => ErrorType.Failure, + HttpStatusCode.Forbidden => ErrorType.Forbidden, + HttpStatusCode.NotFound => ErrorType.NotFound, + HttpStatusCode.MethodNotAllowed => ErrorType.Failure, + HttpStatusCode.NotAcceptable => ErrorType.Failure, + HttpStatusCode.ProxyAuthenticationRequired => ErrorType.Unauthorized, + HttpStatusCode.RequestTimeout => ErrorType.Unexpected, + HttpStatusCode.Conflict => ErrorType.Conflict, + HttpStatusCode.Gone => ErrorType.NotFound, + HttpStatusCode.LengthRequired => ErrorType.Validation, + HttpStatusCode.PreconditionFailed => ErrorType.Conflict, + HttpStatusCode.RequestEntityTooLarge => ErrorType.Validation, + HttpStatusCode.RequestUriTooLong => ErrorType.Validation, + HttpStatusCode.UnsupportedMediaType => ErrorType.Validation, + HttpStatusCode.RequestedRangeNotSatisfiable => ErrorType.Validation, + HttpStatusCode.ExpectationFailed => ErrorType.Validation, + HttpStatusCode.UnprocessableEntity => ErrorType.Validation, + HttpStatusCode.TooManyRequests => ErrorType.Conflict, + >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError => ErrorType.Failure, + >= HttpStatusCode.InternalServerError => ErrorType.Unexpected, + _ => ErrorType.Failure + }; + + public static Error ToError(HttpStatusCode statusCode, string? description = null) + { + string code = $"Http.{(int)statusCode}"; + string msg = description ?? $"HTTP request failed with status code {(int)statusCode} ({statusCode})."; + return statusCode switch + { + HttpStatusCode.BadRequest => Error.Validation(code, msg), + HttpStatusCode.Unauthorized => Error.Unauthorized(code, msg), + HttpStatusCode.PaymentRequired => Error.Failure(code, msg), + HttpStatusCode.Forbidden => Error.Forbidden(code, msg), + HttpStatusCode.NotFound => Error.NotFound(code, msg), + HttpStatusCode.MethodNotAllowed => Error.Failure(code, msg), + HttpStatusCode.NotAcceptable => Error.Failure(code, msg), + HttpStatusCode.ProxyAuthenticationRequired => Error.Unauthorized(code, msg), + HttpStatusCode.RequestTimeout => Error.Unexpected(code, msg), + HttpStatusCode.Conflict => Error.Conflict(code, msg), + HttpStatusCode.Gone => Error.NotFound(code, msg), + HttpStatusCode.LengthRequired => Error.Validation(code, msg), + HttpStatusCode.PreconditionFailed => Error.Conflict(code, msg), + HttpStatusCode.RequestEntityTooLarge => Error.Validation(code, msg), + HttpStatusCode.RequestUriTooLong => Error.Validation(code, msg), + HttpStatusCode.UnsupportedMediaType => Error.Validation(code, msg), + HttpStatusCode.RequestedRangeNotSatisfiable => Error.Validation(code, msg), + HttpStatusCode.ExpectationFailed => Error.Validation(code, msg), + HttpStatusCode.UnprocessableEntity => Error.Validation(code, msg), + HttpStatusCode.TooManyRequests => Error.Conflict(code, msg), + >= HttpStatusCode.InternalServerError => Error.Unexpected(code, msg), + _ => Error.Failure(code, msg) + }; + } +#pragma warning restore IDE0072 +} diff --git a/CSharpEssentials.Http/QueryStringExtensions.cs b/CSharpEssentials.Http/QueryStringExtensions.cs new file mode 100644 index 0000000..03956c2 --- /dev/null +++ b/CSharpEssentials.Http/QueryStringExtensions.cs @@ -0,0 +1,104 @@ +using System.Text; +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; + +namespace CSharpEssentials.Http; + +public static class QueryStringExtensions +{ + public static Result ToQueryString(this Dictionary parameters) + { + if (parameters.Count == 0) + return string.Empty; + + var builder = new StringBuilder(); + foreach (KeyValuePair parameter in parameters) + { + if (string.IsNullOrEmpty(parameter.Key)) + return Error.Validation("QueryString.EmptyKey", "Query parameter key cannot be null or empty."); + + if (parameter.Value is null) + continue; + + if (builder.Length > 0) + builder.Append('&'); + + builder.Append(Uri.EscapeDataString(parameter.Key)); + builder.Append('='); + builder.Append(Uri.EscapeDataString(parameter.Value)); + } + + return builder.ToString(); + } + + public static Result ToQueryString(this object source) + { + if (source is null) + return Error.Validation("QueryString.SourceRequired", "Source cannot be null."); + + var properties = source.GetType().GetProperties() + .Where(p => p.CanRead) + .Select(p => new KeyValuePair(p.Name, p.GetValue(source)?.ToString())) + .ToDictionary(p => p.Key, p => p.Value); + + return properties.ToQueryString(); + } + + public static Result WithQueryString(this Uri uri, Dictionary parameters) + { + if (uri is null) + return Error.Validation("QueryString.UriRequired", "URI cannot be null."); + + Result queryResult = parameters.ToQueryString(); + if (queryResult.IsFailure) + return queryResult.Errors; + + if (string.IsNullOrEmpty(queryResult.Value)) + return uri; + + var builder = new UriBuilder(uri); + builder.Query = string.IsNullOrEmpty(builder.Query) + ? queryResult.Value + : builder.Query.TrimStart('?') + "&" + queryResult.Value; + + return builder.Uri; + } + + public static Result WithQueryString(this Uri uri, object parameters) + { + if (uri is null) + return Error.Validation("QueryString.UriRequired", "URI cannot be null."); + if (parameters is null) + return Error.Validation("QueryString.ParametersRequired", "Parameters cannot be null."); + + Result queryResult = parameters.ToQueryString(); + if (queryResult.IsFailure) + return queryResult.Errors; + + if (string.IsNullOrEmpty(queryResult.Value)) + return uri; + + var builder = new UriBuilder(uri); + builder.Query = string.IsNullOrEmpty(builder.Query) + ? queryResult.Value + : builder.Query.TrimStart('?') + "&" + queryResult.Value; + + return builder.Uri; + } + + public static Result WithQueryString(this Uri uri, string name, string value) + { + if (uri is null) + return Error.Validation("QueryString.UriRequired", "URI cannot be null."); + if (string.IsNullOrEmpty(name)) + return Error.Validation("QueryString.NameRequired", "Query parameter name cannot be null or empty."); + + var builder = new UriBuilder(uri); + string encoded = Uri.EscapeDataString(name) + "=" + Uri.EscapeDataString(value); + builder.Query = string.IsNullOrEmpty(builder.Query) + ? encoded + : builder.Query.TrimStart('?') + "&" + encoded; + + return builder.Uri; + } +} diff --git a/CSharpEssentials.Http/Readme.MD b/CSharpEssentials.Http/Readme.MD new file mode 100644 index 0000000..b915f62 --- /dev/null +++ b/CSharpEssentials.Http/Readme.MD @@ -0,0 +1,138 @@ +# CSharpEssentials.Http + +`CSharpEssentials.Http` bridges `HttpClient` to the `Result` pattern. It turns HTTP calls into type-safe, functional results with automatic status-code-to-error mapping, fluent request builders, and Polly resilience integration. + +## 🚀 Features + +- **Result-based HTTP calls**: `GetFromJsonAsResultAsync`, `PostAsJsonAsResultAsync`, `PutAsJsonAsResultAsync`, `PatchAsJsonAsResultAsync`, `DeleteAsResultAsync`, `SendAsResultAsync` +- **Automatic error mapping**: HTTP status codes mapped to `ErrorType` (Validation, Unauthorized, Forbidden, NotFound, Conflict, Unexpected) +- **JSON integration**: Uses `EnhancedJsonSerializerOptions` from `CSharpEssentials.Json` by default +- **Null-safe deserialization**: Empty or null response bodies yield a structured `NotFound` error +- **Fluent request builder**: Chain methods to build and execute requests declaratively +- **Query string helpers**: Build URIs with query parameters from dictionaries or anonymous objects +- **Polly resilience**: Retry, timeout, and circuit breaker pipelines ready to use + +## 📦 Installation + +```bash +dotnet add package CSharpEssentials.Http +``` + +## 🛠 Usage + +### 1. GET Request as Result + +```csharp +using CSharpEssentials.Http; +using CSharpEssentials.ResultPattern; + +HttpClient client = new() { BaseAddress = new Uri("https://api.example.com") }; + +Result userResult = await client.GetFromJsonAsResultAsync(new Uri("/users/1", UriKind.Relative)); + +userResult.Match( + onSuccess: user => Console.WriteLine(user.Name), + onFailure: errors => Console.WriteLine(errors[0].Description) +); +``` + +### 2. POST, PUT, PATCH Requests as Result + +```csharp +var payload = new { Name = "Alice", Age = 30 }; + +Result postResult = await client.PostAsJsonAsResultAsync( + new Uri("/users", UriKind.Relative), + payload); + +Result putResult = await client.PutAsJsonAsResultAsync( + new Uri("/users/1", UriKind.Relative), + payload); + +Result patchResult = await client.PatchAsJsonAsResultAsync( + new Uri("/users/1", UriKind.Relative), + new { Age = 31 }); +``` + +### 3. DELETE and Raw Send as Result + +```csharp +Result deleteResult = await client.DeleteAsResultAsync(new Uri("/users/1", UriKind.Relative)); + +using var request = new HttpRequestMessage(HttpMethod.Head, new Uri("/health", UriKind.Relative)); +Result headResult = await client.SendAsResultAsync(request); +``` + +### 4. Status Code Mapping + +```csharp +using System.Net; +using CSharpEssentials.Http; + +ErrorType type = HttpStatusCodeMapper.ToErrorType(HttpStatusCode.NotFound); +// type == ErrorType.NotFound + +Error error = HttpStatusCodeMapper.ToError(HttpStatusCode.BadRequest); +// error.Code == "Http.400" +``` + +### 5. Query String Builder + +```csharp +Uri baseUri = new("https://api.example.com/search"); + +// Single parameter +Uri uri1 = baseUri.WithQueryString("q", "csharp"); + +// Multiple parameters from dictionary +var filters = new Dictionary { { "page", "1" }, { "limit", "10" } }; +Uri uri2 = baseUri.WithQueryString(filters); + +// From anonymous object +Uri uri3 = baseUri.WithQueryString(new { Sort = "desc" }); +``` + +### 6. HttpRequestBuilder (Fluent API) + +```csharp +Result result = await HttpRequestBuilder + .Get("/users/1") + .WithHeader("Accept", "application/json") + .WithHeader("x-api-key", "secret") + .WithQuery("include", "profile") + .AsResultAsync(client); + +Result created = await HttpRequestBuilder + .Post("/users") + .WithJsonContent(new { Name = "Bob" }) + .AsResultAsync(client); +``` + +### 7. Polly Resilience Pipelines + +```csharp +using CSharpEssentials.Http; +using Polly; + +// Retry + Timeout combined +var pipeline = HttpClientResilienceExtensions.CreateResiliencePipeline( + maxRetryAttempts: 3, + timeout: TimeSpan.FromSeconds(30), + retryDelay: TimeSpan.FromSeconds(1)); + +Result result = await pipeline.ExecuteAsResultAsync(async token => + await client.GetFromJsonAsResultAsync(new Uri("/users/1"), cancellationToken: token)); + +// Individual pipelines +ResiliencePipeline retryOnly = HttpClientResilienceExtensions.CreateRetryPipeline(maxRetryAttempts: 3); +ResiliencePipeline timeoutOnly = HttpClientResilienceExtensions.CreateTimeoutPipeline(TimeSpan.FromSeconds(10)); +``` + +### 8. HttpContent Extensions + +```csharp +using HttpResponseMessage response = await client.GetAsync(uri); + +Result textResult = await response.Content.ReadAsStringAsResultAsync(); +Result jsonResult = await response.Content.ReadFromJsonAsResultAsync(); +``` diff --git a/CSharpEssentials.Json/CSharpEssentials.Json.csproj b/CSharpEssentials.Json/CSharpEssentials.Json.csproj index 056d198..222efb8 100644 --- a/CSharpEssentials.Json/CSharpEssentials.Json.csproj +++ b/CSharpEssentials.Json/CSharpEssentials.Json.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 CSharpEssentials.Json diff --git a/CSharpEssentials.Json/ConditionalStringEnumConverter.cs b/CSharpEssentials.Json/ConditionalStringEnumConverter.cs index 350daf9..4c0759b 100644 --- a/CSharpEssentials.Json/ConditionalStringEnumConverter.cs +++ b/CSharpEssentials.Json/ConditionalStringEnumConverter.cs @@ -1,6 +1,6 @@ using System.Reflection; -using System.Text.Json.Serialization; using System.Text.Json; +using System.Text.Json.Serialization; using CSharpEssentials.Enums; namespace CSharpEssentials.Json; @@ -30,4 +30,4 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer return new JsonStringEnumConverter(_namingPolicy, _allowIntegerValues) .CreateConverter(typeToConvert, options); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Json/JsonOptions.cs b/CSharpEssentials.Json/JsonOptions.cs index a1786fd..ec8735d 100644 --- a/CSharpEssentials.Json/JsonOptions.cs +++ b/CSharpEssentials.Json/JsonOptions.cs @@ -19,6 +19,22 @@ public static class EnhancedJsonSerializerOptions DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; + /// + /// Strict JSON serializer options that reject trailing commas, comments, and unmapped members. + /// + public static readonly JsonSerializerOptions StrictOptions = new(JsonSerializerDefaults.Web) + { + ReferenceHandler = ReferenceHandler.IgnoreCycles, + WriteIndented = false, + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + AllowTrailingCommas = false, + ReadCommentHandling = JsonCommentHandling.Disallow, + UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow, + }; + /// /// The default JSON serializer options. /// diff --git a/CSharpEssentials.Json/MultiFormatDateTimeConverter.cs b/CSharpEssentials.Json/MultiFormatDateTimeConverter.cs index ebb076c..d6e66e1 100644 --- a/CSharpEssentials.Json/MultiFormatDateTimeConverter.cs +++ b/CSharpEssentials.Json/MultiFormatDateTimeConverter.cs @@ -4,6 +4,7 @@ namespace CSharpEssentials.Json; +#pragma warning disable IDE0370 /// /// A multi-format date time converter factory. /// @@ -14,14 +15,10 @@ public sealed class MultiFormatDateTimeConverterFactory : JsonConverterFactory private static readonly Type _convertType = typeof(MultiFormatDateTimeConverter<>); private readonly string[] _formats; - public MultiFormatDateTimeConverterFactory(params string[] formats) - { -#if NET8_0_OR_GREATER + + public MultiFormatDateTimeConverterFactory(params string[] formats) => _formats = [.. _defaultFormats, .. formats ?? []]; -#else - _formats = _defaultFormats.Concat(formats ?? Array.Empty()).ToArray(); -#endif - } + public override bool CanConvert(Type typeToConvert) => typeToConvert == _dateTimeType || typeToConvert == _nullableDateTimeType; @@ -42,8 +39,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer )!; } - private static readonly string[] _defaultFormats = new[] - { + private static readonly string[] _defaultFormats = [ // ISO 8601 and Web Formats "yyyy-MM-ddTHH:mm:ss.ffffffzzz", // 2024-03-14T15:30:45.123456+03:00 "yyyy-MM-ddTHH:mm:ss.fffZ", // 2024-03-14T15:30:45.123Z @@ -104,7 +100,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer // Unix and SQL Formats "yyyy-MM-dd HH:mm:ss.fff" // 2024-03-14 15:30:45.123 (SQL) - }; + ]; } /// diff --git a/CSharpEssentials.Json/PolymorphicJsonConverterFactory.cs b/CSharpEssentials.Json/PolymorphicJsonConverterFactory.cs index 88ac00d..6f8da81 100644 --- a/CSharpEssentials.Json/PolymorphicJsonConverterFactory.cs +++ b/CSharpEssentials.Json/PolymorphicJsonConverterFactory.cs @@ -29,7 +29,7 @@ public sealed class PolymorphicJsonConverter : JsonConverter .ToDictionary(t => t.FullName ?? t.Name); }); - private static readonly ConditionalWeakTable InnerOptionsCache = new(); + private static readonly ConditionalWeakTable InnerOptionsCache = []; private static JsonSerializerOptions GetInnerOptions(JsonSerializerOptions options) { diff --git a/CSharpEssentials.Maybe/CSharpEssentials.Maybe.csproj b/CSharpEssentials.Maybe/CSharpEssentials.Maybe.csproj index 1032eb5..faf1914 100644 --- a/CSharpEssentials.Maybe/CSharpEssentials.Maybe.csproj +++ b/CSharpEssentials.Maybe/CSharpEssentials.Maybe.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 CSharpEssentials.Maybe diff --git a/CSharpEssentials.Maybe/Extensions/MaybeExtensionMembers.cs b/CSharpEssentials.Maybe/Extensions/MaybeExtensionMembers.cs new file mode 100644 index 0000000..67476f3 --- /dev/null +++ b/CSharpEssentials.Maybe/Extensions/MaybeExtensionMembers.cs @@ -0,0 +1,11 @@ +#if NET10_0_OR_GREATER +namespace CSharpEssentials.Maybe; + +public static class MaybeExtensionMembers +{ + extension(Maybe maybe) + { + public bool IsNone => !maybe.HasValue; + } +} +#endif diff --git a/CSharpEssentials.Maybe/Interfaces/IMaybe.cs b/CSharpEssentials.Maybe/Interfaces/IMaybe.cs index 7c3c2ec..d74a65c 100644 --- a/CSharpEssentials.Maybe/Interfaces/IMaybe.cs +++ b/CSharpEssentials.Maybe/Interfaces/IMaybe.cs @@ -117,4 +117,4 @@ Maybe SelectMany( Maybe Where(Func predicate); Task> Where(Func> predicate, CancellationToken cancellationToken = default); ValueTask> Where(Func> predicate, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Maybe/Maybe.cs b/CSharpEssentials.Maybe/Maybe.cs index 65b8a8e..9b858b6 100644 --- a/CSharpEssentials.Maybe/Maybe.cs +++ b/CSharpEssentials.Maybe/Maybe.cs @@ -1,9 +1,9 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; +using CSharpEssentials.Core; using CSharpEssentials.Json; using CSharpEssentials.Maybe.Interfaces; -using CSharpEssentials.Core; namespace CSharpEssentials.Maybe; diff --git a/CSharpEssentials.Maybe/Modules/GetValueOrDefault.cs b/CSharpEssentials.Maybe/Modules/GetValueOrDefault.cs index 3ce9435..60333ba 100644 --- a/CSharpEssentials.Maybe/Modules/GetValueOrDefault.cs +++ b/CSharpEssentials.Maybe/Modules/GetValueOrDefault.cs @@ -238,4 +238,4 @@ public static TOut GetValueOrDefault(in this Maybe maybe, Func> MapAsync(this ValueTask maybe = await maybeTask.WithCancellation(cancellationToken); return await maybe.MapAsync(valueTask, cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Maybe/Modules/Match.cs b/CSharpEssentials.Maybe/Modules/Match.cs index bf432da..2069f07 100644 --- a/CSharpEssentials.Maybe/Modules/Match.cs +++ b/CSharpEssentials.Maybe/Modules/Match.cs @@ -556,4 +556,4 @@ await some.Invoke( cancellationToken ); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Maybe/Modules/Or.cs b/CSharpEssentials.Maybe/Modules/Or.cs index 335daae..5ed453c 100644 --- a/CSharpEssentials.Maybe/Modules/Or.cs +++ b/CSharpEssentials.Maybe/Modules/Or.cs @@ -384,4 +384,4 @@ public static async ValueTask> Or(this ValueTask> maybeTask return maybe; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Maybe/Modules/Result.AsMaybe.cs b/CSharpEssentials.Maybe/Modules/Result.AsMaybe.cs index 3e23636..18e9386 100644 --- a/CSharpEssentials.Maybe/Modules/Result.AsMaybe.cs +++ b/CSharpEssentials.Maybe/Modules/Result.AsMaybe.cs @@ -8,7 +8,7 @@ public static partial class MaybeExtensions public static Maybe AsMaybe(this Result result) { if (result.IsSuccess) - return Maybe.From(result.Value); + return result.Value; return Maybe.None; } diff --git a/CSharpEssentials.Maybe/Modules/ToList.cs b/CSharpEssentials.Maybe/Modules/ToList.cs index 533e279..d78b1b9 100644 --- a/CSharpEssentials.Maybe/Modules/ToList.cs +++ b/CSharpEssentials.Maybe/Modules/ToList.cs @@ -10,6 +10,6 @@ public List ToList() => #if NET8_0_OR_GREATER this.GetValueOrDefault>(value => [value], []); #else - this.GetValueOrDefault>(value => new List { value }, new List()); + this.GetValueOrDefault>(value => [value], []); #endif } diff --git a/CSharpEssentials.Maybe/Modules/TryFirst.cs b/CSharpEssentials.Maybe/Modules/TryFirst.cs index a31ed10..798f6e9 100644 --- a/CSharpEssentials.Maybe/Modules/TryFirst.cs +++ b/CSharpEssentials.Maybe/Modules/TryFirst.cs @@ -10,7 +10,7 @@ public static partial class MaybeExtensions /// public static Maybe TryFirst(this IEnumerable source) { - return source.FirstOrDefault() is T result ? Maybe.From(result) : Maybe.None; + return source.FirstOrDefault() is T result ? result : Maybe.None; } /// @@ -22,6 +22,6 @@ public static Maybe TryFirst(this IEnumerable source) /// public static Maybe TryFirst(this IEnumerable source, Func predicate) { - return source.FirstOrDefault(predicate) is T result ? Maybe.From(result) : Maybe.None; + return source.FirstOrDefault(predicate) is T result ? result : Maybe.None; } } diff --git a/CSharpEssentials.Maybe/Modules/Where.cs b/CSharpEssentials.Maybe/Modules/Where.cs index c211b9d..6b39082 100644 --- a/CSharpEssentials.Maybe/Modules/Where.cs +++ b/CSharpEssentials.Maybe/Modules/Where.cs @@ -95,4 +95,4 @@ public static async ValueTask> Where(this ValueTask> maybeT Maybe maybe = await maybeTask.WithCancellation(cancellationToken); return await maybe.Where(predicate, cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.RequestResponseLogging/Attributes/SkipResponseLoggingAttribute.cs b/CSharpEssentials.RequestResponseLogging/Attributes/SkipResponseLoggingAttribute.cs index 326816f..187eaa7 100644 --- a/CSharpEssentials.RequestResponseLogging/Attributes/SkipResponseLoggingAttribute.cs +++ b/CSharpEssentials.RequestResponseLogging/Attributes/SkipResponseLoggingAttribute.cs @@ -1,4 +1,4 @@ namespace CSharpEssentials.RequestResponseLogging; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class SkipResponseLoggingAttribute : Attribute; \ No newline at end of file +public sealed class SkipResponseLoggingAttribute : Attribute; diff --git a/CSharpEssentials.RequestResponseLogging/CSharpEssentials.RequestResponseLogging.csproj b/CSharpEssentials.RequestResponseLogging/CSharpEssentials.RequestResponseLogging.csproj index 4e29c50..03c189f 100644 --- a/CSharpEssentials.RequestResponseLogging/CSharpEssentials.RequestResponseLogging.csproj +++ b/CSharpEssentials.RequestResponseLogging/CSharpEssentials.RequestResponseLogging.csproj @@ -2,7 +2,7 @@ - net9.0;net8.0 + net11.0;net10.0;net9.0;net8.0 CSharpEssentials.RequestResponseLogging diff --git a/CSharpEssentials.RequestResponseLogging/GlobalUsings.cs b/CSharpEssentials.RequestResponseLogging/GlobalUsings.cs index 707ff4f..cb70983 100644 --- a/CSharpEssentials.RequestResponseLogging/GlobalUsings.cs +++ b/CSharpEssentials.RequestResponseLogging/GlobalUsings.cs @@ -1,13 +1,10 @@ -global using System.Text; -global using System.Diagnostics; - -global using Microsoft.IO; -global using Microsoft.AspNetCore.Http; -global using Microsoft.Extensions.Logging; -global using Microsoft.AspNetCore.Http.Extensions; - - +global using System.Diagnostics; +global using System.Text; global using CSharpEssentials.RequestResponseLogging.Infrastructure.Interfaces; -global using CSharpEssentials.RequestResponseLogging.Infrastructure.Middlewares; global using CSharpEssentials.RequestResponseLogging.Infrastructure.MessageCreators; +global using CSharpEssentials.RequestResponseLogging.Infrastructure.Middlewares; global using CSharpEssentials.RequestResponseLogging.LogWriters; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Http.Extensions; +global using Microsoft.Extensions.Logging; +global using Microsoft.IO; diff --git a/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/BaseLogMessageCreator.cs b/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/BaseLogMessageCreator.cs index 1c98765..802e8b5 100644 --- a/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/BaseLogMessageCreator.cs +++ b/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/BaseLogMessageCreator.cs @@ -34,4 +34,4 @@ private static string GetHeaders(RequestResponseContext requestResponseContext, return $"[{string.Join(",", filteredHeaders)}]"; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/LogMessageCreator.cs b/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/LogMessageCreator.cs index 359634e..49fa099 100644 --- a/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/LogMessageCreator.cs +++ b/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/LogMessageCreator.cs @@ -16,4 +16,4 @@ internal sealed class LogMessageCreator(LoggingOptions loggingOptions) : BaseLog return (sb.ToString(), null); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/LogMessageWithContextCreator.cs b/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/LogMessageWithContextCreator.cs index 8d192fa..b795cd6 100644 --- a/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/LogMessageWithContextCreator.cs +++ b/CSharpEssentials.RequestResponseLogging/Infrastructure/MessageCreators/LogMessageWithContextCreator.cs @@ -6,16 +6,15 @@ internal sealed class LogMessageWithContextCreator : BaseLogMessageCreator, ILog { private readonly LoggingOptions _loggingOptions; - public LogMessageWithContextCreator(LoggingOptions loggingOptions) - { - _loggingOptions = loggingOptions; - } + public LogMessageWithContextCreator(LoggingOptions loggingOptions) => _loggingOptions = loggingOptions; public (string logString, List? values) Create(RequestResponseContext requestResponseContext) { #if NET8_0_OR_GREATER +#pragma warning disable IDE0028 List valueList = _loggingOptions.LoggingFields.Count > 0 ? - new List(_loggingOptions.LoggingFields.Count) : []; + new List(_loggingOptions.LoggingFields.Count) : new List(); +#pragma warning restore IDE0028 #else List valueList = _loggingOptions.LoggingFields.Count > 0 ? new List(_loggingOptions.LoggingFields.Count) : new List(); diff --git a/CSharpEssentials.RequestResponseLogging/Infrastructure/Middlewares/BaseMiddleware.cs b/CSharpEssentials.RequestResponseLogging/Infrastructure/Middlewares/BaseMiddleware.cs index 371f1f3..f65c773 100644 --- a/CSharpEssentials.RequestResponseLogging/Infrastructure/Middlewares/BaseMiddleware.cs +++ b/CSharpEssentials.RequestResponseLogging/Infrastructure/Middlewares/BaseMiddleware.cs @@ -1,5 +1,5 @@ -using Microsoft.AspNetCore.Http.Features; -using System.Buffers; +using System.Buffers; +using Microsoft.AspNetCore.Http.Features; namespace CSharpEssentials.RequestResponseLogging.Infrastructure.Middlewares; @@ -21,28 +21,28 @@ protected BaseMiddleware( ILogWriter logWriter, string[] ignoredPaths) { - #if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(logWriter); + ArgumentNullException.ThrowIfNull(logWriter); - #else +#else if (logWriter is null) throw new ArgumentNullException(nameof(logWriter)); - #endif - #if NET6_0_OR_GREATER +#endif +#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(ignoredPaths); + ArgumentNullException.ThrowIfNull(ignoredPaths); - #else +#else if (ignoredPaths is null) throw new ArgumentNullException(nameof(ignoredPaths)); - #endif +#endif _logWriter = logWriter is not NullLogWriter ? logWriter : null; _ignoredPaths = new HashSet( @@ -54,17 +54,17 @@ protected BaseMiddleware( protected bool IsIgnoredPath(HttpContext context) { - #if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(context); - #else +#else if (context is null) throw new ArgumentNullException(nameof(context)); - #endif +#endif string? requestPath = context.Request.Path.Value?.TrimEnd('/'); return !string.IsNullOrEmpty(requestPath) && _ignoredPaths.Any(ignorePath => requestPath.StartsWith(ignorePath, StringComparison.OrdinalIgnoreCase)); @@ -72,28 +72,28 @@ protected bool IsIgnoredPath(HttpContext context) protected async Task InvokeMiddleware(RequestDelegate next, HttpContext httpContext) { - #if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(next); + ArgumentNullException.ThrowIfNull(next); - #else +#else if (next is null) throw new ArgumentNullException(nameof(next)); - #endif - #if NET6_0_OR_GREATER +#endif +#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(httpContext); + ArgumentNullException.ThrowIfNull(httpContext); - #else +#else if (httpContext is null) throw new ArgumentNullException(nameof(httpContext)); - #endif +#endif (bool isSkipRequestLogging, bool isSkipResponseLogging) = GetLoggingConfiguration(httpContext); string requestText; diff --git a/CSharpEssentials.RequestResponseLogging/Infrastructure/Middlewares/DefaultRequestResponseMiddleware.cs b/CSharpEssentials.RequestResponseLogging/Infrastructure/Middlewares/DefaultRequestResponseMiddleware.cs index 3e7f1d0..392ebb3 100644 --- a/CSharpEssentials.RequestResponseLogging/Infrastructure/Middlewares/DefaultRequestResponseMiddleware.cs +++ b/CSharpEssentials.RequestResponseLogging/Infrastructure/Middlewares/DefaultRequestResponseMiddleware.cs @@ -4,10 +4,7 @@ internal sealed class DefaultRequestResponseMiddleware : BaseMiddleware { private readonly RequestDelegate _next; - public DefaultRequestResponseMiddleware(RequestDelegate next, ILogWriter logWriter, string[] ignoredPaths) : base(logWriter, ignoredPaths) - { - _next = next; - } + public DefaultRequestResponseMiddleware(RequestDelegate next, ILogWriter logWriter, string[] ignoredPaths) : base(logWriter, ignoredPaths) => _next = next; public Task InvokeAsync(HttpContext httpContext) => IsIgnoredPath(httpContext) ? _next(httpContext) : InvokeMiddleware(_next, httpContext); } diff --git a/CSharpEssentials.RequestResponseLogging/LogWriters/LoggerFactoryLogWriter.cs b/CSharpEssentials.RequestResponseLogging/LogWriters/LoggerFactoryLogWriter.cs index 8ef294b..c74f975 100644 --- a/CSharpEssentials.RequestResponseLogging/LogWriters/LoggerFactoryLogWriter.cs +++ b/CSharpEssentials.RequestResponseLogging/LogWriters/LoggerFactoryLogWriter.cs @@ -35,15 +35,17 @@ public Task Write(RequestResponseContext requestResponseContext) string?[]? parameters = null; if (values is not null) - parameters = values.ToArray(); + parameters = [.. values]; #pragma warning disable CA2254 #if NET8_0_OR_GREATER - _logger.Log(options.LoggingLevel, logString, parameters ?? Array.Empty()); + if (_logger.IsEnabled(options.LoggingLevel)) + _logger.Log(options.LoggingLevel, logString, parameters ?? Array.Empty()); #else - _logger.Log(_loggingLevel, logString, parameters ?? Array.Empty()); + if (_logger.IsEnabled(_loggingLevel)) + _logger.Log(_loggingLevel, logString, parameters ?? Array.Empty()); #endif #pragma warning restore CA2254 return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.RequestResponseLogging/Models/RequestResponseContext.cs b/CSharpEssentials.RequestResponseLogging/Models/RequestResponseContext.cs index 19c1547..1d7c9ab 100644 --- a/CSharpEssentials.RequestResponseLogging/Models/RequestResponseContext.cs +++ b/CSharpEssentials.RequestResponseLogging/Models/RequestResponseContext.cs @@ -27,8 +27,10 @@ public class RequestResponseContext public int? ResponseLength => ResponseBody?.Length; - private string? url; - public string Url => url ??= BuildUrl().ToString(); +#pragma warning disable IDE0032 // Auto-property not possible due to BuildUrl() call + private string? _url; + public string Url => _url ??= BuildUrl().ToString(); +#pragma warning restore IDE0032 internal Uri BuildUrl() { diff --git a/CSharpEssentials.Results/CSharpEssentials.Results.csproj b/CSharpEssentials.Results/CSharpEssentials.Results.csproj index 8bcb2ae..a8d1f05 100644 --- a/CSharpEssentials.Results/CSharpEssentials.Results.csproj +++ b/CSharpEssentials.Results/CSharpEssentials.Results.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 CSharpEssentials.Results diff --git a/CSharpEssentials.Results/Comparers/ResultComparer.cs b/CSharpEssentials.Results/Comparers/ResultComparer.cs index 7dd09a8..aac1d7f 100644 --- a/CSharpEssentials.Results/Comparers/ResultComparer.cs +++ b/CSharpEssentials.Results/Comparers/ResultComparer.cs @@ -50,4 +50,4 @@ public int GetHashCode([DisallowNull] IResult obj) return obj.Value!.GetHashCode(); return ResultLogic.CreateErrorCodeHash(obj.ErrorsOrEmptyArray); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Extensions/ResultExtensionMembers.cs b/CSharpEssentials.Results/Extensions/ResultExtensionMembers.cs new file mode 100644 index 0000000..b223920 --- /dev/null +++ b/CSharpEssentials.Results/Extensions/ResultExtensionMembers.cs @@ -0,0 +1,17 @@ +#if NET10_0_OR_GREATER +namespace CSharpEssentials.ResultPattern; + +public static class ResultExtensionMembers +{ + extension(Result result) + { + public T? ValueOrDefault => result.IsSuccess ? result.Value : default; + } + + extension(Result left) + { + public static Result operator |(Result l, Result r) + => l.IsSuccess ? l : r; + } +} +#endif diff --git a/CSharpEssentials.Results/Extensions/ResultExtensions.cs b/CSharpEssentials.Results/Extensions/ResultExtensions.cs index 63ce9aa..e60e8af 100644 --- a/CSharpEssentials.Results/Extensions/ResultExtensions.cs +++ b/CSharpEssentials.Results/Extensions/ResultExtensions.cs @@ -9,13 +9,13 @@ public static partial class ResultExtensions /// /// /// - public static Result ToResult(this Error error) => Result.Failure(error); + public static Result ToResult(this Error error) => error; /// /// Converts a collection of errors to a result. /// /// /// - public static Result ToResult(this IEnumerable errors) => Result.Failure(errors); + public static Result ToResult(this IEnumerable errors) => errors.ToArray(); /// /// Converts a value to a result. @@ -23,7 +23,7 @@ public static partial class ResultExtensions /// /// /// - public static Result ToResult(this TValue value) => Result.Success(value); + public static Result ToResult(this TValue value) => value; /// /// Converts an error to a result. @@ -31,7 +31,7 @@ public static partial class ResultExtensions /// /// /// - public static Result ToResult(this Error error) => Result.Failure(error); + public static Result ToResult(this Error error) => error; /// /// Converts a collection of errors to a result. @@ -39,5 +39,5 @@ public static partial class ResultExtensions /// /// /// - public static Result ToResult(this IEnumerable errors) => Result.Failure(errors); + public static Result ToResult(this IEnumerable errors) => errors.ToArray(); } diff --git a/CSharpEssentials.Results/Extensions/StringResultExtensions.cs b/CSharpEssentials.Results/Extensions/StringResultExtensions.cs index 3d152b9..e5bdb5b 100644 --- a/CSharpEssentials.Results/Extensions/StringResultExtensions.cs +++ b/CSharpEssentials.Results/Extensions/StringResultExtensions.cs @@ -1,4 +1,4 @@ -using System; + using CSharpEssentials.Core; using CSharpEssentials.Errors; diff --git a/CSharpEssentials.Results/Interfaces/IResult.cs b/CSharpEssentials.Results/Interfaces/IResult.cs index 1cecfda..31c2b2f 100644 --- a/CSharpEssentials.Results/Interfaces/IResult.cs +++ b/CSharpEssentials.Results/Interfaces/IResult.cs @@ -13,7 +13,7 @@ public interface IResult : IResultBase /// /// A static comparer for comparing two instances. /// - new static readonly IEqualityComparer Comparer = new ResultComparer(); + static new readonly IEqualityComparer Comparer = new ResultComparer(); /// /// Returns a new result if the current result is a failure, using the provided function to handle errors. @@ -238,4 +238,4 @@ public interface IResult : IResultBase /// /// ValueTask Bind(Func> valueTask); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Interfaces/IResultT.cs b/CSharpEssentials.Results/Interfaces/IResultT.cs index 7f59892..8b24465 100644 --- a/CSharpEssentials.Results/Interfaces/IResultT.cs +++ b/CSharpEssentials.Results/Interfaces/IResultT.cs @@ -11,7 +11,7 @@ public interface IResult : IResultBase /// /// A static readonly instance of for comparing instances of . /// - new static readonly IEqualityComparer> Comparer = new ResultComparer(); + static new readonly IEqualityComparer> Comparer = new ResultComparer(); /// /// Gets the value of the result. /// @@ -329,4 +329,4 @@ public interface IResult : IResultBase /// /// ValueTask Bind(Func> valueTask); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Modules/Result.Bind.cs b/CSharpEssentials.Results/Modules/Result.Bind.cs index 6d4f535..84a2092 100644 --- a/CSharpEssentials.Results/Modules/Result.Bind.cs +++ b/CSharpEssentials.Results/Modules/Result.Bind.cs @@ -38,7 +38,7 @@ public Result Bind(Func func) public Task> Bind(Func>> func) { if (IsFailure) - return Result.Failure(Errors).AsTask(); + return ((Result)Errors).AsTask(); return func(); } @@ -50,7 +50,7 @@ public Task> Bind(Func>> func) public Task Bind(Func> func) { if (IsFailure) - return Failure(Errors).AsTask(); + return ((Result)Errors).AsTask(); return func(); } @@ -162,4 +162,4 @@ public static async ValueTask BindAsync(this ValueTask task, Fun Result result = await task.WithCancellation(cancellationToken); return await result.Bind(func).WithCancellation(cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Modules/Result.Else.cs b/CSharpEssentials.Results/Modules/Result.Else.cs index 45a1f01..3d3979f 100644 --- a/CSharpEssentials.Results/Modules/Result.Else.cs +++ b/CSharpEssentials.Results/Modules/Result.Else.cs @@ -1,5 +1,5 @@ -using CSharpEssentials.Errors; using CSharpEssentials.Core; +using CSharpEssentials.Errors; namespace CSharpEssentials.ResultPattern; @@ -238,4 +238,4 @@ public static async ValueTask ElseAsync(this ValueTask task, Tas Result result = await task.WithCancellation(cancellationToken); return await result.ElseAsync(onFailure, cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Modules/Result.FailureIf.cs b/CSharpEssentials.Results/Modules/Result.FailureIf.cs index 862af81..19d7284 100644 --- a/CSharpEssentials.Results/Modules/Result.FailureIf.cs +++ b/CSharpEssentials.Results/Modules/Result.FailureIf.cs @@ -4,9 +4,9 @@ namespace CSharpEssentials.ResultPattern; public readonly partial record struct Result { - public static Result FailureIf(bool condition, Error error) => condition ? Failure(error) : Success(); + public static Result FailureIf(bool condition, Error error) => condition ? error : Success(); - public static Result FailureIf(Func predicate, Error error) => predicate() ? Failure(error) : Success(); + public static Result FailureIf(Func predicate, Error error) => predicate() ? error : Success(); - public static Result FailureIf(bool condition, Error error) => condition ? Failure(error) : Result.Success(default!); + public static Result FailureIf(bool condition, Error error) => condition ? error : default(TValue)!; } diff --git a/CSharpEssentials.Results/Modules/Result.IsSuccess.cs b/CSharpEssentials.Results/Modules/Result.IsSuccess.cs new file mode 100644 index 0000000..8483ff7 --- /dev/null +++ b/CSharpEssentials.Results/Modules/Result.IsSuccess.cs @@ -0,0 +1,23 @@ +using CSharpEssentials.Errors; + +namespace CSharpEssentials.ResultPattern; + +public readonly partial record struct Result +{ + /// + /// Attempts to extract the errors from the result. + /// + /// The errors when is true; otherwise, null. + /// true if the result is successful; otherwise, false. + public bool TryGet(out Error[]? errors) + { + if (IsSuccess) + { + errors = null; + return true; + } + + errors = _errors; + return false; + } +} diff --git a/CSharpEssentials.Results/Modules/Result.MapError.cs b/CSharpEssentials.Results/Modules/Result.MapError.cs index 4cc0590..df4981e 100644 --- a/CSharpEssentials.Results/Modules/Result.MapError.cs +++ b/CSharpEssentials.Results/Modules/Result.MapError.cs @@ -8,13 +8,13 @@ public Result MapError(Func errorMapper) { if (IsSuccess) return this; - return Result.Failure(errorMapper(Errors)); + return errorMapper(Errors); } public Result MapError(Func errorMapper) { if (IsSuccess) return this; - return errorMapper(FirstError).ToResult(); + return errorMapper(FirstError); } } diff --git a/CSharpEssentials.Results/Modules/Result.Match.cs b/CSharpEssentials.Results/Modules/Result.Match.cs index c4bb648..4357d83 100644 --- a/CSharpEssentials.Results/Modules/Result.Match.cs +++ b/CSharpEssentials.Results/Modules/Result.Match.cs @@ -1,5 +1,5 @@ -using CSharpEssentials.Errors; using CSharpEssentials.Core; +using CSharpEssentials.Errors; namespace CSharpEssentials.ResultPattern; diff --git a/CSharpEssentials.Results/Modules/Result.Switch.cs b/CSharpEssentials.Results/Modules/Result.Switch.cs index 6be4168..a88ba71 100644 --- a/CSharpEssentials.Results/Modules/Result.Switch.cs +++ b/CSharpEssentials.Results/Modules/Result.Switch.cs @@ -1,5 +1,5 @@ -using CSharpEssentials.Errors; using CSharpEssentials.Core; +using CSharpEssentials.Errors; namespace CSharpEssentials.ResultPattern; diff --git a/CSharpEssentials.Results/Modules/Result.TryCatch.cs b/CSharpEssentials.Results/Modules/Result.TryCatch.cs index 9ea4fad..3c02d80 100644 --- a/CSharpEssentials.Results/Modules/Result.TryCatch.cs +++ b/CSharpEssentials.Results/Modules/Result.TryCatch.cs @@ -1,5 +1,5 @@ -using CSharpEssentials.Errors; using CSharpEssentials.Core; +using CSharpEssentials.Errors; namespace CSharpEssentials.ResultPattern; diff --git a/CSharpEssentials.Results/Modules/ResultT.Bind.cs b/CSharpEssentials.Results/Modules/ResultT.Bind.cs index 9e9406a..49e4d69 100644 --- a/CSharpEssentials.Results/Modules/ResultT.Bind.cs +++ b/CSharpEssentials.Results/Modules/ResultT.Bind.cs @@ -38,7 +38,7 @@ public Result Bind(Func func) public Task> Bind(Func>> func) { if (IsFailure) - return Result.Failure(Errors).AsTask(); + return ((Result)Errors).AsTask(); return func(Value); } @@ -50,7 +50,7 @@ public Task> Bind(Func>> func) public Task Bind(Func> func) { if (IsFailure) - return Result.Failure(Errors).AsTask(); + return ((Result)Errors).AsTask(); return func(Value); } @@ -63,7 +63,7 @@ public Task Bind(Func> func) public ValueTask> Bind(Func>> valueTask) { if (IsFailure) - return Result.Failure(Errors).AsValueTask(); + return ((Result)Errors).AsValueTask(); return valueTask(Value); } @@ -76,7 +76,7 @@ public ValueTask> Bind(Func>> public ValueTask Bind(Func> valueTask) { if (IsFailure) - return Result.Failure(Errors).AsValueTask(); + return ((Result)Errors).AsValueTask(); return valueTask(Value); } } @@ -213,4 +213,4 @@ public static async ValueTask BindAsync(this ValueTask result = await task.WithCancellation(cancellationToken); return await result.Bind(func).WithCancellation(cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Modules/ResultT.Combine.cs b/CSharpEssentials.Results/Modules/ResultT.Combine.cs new file mode 100644 index 0000000..5577bb9 --- /dev/null +++ b/CSharpEssentials.Results/Modules/ResultT.Combine.cs @@ -0,0 +1,188 @@ +using CSharpEssentials.Errors; + +namespace CSharpEssentials.ResultPattern; + +public readonly partial record struct Result +{ + /// + /// Combines two results into a single result containing a tuple of their values. + /// If either result is a failure, returns a failure with all errors. + /// + public static Result<(TValue, TOther)> Combine(Result first, Result second) + { + if (first.IsSuccess && second.IsSuccess) + { + return (first.Value, second.Value); + } + + List errors = []; + if (first.IsFailure) + errors.AddRange(first.ErrorsOrEmptyArray); + if (second.IsFailure) + errors.AddRange(second.ErrorsOrEmptyArray); + + return errors; + } + + /// + /// Combines three results into a single result containing a tuple of their values. + /// If any result is a failure, returns a failure with all errors. + /// + public static Result<(TValue, T2, T3)> Combine(Result first, Result second, Result third) + { + if (first.IsSuccess && second.IsSuccess && third.IsSuccess) + { + return (first.Value, second.Value, third.Value); + } + + List errors = []; + if (first.IsFailure) + errors.AddRange(first.ErrorsOrEmptyArray); + if (second.IsFailure) + errors.AddRange(second.ErrorsOrEmptyArray); + if (third.IsFailure) + errors.AddRange(third.ErrorsOrEmptyArray); + + return errors; + } + + /// + /// Combines four results into a single result containing a tuple of their values. + /// If any result is a failure, returns a failure with all errors. + /// + public static Result<(TValue, T2, T3, T4)> Combine(Result first, Result second, Result third, Result fourth) + { + if (first.IsSuccess && second.IsSuccess && third.IsSuccess && fourth.IsSuccess) + { + return (first.Value, second.Value, third.Value, fourth.Value); + } + + List errors = []; + if (first.IsFailure) + errors.AddRange(first.ErrorsOrEmptyArray); + if (second.IsFailure) + errors.AddRange(second.ErrorsOrEmptyArray); + if (third.IsFailure) + errors.AddRange(third.ErrorsOrEmptyArray); + if (fourth.IsFailure) + errors.AddRange(fourth.ErrorsOrEmptyArray); + + return errors; + } + + /// + /// Combines five results into a single result containing a tuple of their values. + /// If any result is a failure, returns a failure with all errors. + /// + public static Result<(TValue, T2, T3, T4, T5)> Combine(Result first, Result second, Result third, Result fourth, Result fifth) + { + if (first.IsSuccess && second.IsSuccess && third.IsSuccess && fourth.IsSuccess && fifth.IsSuccess) + { + return (first.Value, second.Value, third.Value, fourth.Value, fifth.Value); + } + + List errors = []; + if (first.IsFailure) + errors.AddRange(first.ErrorsOrEmptyArray); + if (second.IsFailure) + errors.AddRange(second.ErrorsOrEmptyArray); + if (third.IsFailure) + errors.AddRange(third.ErrorsOrEmptyArray); + if (fourth.IsFailure) + errors.AddRange(fourth.ErrorsOrEmptyArray); + if (fifth.IsFailure) + errors.AddRange(fifth.ErrorsOrEmptyArray); + + return errors; + } + + /// + /// Combines six results into a single result containing a tuple of their values. + /// If any result is a failure, returns a failure with all errors. + /// + public static Result<(TValue, T2, T3, T4, T5, T6)> Combine(Result first, Result second, Result third, Result fourth, Result fifth, Result sixth) + { + if (first.IsSuccess && second.IsSuccess && third.IsSuccess && fourth.IsSuccess && fifth.IsSuccess && sixth.IsSuccess) + { + return (first.Value, second.Value, third.Value, fourth.Value, fifth.Value, sixth.Value); + } + + List errors = []; + if (first.IsFailure) + errors.AddRange(first.ErrorsOrEmptyArray); + if (second.IsFailure) + errors.AddRange(second.ErrorsOrEmptyArray); + if (third.IsFailure) + errors.AddRange(third.ErrorsOrEmptyArray); + if (fourth.IsFailure) + errors.AddRange(fourth.ErrorsOrEmptyArray); + if (fifth.IsFailure) + errors.AddRange(fifth.ErrorsOrEmptyArray); + if (sixth.IsFailure) + errors.AddRange(sixth.ErrorsOrEmptyArray); + + return errors; + } + + /// + /// Combines seven results into a single result containing a tuple of their values. + /// If any result is a failure, returns a failure with all errors. + /// + public static Result<(TValue, T2, T3, T4, T5, T6, T7)> Combine(Result first, Result second, Result third, Result fourth, Result fifth, Result sixth, Result seventh) + { + if (first.IsSuccess && second.IsSuccess && third.IsSuccess && fourth.IsSuccess && fifth.IsSuccess && sixth.IsSuccess && seventh.IsSuccess) + { + return (first.Value, second.Value, third.Value, fourth.Value, fifth.Value, sixth.Value, seventh.Value); + } + + List errors = []; + if (first.IsFailure) + errors.AddRange(first.ErrorsOrEmptyArray); + if (second.IsFailure) + errors.AddRange(second.ErrorsOrEmptyArray); + if (third.IsFailure) + errors.AddRange(third.ErrorsOrEmptyArray); + if (fourth.IsFailure) + errors.AddRange(fourth.ErrorsOrEmptyArray); + if (fifth.IsFailure) + errors.AddRange(fifth.ErrorsOrEmptyArray); + if (sixth.IsFailure) + errors.AddRange(sixth.ErrorsOrEmptyArray); + if (seventh.IsFailure) + errors.AddRange(seventh.ErrorsOrEmptyArray); + + return errors; + } + + /// + /// Combines eight results into a single result containing a tuple of their values. + /// If any result is a failure, returns a failure with all errors. + /// + public static Result<(TValue, T2, T3, T4, T5, T6, T7, T8)> Combine(Result first, Result second, Result third, Result fourth, Result fifth, Result sixth, Result seventh, Result eighth) + { + if (first.IsSuccess && second.IsSuccess && third.IsSuccess && fourth.IsSuccess && fifth.IsSuccess && sixth.IsSuccess && seventh.IsSuccess && eighth.IsSuccess) + { + return (first.Value, second.Value, third.Value, fourth.Value, fifth.Value, sixth.Value, seventh.Value, eighth.Value); + } + + List errors = []; + if (first.IsFailure) + errors.AddRange(first.ErrorsOrEmptyArray); + if (second.IsFailure) + errors.AddRange(second.ErrorsOrEmptyArray); + if (third.IsFailure) + errors.AddRange(third.ErrorsOrEmptyArray); + if (fourth.IsFailure) + errors.AddRange(fourth.ErrorsOrEmptyArray); + if (fifth.IsFailure) + errors.AddRange(fifth.ErrorsOrEmptyArray); + if (sixth.IsFailure) + errors.AddRange(sixth.ErrorsOrEmptyArray); + if (seventh.IsFailure) + errors.AddRange(seventh.ErrorsOrEmptyArray); + if (eighth.IsFailure) + errors.AddRange(eighth.ErrorsOrEmptyArray); + + return errors; + } +} diff --git a/CSharpEssentials.Results/Modules/ResultT.Else.cs b/CSharpEssentials.Results/Modules/ResultT.Else.cs index 6e1cb7b..a20ad26 100644 --- a/CSharpEssentials.Results/Modules/ResultT.Else.cs +++ b/CSharpEssentials.Results/Modules/ResultT.Else.cs @@ -1,5 +1,5 @@ -using CSharpEssentials.Errors; using CSharpEssentials.Core; +using CSharpEssentials.Errors; namespace CSharpEssentials.ResultPattern; @@ -366,4 +366,4 @@ public static async ValueTask> ElseAsync(this ValueTask result = await task.WithCancellation(cancellationToken); return await result.ElseAsync(onFailure, cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Modules/ResultT.FailIf.cs b/CSharpEssentials.Results/Modules/ResultT.FailIf.cs index 174749f..213b691 100644 --- a/CSharpEssentials.Results/Modules/ResultT.FailIf.cs +++ b/CSharpEssentials.Results/Modules/ResultT.FailIf.cs @@ -1,7 +1,7 @@ -using CSharpEssentials.Errors; using CSharpEssentials.Core; +using CSharpEssentials.Errors; namespace CSharpEssentials.ResultPattern; @@ -191,4 +191,4 @@ public static async ValueTask> FailIfAsync( Result result = await task.WithCancellation(cancellationToken); return await result.FailIfAsync(onSuccess, func, cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Modules/ResultT.IsSuccess.cs b/CSharpEssentials.Results/Modules/ResultT.IsSuccess.cs new file mode 100644 index 0000000..36dc0ca --- /dev/null +++ b/CSharpEssentials.Results/Modules/ResultT.IsSuccess.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; +using CSharpEssentials.Errors; + +namespace CSharpEssentials.ResultPattern; + +public readonly partial record struct Result +{ + /// + /// Attempts to extract the success value and errors from the result. + /// + /// The success value when is true; otherwise, default. + /// The errors when is true; otherwise, null. + /// true if the result is successful; otherwise, false. + public bool TryGet([MaybeNullWhen(false)] out TValue value, out Error[]? errors) + { + if (IsSuccess) + { + value = Value; + errors = null; + return true; + } + + value = default; + errors = _errors; + return false; + } +} diff --git a/CSharpEssentials.Results/Modules/ResultT.Match.cs b/CSharpEssentials.Results/Modules/ResultT.Match.cs index 1f79ba6..8b3f2ca 100644 --- a/CSharpEssentials.Results/Modules/ResultT.Match.cs +++ b/CSharpEssentials.Results/Modules/ResultT.Match.cs @@ -1,5 +1,5 @@ -using CSharpEssentials.Errors; using CSharpEssentials.Core; +using CSharpEssentials.Errors; namespace CSharpEssentials.ResultPattern; @@ -231,4 +231,4 @@ public static async ValueTask MatchLastAsync(this ValueTask result = await task.WithCancellation(cancellationToken); return await result.MatchLastAsync(onSuccess, onError, cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Modules/ResultT.Recover.cs b/CSharpEssentials.Results/Modules/ResultT.Recover.cs new file mode 100644 index 0000000..6a7a822 --- /dev/null +++ b/CSharpEssentials.Results/Modules/ResultT.Recover.cs @@ -0,0 +1,144 @@ +using CSharpEssentials.Errors; + +namespace CSharpEssentials.ResultPattern; + +public readonly partial record struct Result +{ + /// + /// Recovers from a specific error type by applying a recovery function. + /// + /// The error type to recover from. + /// The function to apply when an error of the specified type is found. + /// A new result with the recovered value if the error matched; otherwise, the original failure result. + public Result Recover(ErrorType errorType, Func> recovery) + { + if (IsSuccess) + { + return this; + } + + foreach (Error error in ErrorsOrEmptyArray) + { + if (error.Type == errorType) + { + return recovery(error); + } + } + + return this; + } + + /// + /// Recovers from a specific error type by applying a recovery function that produces a value. + /// + /// The error type to recover from. + /// The function to apply when an error of the specified type is found. + /// A new success result with the recovered value if the error matched; otherwise, the original failure result. + public Result Recover(ErrorType errorType, Func recovery) + { + if (IsSuccess) + { + return this; + } + + foreach (Error error in ErrorsOrEmptyArray) + { + if (error.Type == errorType) + { + return recovery(error); + } + } + + return this; + } + + /// + /// Recovers from the first error if it matches the specified type. + /// + /// The error type to recover from. + /// The function to apply when the first error is of the specified type. + /// A new result with the recovered value if the first error matched; otherwise, the original failure result. + public Result RecoverFirst(ErrorType errorType, Func> recovery) + { + if (IsSuccess) + { + return this; + } + + if (FirstError.Type == errorType) + { + return recovery(FirstError); + } + + return this; + } + + /// + /// Recovers from the first error if it matches the specified type by producing a value. + /// + /// The error type to recover from. + /// The function to apply when the first error is of the specified type. + /// A new success result with the recovered value if the first error matched; otherwise, the original failure result. + public Result RecoverFirst(ErrorType errorType, Func recovery) + { + if (IsSuccess) + { + return this; + } + + if (FirstError.Type == errorType) + { + return recovery(FirstError); + } + + return this; + } + + /// + /// Recovers from errors matching the specified predicate. + /// + /// The predicate to match errors against. + /// The function to apply when a matching error is found. + /// A new result with the recovered value if a matching error was found; otherwise, the original failure result. + public Result Recover(Func predicate, Func> recovery) + { + if (IsSuccess) + { + return this; + } + + foreach (Error error in ErrorsOrEmptyArray) + { + if (predicate(error)) + { + return recovery(error); + } + } + + return this; + } + + /// + /// Recovers from errors matching the specified predicate by producing a value. + /// + /// The predicate to match errors against. + /// The function to apply when a matching error is found. + /// A new success result with the recovered value if a matching error was found; otherwise, the original failure result. + public Result Recover(Func predicate, Func recovery) + { + if (IsSuccess) + { + return this; + } + + foreach (Error error in ErrorsOrEmptyArray) + { + if (predicate(error)) + { + return recovery(error); + } + } + + return this; + } +} diff --git a/CSharpEssentials.Results/Modules/ResultT.Switch.cs b/CSharpEssentials.Results/Modules/ResultT.Switch.cs index 6b2718b..e780653 100644 --- a/CSharpEssentials.Results/Modules/ResultT.Switch.cs +++ b/CSharpEssentials.Results/Modules/ResultT.Switch.cs @@ -1,5 +1,5 @@ -using CSharpEssentials.Errors; using CSharpEssentials.Core; +using CSharpEssentials.Errors; namespace CSharpEssentials.ResultPattern; @@ -247,4 +247,4 @@ public static async ValueTask SwitchLastAsync(this ValueTask result = await task.WithCancellation(cancellationToken); await result.SwitchLastAsync(onSuccess, onError, cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Modules/ResultT.Then.cs b/CSharpEssentials.Results/Modules/ResultT.Then.cs index a105bae..bf07e85 100644 --- a/CSharpEssentials.Results/Modules/ResultT.Then.cs +++ b/CSharpEssentials.Results/Modules/ResultT.Then.cs @@ -230,4 +230,4 @@ public static async ValueTask> ThenDoAsync(this ValueTask Result result = await task.WithCancellation(cancellationToken); return await result.ThenDoAsync(action, cancellationToken); } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Results/Modules/ResultT.ThenEnsure.cs b/CSharpEssentials.Results/Modules/ResultT.ThenEnsure.cs index 693ab9e..9e2104c 100644 --- a/CSharpEssentials.Results/Modules/ResultT.ThenEnsure.cs +++ b/CSharpEssentials.Results/Modules/ResultT.ThenEnsure.cs @@ -1,6 +1,3 @@ -using CSharpEssentials.Core; -using CSharpEssentials.Errors; - namespace CSharpEssentials.ResultPattern; public readonly partial record struct Result @@ -24,14 +21,16 @@ public async Task> ThenEnsureAsync(Func> ThenEnsureAsync(Func> validator, CancellationToken cancellationToken = default) { if (IsFailure) return this; - Result result = await validator(Value).WithCancellation(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); + Result result = await validator(Value); return result.IsSuccess ? this : result.Errors.ToResult(); } } @@ -40,25 +39,25 @@ public static partial class ResultExtensions { public static async Task> ThenEnsureAsync(this Task> task, Func>> validator, CancellationToken cancellationToken = default) { - Result result = await task.WithCancellation(cancellationToken); + Result result = await task; return await result.ThenEnsureAsync(validator, cancellationToken); } public static async Task> ThenEnsureAsync(this Task> task, Func> validator, CancellationToken cancellationToken = default) { - Result result = await task.WithCancellation(cancellationToken); + Result result = await task; return await result.ThenEnsureAsync(validator, cancellationToken); } public static async ValueTask> ThenEnsureAsync(this ValueTask> task, Func>> validator, CancellationToken cancellationToken = default) { - Result result = await task.WithCancellation(cancellationToken); + Result result = await task; return await result.ThenEnsureAsync(validator, cancellationToken); } public static async ValueTask> ThenEnsureAsync(this ValueTask> task, Func> validator, CancellationToken cancellationToken = default) { - Result result = await task.WithCancellation(cancellationToken); + Result result = await task; return await result.ThenEnsureAsync(validator, cancellationToken); } } diff --git a/CSharpEssentials.Results/Modules/ResultT.TryCatch.cs b/CSharpEssentials.Results/Modules/ResultT.TryCatch.cs index 27d08d1..4fbc65f 100644 --- a/CSharpEssentials.Results/Modules/ResultT.TryCatch.cs +++ b/CSharpEssentials.Results/Modules/ResultT.TryCatch.cs @@ -1,5 +1,5 @@ -using CSharpEssentials.Errors; using CSharpEssentials.Core; +using CSharpEssentials.Errors; namespace CSharpEssentials.ResultPattern; diff --git a/CSharpEssentials.Results/Modules/ResultT.Unwrap.cs b/CSharpEssentials.Results/Modules/ResultT.Unwrap.cs new file mode 100644 index 0000000..89e80cc --- /dev/null +++ b/CSharpEssentials.Results/Modules/ResultT.Unwrap.cs @@ -0,0 +1,29 @@ +namespace CSharpEssentials.ResultPattern; + +public readonly partial record struct Result +{ + /// + /// Returns the success value if the result is successful; otherwise, throws a . + /// + /// The success value. + /// Thrown when the result is in a failure state. + public TValue Unwrap() + { + if (IsSuccess) + { + return Value; + } + + throw new ResultUnwrapException(ErrorsOrEmptyArray); + } + + /// + /// Returns the success value if the result is successful; otherwise, returns the specified default value. + /// + /// The value to return if the result is in a failure state. + /// The success value, or if the result is a failure. + public TValue UnwrapOrDefault(TValue defaultValue) + { + return IsSuccess ? Value : defaultValue; + } +} diff --git a/CSharpEssentials.Results/Polyfills/MemberNotNullWhenAttribute.cs b/CSharpEssentials.Results/Polyfills/MemberNotNullWhenAttribute.cs index fbb6bbb..0c642d0 100644 --- a/CSharpEssentials.Results/Polyfills/MemberNotNullWhenAttribute.cs +++ b/CSharpEssentials.Results/Polyfills/MemberNotNullWhenAttribute.cs @@ -1,6 +1,4 @@ #if !NET5_0_OR_GREATER -using System; - namespace System.Diagnostics.CodeAnalysis; [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] diff --git a/CSharpEssentials.Results/Result.cs b/CSharpEssentials.Results/Result.cs index 847a61d..17eb27e 100644 --- a/CSharpEssentials.Results/Result.cs +++ b/CSharpEssentials.Results/Result.cs @@ -10,24 +10,18 @@ namespace CSharpEssentials.ResultPattern; private readonly Error[]? _errors = null; [JsonConstructor] -#pragma warning disable IDE0051 + [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used by System.Text.Json")] private Result(bool isFailure, Error[]? errorsOrEmptyArray = null) => _errors = isFailure ? errorsOrEmptyArray : null; -#pragma warning restore IDE0051 private Result(IEnumerable errors) { - #if NET6_0_OR_GREATER - - ArgumentNullException.ThrowIfNull(errors); - - #else - - if (errors is null) - - throw new ArgumentNullException(nameof(errors)); - - #endif - Error[] errorArray = errors.ToArray(); +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(errors); +#else + if (errors is null) + throw new ArgumentNullException(nameof(errors)); +#endif + Error[] errorArray = [.. errors]; if (errorArray.Length == 0) throw ResultLogic.CreateEmptyErrorArrayException(); @@ -42,21 +36,21 @@ private Result(IEnumerable errors) [MemberNotNullWhen(false, nameof(_errors))] public readonly bool IsSuccess => _errors is null; [JsonPropertyName("errors")] -#if NET8_0_OR_GREATER - public readonly Error[] ErrorsOrEmptyArray => IsFailure ? _errors! : []; -#else - public readonly Error[] ErrorsOrEmptyArray => IsFailure ? _errors! : Array.Empty(); -#endif + public readonly Error[] ErrorsOrEmptyArray => IsFailure ? _errors : []; [JsonIgnore] -#if NET8_0_OR_GREATER - public readonly Error[] Errors => IsFailure ? _errors! : [Error.NoErrors]; -#else - public readonly Error[] Errors => IsFailure ? _errors! : new[] { Error.NoErrors }; -#endif + public readonly Error[] Errors + { + get + { + if (IsFailure) + return _errors; + return Error.NoErrors; + } + } [JsonIgnore] - public readonly Error FirstError => IsFailure ? _errors![0] : Error.NoFirstError; + public readonly Error FirstError => IsFailure ? _errors[0] : Error.NoFirstError; [JsonIgnore] - public readonly Error LastError => IsFailure ? _errors![_errors!.Length - 1] : Error.NoLastError; + public readonly Error LastError => IsFailure ? _errors[_errors.Length - 1] : Error.NoLastError; public override string ToString() @@ -84,4 +78,5 @@ public override int GetHashCode() return IsSuccess.GetHashCode(); return ResultLogic.CreateErrorCodeHash(ErrorsOrEmptyArray); } + } diff --git a/CSharpEssentials.Results/ResultFactory.cs b/CSharpEssentials.Results/ResultFactory.cs index 3351c06..8951af5 100644 --- a/CSharpEssentials.Results/ResultFactory.cs +++ b/CSharpEssentials.Results/ResultFactory.cs @@ -86,7 +86,7 @@ public static Result Or(params IEnumerable results) /// /// /// - public static Result Success(TValue value) => Result.Success(value); + public static Result Success(TValue value) => value; /// /// Creates a new failure result. /// @@ -120,7 +120,7 @@ public static Result Or(params IEnumerable results) #if NET8_0_OR_GREATER public static implicit operator Result(Error error) => new([error]); #else - public static implicit operator Result(Error error) => new(new[] { error }); + public static implicit operator Result(Error error) => new([error]); #endif public static implicit operator Result(Error[] errors) => new(errors); public static implicit operator Result(List errors) => new(errors); diff --git a/CSharpEssentials.Results/ResultT.cs b/CSharpEssentials.Results/ResultT.cs index 172d21b..18a4542 100644 --- a/CSharpEssentials.Results/ResultT.cs +++ b/CSharpEssentials.Results/ResultT.cs @@ -12,43 +12,32 @@ namespace CSharpEssentials.ResultPattern; private readonly Error[]? _errors = null; private readonly TValue? _value = default; [JsonConstructor] -#pragma warning disable IDE0051 + [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used by System.Text.Json")] private Result(bool isSuccess, TValue? value = default, Error[]? errorsOrEmptyArray = null) { _value = isSuccess ? value : default; _errors = !isSuccess ? errorsOrEmptyArray : null; } -#pragma warning restore IDE0051 private Result(TValue value) { - #if NET6_0_OR_GREATER - - ArgumentNullException.ThrowIfNull(value); - - #else - - if (value is null) - - throw new ArgumentNullException(nameof(value)); - - #endif +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(value); +#else + if (value is null) + throw new ArgumentNullException(nameof(value)); +#endif _value = value; } private Result(IEnumerable errors) { - #if NET6_0_OR_GREATER - - ArgumentNullException.ThrowIfNull(errors); - - #else - - if (errors is null) - - throw new ArgumentNullException(nameof(errors)); - - #endif - Error[] errorArray = errors.ToArray(); +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(errors); +#else + if (errors is null) + throw new ArgumentNullException(nameof(errors)); +#endif + Error[] errorArray = [.. errors]; if (errorArray.Length == 0) throw ResultLogic.CreateEmptyErrorArrayException(); @@ -69,17 +58,17 @@ private Result(IEnumerable errors) public readonly bool IsSuccess => _errors is null; public readonly TValue Value => IsSuccess ? _value : default!; [JsonPropertyName("errors")] -#if NET8_0_OR_GREATER - public readonly Error[] ErrorsOrEmptyArray => IsFailure ? _errors! : []; -#else - public readonly Error[] ErrorsOrEmptyArray => IsFailure ? _errors! : Array.Empty(); -#endif + public readonly Error[] ErrorsOrEmptyArray => IsFailure ? _errors : []; [JsonIgnore] -#if NET8_0_OR_GREATER - public readonly Error[] Errors => IsFailure ? _errors! : [Error.NoErrors]; -#else - public readonly Error[] Errors => IsFailure ? _errors! : new[] { Error.NoErrors }; -#endif + public readonly Error[] Errors + { + get + { + if (IsFailure) + return _errors; + return Error.NoErrors; + } + } [JsonIgnore] public readonly Error FirstError => IsFailure ? _errors[0] : Error.NoFirstError; [JsonIgnore] @@ -91,7 +80,7 @@ private Result(IEnumerable errors) /// public Result ToResult() => IsSuccess ? Result.Success() : - Result.Failure(Errors); + Errors; diff --git a/CSharpEssentials.Results/ResultTFactory.cs b/CSharpEssentials.Results/ResultTFactory.cs index e8ca835..c99dd29 100644 --- a/CSharpEssentials.Results/ResultTFactory.cs +++ b/CSharpEssentials.Results/ResultTFactory.cs @@ -94,7 +94,7 @@ public static Result Or(params IEnumerable> results) #if NET8_0_OR_GREATER public static implicit operator Result(Error error) => new([error]); #else - public static implicit operator Result(Error error) => new(new[] { error }); + public static implicit operator Result(Error error) => new([error]); #endif public static implicit operator Result(Error[] errors) => new(errors); public static implicit operator Result(List errors) => new(errors); diff --git a/CSharpEssentials.Results/ResultUnwrapException.cs b/CSharpEssentials.Results/ResultUnwrapException.cs new file mode 100644 index 0000000..4f6737a --- /dev/null +++ b/CSharpEssentials.Results/ResultUnwrapException.cs @@ -0,0 +1,29 @@ +using CSharpEssentials.Errors; + +namespace CSharpEssentials.ResultPattern; + +/// +/// Represents an exception thrown when attempting to unwrap a or that is in a failure state. +/// +public sealed class ResultUnwrapException : InvalidOperationException +{ + /// + /// Gets the errors associated with the failed result, if any. + /// + public Error[] Errors { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The errors that caused the unwrap to fail. + public ResultUnwrapException(Error[] errors) + : base($"Cannot unwrap a failed result. Errors: {string.Join(", ", errors.Select(e => e.Code))}") => Errors = errors; + + /// + /// Initializes a new instance of the class with a specified message. + /// + /// The message that describes the error. + /// The errors that caused the unwrap to fail. + public ResultUnwrapException(string message, Error[] errors) + : base(message) => Errors = errors; +} diff --git a/CSharpEssentials.Rules/Adapters/LinearAsyncRuleAdapter.cs b/CSharpEssentials.Rules/Adapters/LinearAsyncRuleAdapter.cs index b512522..3b136d5 100644 --- a/CSharpEssentials.Rules/Adapters/LinearAsyncRuleAdapter.cs +++ b/CSharpEssentials.Rules/Adapters/LinearAsyncRuleAdapter.cs @@ -30,4 +30,4 @@ internal static LinearAsyncRuleAdapter From( IAsyncRule rule, IRuleBase next ) => new(rule, next); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Adapters/LinearRuleWithCancellationTokenAdapter.cs b/CSharpEssentials.Rules/Adapters/LinearRuleWithCancellationTokenAdapter.cs index 84d4e3e..da31ef3 100644 --- a/CSharpEssentials.Rules/Adapters/LinearRuleWithCancellationTokenAdapter.cs +++ b/CSharpEssentials.Rules/Adapters/LinearRuleWithCancellationTokenAdapter.cs @@ -30,4 +30,4 @@ internal static LinearRuleWithCancellationTokenAdapter From( IRule rule, IRuleBase next ) => new(rule, next); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Adapters/SimpleAsyncRuleAdapter.cs b/CSharpEssentials.Rules/Adapters/SimpleAsyncRuleAdapter.cs index b94d0cf..823333c 100644 --- a/CSharpEssentials.Rules/Adapters/SimpleAsyncRuleAdapter.cs +++ b/CSharpEssentials.Rules/Adapters/SimpleAsyncRuleAdapter.cs @@ -20,4 +20,4 @@ Func>> Rule public ValueTask> EvaluateAsync(TContext context, CancellationToken cancellationToken = default) => Rule(context, cancellationToken); internal static SimpleAsyncRuleAdapter From(Func>> rule) => new(rule); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/CSharpEssentials.Rules.csproj b/CSharpEssentials.Rules/CSharpEssentials.Rules.csproj index 6aad385..d12e42e 100644 --- a/CSharpEssentials.Rules/CSharpEssentials.Rules.csproj +++ b/CSharpEssentials.Rules/CSharpEssentials.Rules.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 CSharpEssentials.Rules diff --git a/CSharpEssentials.Rules/Engines/And.cs b/CSharpEssentials.Rules/Engines/And.cs index 129898b..c381f9e 100644 --- a/CSharpEssentials.Rules/Engines/And.cs +++ b/CSharpEssentials.Rules/Engines/And.cs @@ -4,27 +4,27 @@ namespace CSharpEssentials.Rules; public static partial class RuleEngine { - public static Result And(IRule[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.And(), context, cancellationToken); - - public static Result And(IRule[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.And(), context, cancellationToken); + public static Result And(IRule[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.And(), context, cancellationToken); - public static Result And(Func[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.And(), context, cancellationToken); + public static Result And(IRule[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.And(), context, cancellationToken); - public static Result And(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.And(), context, cancellationToken); + public static Result And(Func[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.And(), context, cancellationToken); - public static Result And(Func[] rules, TContext context, CancellationToken cancellationToken = default) => + public static Result And(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => Evaluate(rules.And(), context, cancellationToken); - public static Result And(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.And(), context, cancellationToken); + public static Result And(Func[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.And(), context, cancellationToken); - public static Result And(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.And(), context, cancellationToken); + public static Result And(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.And(), context, cancellationToken); + + public static Result And(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.And(), context, cancellationToken); - public static Result And(Func>>[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.And(), context, cancellationToken); -} \ No newline at end of file + public static Result And(Func>>[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.And(), context, cancellationToken); +} diff --git a/CSharpEssentials.Rules/Engines/Conditional.cs b/CSharpEssentials.Rules/Engines/Conditional.cs index 6f2867b..10aa17c 100644 --- a/CSharpEssentials.Rules/Engines/Conditional.cs +++ b/CSharpEssentials.Rules/Engines/Conditional.cs @@ -34,4 +34,4 @@ public static Result If(Func If(Func>> rule, Func>> success, Func>> failure, TContext context, CancellationToken cancellationToken = default) => Evaluate(ConditionalRuleAdapter.From(rule.ToRule(), success.ToRule(), failure.ToRule()), context, cancellationToken); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Engines/Or.cs b/CSharpEssentials.Rules/Engines/Or.cs index 03abf1f..03bd2f6 100644 --- a/CSharpEssentials.Rules/Engines/Or.cs +++ b/CSharpEssentials.Rules/Engines/Or.cs @@ -4,27 +4,27 @@ namespace CSharpEssentials.Rules; public static partial class RuleEngine { - public static Result Or(IRule[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.Or(), context, cancellationToken); - - public static Result Or(IRule[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.Or(), context, cancellationToken); + public static Result Or(IRule[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.Or(), context, cancellationToken); - public static Result Or(Func[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.Or(), context, cancellationToken); + public static Result Or(IRule[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.Or(), context, cancellationToken); - public static Result Or(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.Or(), context, cancellationToken); + public static Result Or(Func[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.Or(), context, cancellationToken); - public static Result Or(Func[] rules, TContext context, CancellationToken cancellationToken = default) => + public static Result Or(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => Evaluate(rules.Or(), context, cancellationToken); - public static Result Or(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.Or(), context, cancellationToken); + public static Result Or(Func[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.Or(), context, cancellationToken); - public static Result Or(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.Or(), context, cancellationToken); + public static Result Or(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.Or(), context, cancellationToken); + + public static Result Or(Func>[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.Or(), context, cancellationToken); - public static Result Or(Func>>[] rules, TContext context, CancellationToken cancellationToken = default) => - Evaluate(rules.Or(), context, cancellationToken); + public static Result Or(Func>>[] rules, TContext context, CancellationToken cancellationToken = default) => + Evaluate(rules.Or(), context, cancellationToken); } diff --git a/CSharpEssentials.Rules/Engines/RuleEngine.cs b/CSharpEssentials.Rules/Engines/RuleEngine.cs index 32415e1..a9d1979 100644 --- a/CSharpEssentials.Rules/Engines/RuleEngine.cs +++ b/CSharpEssentials.Rules/Engines/RuleEngine.cs @@ -1,7 +1,4 @@ -using System.Data; using CSharpEssentials.ResultPattern; -using CSharpEssentials.Rules; -using CSharpEssentials.Core; namespace CSharpEssentials.Rules; @@ -79,12 +76,12 @@ private static Result Evaluate(IConditionalRule rule, TConte { Result result = rule.Evaluate(context, cancellationToken); if (result.IsFailure) - if (rule.Failure.IsNull()) + if (rule.Failure is null) return result; else return InternalEvaluate(rule.Failure, context, cancellationToken); - if (rule.Success.IsNull()) + if (rule.Success is null) return result; return InternalEvaluate(rule.Success, context, cancellationToken); } @@ -99,14 +96,14 @@ private static async ValueTask Evaluate(IConditionalAsyncRule< { Result result = await rule.EvaluateAsync(context, cancellationToken); if (result.IsFailure) - if (rule.Failure.IsNull()) + if (rule.Failure is null) return result; else #pragma warning disable S6966 return InternalEvaluate(rule.Failure, context, cancellationToken); #pragma warning restore S6966 - if (rule.Success.IsNull()) + if (rule.Success is null) return result; #pragma warning disable S6966 return InternalEvaluate(rule.Success, context, cancellationToken); diff --git a/CSharpEssentials.Rules/Engines/RuleEngineTResult.cs b/CSharpEssentials.Rules/Engines/RuleEngineTResult.cs index 1b6fe05..43db2c5 100644 --- a/CSharpEssentials.Rules/Engines/RuleEngineTResult.cs +++ b/CSharpEssentials.Rules/Engines/RuleEngineTResult.cs @@ -1,6 +1,4 @@ using CSharpEssentials.ResultPattern; -using CSharpEssentials.Rules; -using CSharpEssentials.Core; namespace CSharpEssentials.Rules; @@ -78,14 +76,14 @@ private static Result Evaluate(IConditionalRule result = rule.Evaluate(context, cancellationToken); if (result.IsFailure) - if (rule.Failure.IsNull()) + if (rule.Failure is null) return result; else #pragma warning disable S6966 return InternalEvaluate(rule.Failure, context, cancellationToken); #pragma warning restore S6966 - if (rule.Success.IsNull()) + if (rule.Success is null) return result; #pragma warning disable S6966 return InternalEvaluate(rule.Success, context, cancellationToken); @@ -103,14 +101,14 @@ private static async ValueTask> Evaluate(ICon { Result result = await rule.EvaluateAsync(context, cancellationToken); if (result.IsFailure) - if (rule.Failure.IsNull()) + if (rule.Failure is null) return result; else #pragma warning disable S6966 return InternalEvaluate(rule.Failure, context, cancellationToken); #pragma warning restore S6966 - if (rule.Success.IsNull()) + if (rule.Success is null) return result; #pragma warning disable S6966 return InternalEvaluate(rule.Success, context, cancellationToken); diff --git a/CSharpEssentials.Rules/Extensions/AndRuleExtensions.cs b/CSharpEssentials.Rules/Extensions/AndRuleExtensions.cs index 968cf8c..2c93a31 100644 --- a/CSharpEssentials.Rules/Extensions/AndRuleExtensions.cs +++ b/CSharpEssentials.Rules/Extensions/AndRuleExtensions.cs @@ -11,32 +11,20 @@ public static IRuleBase And(this IRuleBase AndRuleAdapter.From(rules); public static IRuleBase And(this Func[] rules) => - AndRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); + AndRuleAdapter.From([.. rules.Select(item => item.ToRule())]); public static IRuleBase And(this Func>[] rules) => -#if NET8_0_OR_GREATER AndRuleAdapter.From([.. rules.Select(item => item.ToRule())]); -#else - AndRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); -#endif public static IRuleBase And(this Func[] rules) => - AndRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); + AndRuleAdapter.From([.. rules.Select(item => item.ToRule())]); public static IRuleBase And(this Func>[] rules) => -#if NET8_0_OR_GREATER AndRuleAdapter.From([.. rules.Select(item => item.ToRule())]); -#else - AndRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); -#endif public static IRuleBase And(this Func>[] rules) => - AndRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); + AndRuleAdapter.From([.. rules.Select(item => item.ToRule())]); public static IRuleBase And(this Func>>[] rules) => -#if NET8_0_OR_GREATER AndRuleAdapter.From([.. rules.Select(item => item.ToRule())]); -#else - AndRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); -#endif } diff --git a/CSharpEssentials.Rules/Extensions/OrRuleExtensions.cs b/CSharpEssentials.Rules/Extensions/OrRuleExtensions.cs index ca98584..0c331db 100644 --- a/CSharpEssentials.Rules/Extensions/OrRuleExtensions.cs +++ b/CSharpEssentials.Rules/Extensions/OrRuleExtensions.cs @@ -1,4 +1,3 @@ - using CSharpEssentials.ResultPattern; namespace CSharpEssentials.Rules; @@ -12,32 +11,20 @@ public static IRuleBase Or(this IRuleBase< OrRuleAdapter.From(rules); public static IRuleBase Or(this Func[] rules) => - OrRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); + OrRuleAdapter.From([.. rules.Select(item => item.ToRule())]); public static IRuleBase Or(this Func>[] rules) => -#if NET8_0_OR_GREATER OrRuleAdapter.From([.. rules.Select(item => item.ToRule())]); -#else - OrRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); -#endif public static IRuleBase Or(this Func[] rules) => - OrRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); + OrRuleAdapter.From([.. rules.Select(item => item.ToRule())]); public static IRuleBase Or(this Func>[] rules) => -#if NET8_0_OR_GREATER OrRuleAdapter.From([.. rules.Select(item => item.ToRule())]); -#else - OrRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); -#endif public static IRuleBase Or(this Func>[] rules) => - OrRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); + OrRuleAdapter.From([.. rules.Select(item => item.ToRule())]); public static IRuleBase Or(this Func>>[] rules) => -#if NET8_0_OR_GREATER OrRuleAdapter.From([.. rules.Select(item => item.ToRule())]); -#else - OrRuleAdapter.From(rules.Select(item => item.ToRule()).ToArray()); -#endif } diff --git a/CSharpEssentials.Rules/Interfaces/IAndAsyncRule.cs b/CSharpEssentials.Rules/Interfaces/IAndAsyncRule.cs index 51d11a5..4bc9123 100644 --- a/CSharpEssentials.Rules/Interfaces/IAndAsyncRule.cs +++ b/CSharpEssentials.Rules/Interfaces/IAndAsyncRule.cs @@ -9,4 +9,4 @@ public interface IAndAsyncRule : IAsyncRule public interface IAndAsyncRule : IAsyncRule { IRuleBase[] Rules { get; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/IAndRule.cs b/CSharpEssentials.Rules/Interfaces/IAndRule.cs index 2e0361a..6bf4885 100644 --- a/CSharpEssentials.Rules/Interfaces/IAndRule.cs +++ b/CSharpEssentials.Rules/Interfaces/IAndRule.cs @@ -9,4 +9,4 @@ public interface IAndRule : IRule public interface IAndRule : IRule { IRuleBase[] Rules { get; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/IAsyncRule.cs b/CSharpEssentials.Rules/Interfaces/IAsyncRule.cs index 7d9c3e0..97beb92 100644 --- a/CSharpEssentials.Rules/Interfaces/IAsyncRule.cs +++ b/CSharpEssentials.Rules/Interfaces/IAsyncRule.cs @@ -11,4 +11,4 @@ public interface IAsyncRule : IAsyncRuleBase public interface IAsyncRule : IAsyncRuleBase { ValueTask> EvaluateAsync(TContext context, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/IAsyncRuleBase.cs b/CSharpEssentials.Rules/Interfaces/IAsyncRuleBase.cs index d305fe1..531766e 100644 --- a/CSharpEssentials.Rules/Interfaces/IAsyncRuleBase.cs +++ b/CSharpEssentials.Rules/Interfaces/IAsyncRuleBase.cs @@ -2,4 +2,4 @@ namespace CSharpEssentials.Rules; public interface IAsyncRuleBase : IRuleBase; -public interface IAsyncRuleBase : IRuleBase; \ No newline at end of file +public interface IAsyncRuleBase : IRuleBase; diff --git a/CSharpEssentials.Rules/Interfaces/IConditionalAsyncRule.cs b/CSharpEssentials.Rules/Interfaces/IConditionalAsyncRule.cs index 6388b94..3b73a2a 100644 --- a/CSharpEssentials.Rules/Interfaces/IConditionalAsyncRule.cs +++ b/CSharpEssentials.Rules/Interfaces/IConditionalAsyncRule.cs @@ -11,4 +11,4 @@ public interface IConditionalAsyncRule : IAsyncRule? Success { get; } IRuleBase? Failure { get; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/IConditionalRule.cs b/CSharpEssentials.Rules/Interfaces/IConditionalRule.cs index a639ade..e5c5adb 100644 --- a/CSharpEssentials.Rules/Interfaces/IConditionalRule.cs +++ b/CSharpEssentials.Rules/Interfaces/IConditionalRule.cs @@ -11,4 +11,4 @@ public interface IConditionalRule : IRule { IRuleBase? Success { get; } IRuleBase? Failure { get; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/ILinearAsyncRule.cs b/CSharpEssentials.Rules/Interfaces/ILinearAsyncRule.cs index c1a12fb..4f7a1b4 100644 --- a/CSharpEssentials.Rules/Interfaces/ILinearAsyncRule.cs +++ b/CSharpEssentials.Rules/Interfaces/ILinearAsyncRule.cs @@ -9,4 +9,4 @@ public interface ILinearAsyncRule : IAsyncRule public interface ILinearAsyncRule : IAsyncRule { IRuleBase? Next { get; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/ILinearRule.cs b/CSharpEssentials.Rules/Interfaces/ILinearRule.cs index f62b6e9..5d7ce60 100644 --- a/CSharpEssentials.Rules/Interfaces/ILinearRule.cs +++ b/CSharpEssentials.Rules/Interfaces/ILinearRule.cs @@ -9,4 +9,4 @@ public interface ILinearRule : IRule public interface ILinearRule : IRule { IRuleBase? Next { get; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/IOrAsyncRule.cs b/CSharpEssentials.Rules/Interfaces/IOrAsyncRule.cs index 35eba39..15b9af4 100644 --- a/CSharpEssentials.Rules/Interfaces/IOrAsyncRule.cs +++ b/CSharpEssentials.Rules/Interfaces/IOrAsyncRule.cs @@ -9,4 +9,4 @@ public interface IOrAsyncRule : IAsyncRule public interface IOrAsyncRule : IAsyncRule { IRuleBase[] Rules { get; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/IOrRule.cs b/CSharpEssentials.Rules/Interfaces/IOrRule.cs index de40269..84a99f8 100644 --- a/CSharpEssentials.Rules/Interfaces/IOrRule.cs +++ b/CSharpEssentials.Rules/Interfaces/IOrRule.cs @@ -9,4 +9,4 @@ public interface IOrRule : IRule public interface IOrRule : IRule { IRuleBase[] Rules { get; } -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/IRule.cs b/CSharpEssentials.Rules/Interfaces/IRule.cs index 77a703a..79488a7 100644 --- a/CSharpEssentials.Rules/Interfaces/IRule.cs +++ b/CSharpEssentials.Rules/Interfaces/IRule.cs @@ -11,4 +11,4 @@ public interface IRule : IRuleBase public interface IRule : IRuleBase { Result Evaluate(TContext context, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/CSharpEssentials.Rules/Interfaces/IRuleBase.cs b/CSharpEssentials.Rules/Interfaces/IRuleBase.cs index fa9a604..96893c2 100644 --- a/CSharpEssentials.Rules/Interfaces/IRuleBase.cs +++ b/CSharpEssentials.Rules/Interfaces/IRuleBase.cs @@ -4,4 +4,4 @@ namespace CSharpEssentials.Rules; #pragma warning disable S2326 public interface IRuleBase; public interface IRuleBase; -#pragma warning restore S2326 \ No newline at end of file +#pragma warning restore S2326 diff --git a/CSharpEssentials.Tests/Any/AnyExtensionsTests.cs b/CSharpEssentials.Tests/Any/AnyExtensionsTests.cs index dbff21a..24101f6 100644 --- a/CSharpEssentials.Tests/Any/AnyExtensionsTests.cs +++ b/CSharpEssentials.Tests/Any/AnyExtensionsTests.cs @@ -11,7 +11,7 @@ public class AnyExtensionsTests public void Deconstruct_AnyT2_First_ShouldReturnFirst() { Any any = 42; - var (first, second) = any; + (int first, string? second) = any; first.Should().Be(42); second.Should().BeNull(); @@ -21,7 +21,7 @@ public void Deconstruct_AnyT2_First_ShouldReturnFirst() public void Deconstruct_AnyT2_Second_ShouldReturnSecond() { Any any = "hello"; - var (first, second) = any; + (int first, string? second) = any; first.Should().Be(0); second.Should().Be("hello"); @@ -31,7 +31,7 @@ public void Deconstruct_AnyT2_Second_ShouldReturnSecond() public void ToTuple_AnyT2_First_ShouldReturnFirst() { Any any = 42; - var tuple = any.ToTuple(); + (int First, string? Second) tuple = any.ToTuple(); tuple.First.Should().Be(42); tuple.Second.Should().BeNull(); @@ -41,7 +41,7 @@ public void ToTuple_AnyT2_First_ShouldReturnFirst() public void ToTuple_AnyT2_Second_ShouldReturnSecond() { Any any = "hello"; - var tuple = any.ToTuple(); + (int First, string? Second) tuple = any.ToTuple(); tuple.First.Should().Be(0); tuple.Second.Should().Be("hello"); @@ -148,7 +148,7 @@ public void TryAs_AnyT2_Second_WithMatchingType_ShouldReturnTrueAndValue() public void Deconstruct_AnyT3_First_ShouldReturnFirst() { Any any = 42; - var (first, second, third) = any; + (int first, string? second, double third) = any; first.Should().Be(42); second.Should().BeNull(); @@ -159,7 +159,7 @@ public void Deconstruct_AnyT3_First_ShouldReturnFirst() public void Deconstruct_AnyT3_Second_ShouldReturnSecond() { Any any = "hello"; - var (first, second, third) = any; + (int first, string? second, double third) = any; first.Should().Be(0); second.Should().Be("hello"); @@ -170,7 +170,7 @@ public void Deconstruct_AnyT3_Second_ShouldReturnSecond() public void Deconstruct_AnyT3_Third_ShouldReturnThird() { Any any = 3.14; - var (first, second, third) = any; + (int first, string? second, double third) = any; first.Should().Be(0); second.Should().BeNull(); @@ -181,7 +181,7 @@ public void Deconstruct_AnyT3_Third_ShouldReturnThird() public void ToTuple_AnyT3_First_ShouldReturnFirst() { Any any = 42; - var tuple = any.ToTuple(); + (int First, string? Second, double Third) tuple = any.ToTuple(); tuple.First.Should().Be(42); tuple.Second.Should().BeNull(); @@ -192,7 +192,7 @@ public void ToTuple_AnyT3_First_ShouldReturnFirst() public void ToTuple_AnyT3_Second_ShouldReturnSecond() { Any any = "hello"; - var tuple = any.ToTuple(); + (int First, string? Second, double Third) tuple = any.ToTuple(); tuple.First.Should().Be(0); tuple.Second.Should().Be("hello"); @@ -203,7 +203,7 @@ public void ToTuple_AnyT3_Second_ShouldReturnSecond() public void ToTuple_AnyT3_Third_ShouldReturnThird() { Any any = 3.14; - var tuple = any.ToTuple(); + (int First, string? Second, double Third) tuple = any.ToTuple(); tuple.First.Should().Be(0); tuple.Second.Should().BeNull(); diff --git a/CSharpEssentials.Tests/Any/AnyT2Tests.cs b/CSharpEssentials.Tests/Any/AnyT2Tests.cs index 5e40ccf..2842497 100644 --- a/CSharpEssentials.Tests/Any/AnyT2Tests.cs +++ b/CSharpEssentials.Tests/Any/AnyT2Tests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Any; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Any; @@ -59,7 +59,7 @@ public void Switch_WhenFirst_ShouldExecuteFirstAction() { Any any = 42; bool executed = false; - var status = any.Switch(first: _ => executed = true); + AnyActionStatus status = any.Switch(first: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -69,7 +69,7 @@ public void Switch_WhenSecond_ShouldExecuteSecondAction() { Any any = "hello"; bool executed = false; - var status = any.Switch(second: _ => executed = true); + AnyActionStatus status = any.Switch(second: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -78,7 +78,7 @@ public void Switch_WhenSecond_ShouldExecuteSecondAction() public void Switch_WithNoMatchingAction_ShouldReturnNotExecuted() { Any any = 42; - var status = any.Switch(second: _ => { }); + AnyActionStatus status = any.Switch(second: _ => { }); status.Should().Be(AnyActionStatus.NotExecuted); } @@ -93,7 +93,7 @@ public void Switch_WithNullValue_ShouldThrowInvalidOperationException() public void Match_WhenFirst_ShouldReturnFirstResult() { Any any = 42; - var result = any.Match(first: x => x * 2, second: _ => 0); + AnyActionResult result = any.Match(first: x => x * 2, second: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(84); } @@ -102,7 +102,7 @@ public void Match_WhenFirst_ShouldReturnFirstResult() public void Match_WhenSecond_ShouldReturnSecondResult() { Any any = "hello"; - var result = any.Match(first: _ => 0, second: x => x.Length); + AnyActionResult result = any.Match(first: _ => 0, second: x => x.Length); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(5); } @@ -111,7 +111,7 @@ public void Match_WhenSecond_ShouldReturnSecondResult() public void Match_WithNoMatchingFunction_ShouldReturnNotExecuted() { Any any = 42; - var result = any.Match(second: _ => 0); + AnyActionResult result = any.Match(second: _ => 0); result.Status.Should().Be(AnyActionStatus.NotExecuted); } @@ -134,7 +134,7 @@ public void JsonSerialization_First_ShouldRoundTrip() { Any original = 42; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFirst().Should().Be(42); } @@ -144,7 +144,7 @@ public void JsonSerialization_Second_ShouldRoundTrip() { Any original = "hello"; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSecond().Should().Be("hello"); } diff --git a/CSharpEssentials.Tests/Any/AnyT3Tests.cs b/CSharpEssentials.Tests/Any/AnyT3Tests.cs index 3ccb92e..4df6d02 100644 --- a/CSharpEssentials.Tests/Any/AnyT3Tests.cs +++ b/CSharpEssentials.Tests/Any/AnyT3Tests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Any; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Any; @@ -86,7 +86,7 @@ public void Switch_WhenFirst_ShouldExecuteFirstAction() { Any any = 42; bool executed = false; - var status = any.Switch(first: _ => executed = true); + AnyActionStatus status = any.Switch(first: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -96,7 +96,7 @@ public void Switch_WhenSecond_ShouldExecuteSecondAction() { Any any = "hello"; bool executed = false; - var status = any.Switch(second: _ => executed = true); + AnyActionStatus status = any.Switch(second: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -106,7 +106,7 @@ public void Switch_WhenThird_ShouldExecuteThirdAction() { Any any = true; bool executed = false; - var status = any.Switch(third: _ => executed = true); + AnyActionStatus status = any.Switch(third: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -115,7 +115,7 @@ public void Switch_WhenThird_ShouldExecuteThirdAction() public void Switch_WithNoMatchingAction_ShouldReturnNotExecuted() { Any any = 42; - var status = any.Switch(second: _ => { }); + AnyActionStatus status = any.Switch(second: _ => { }); status.Should().Be(AnyActionStatus.NotExecuted); } @@ -130,7 +130,7 @@ public void Switch_WithNullValue_ShouldThrowInvalidOperationException() public void Match_WhenFirst_ShouldReturnFirstResult() { Any any = 42; - var result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0); + AnyActionResult result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(84); } @@ -139,7 +139,7 @@ public void Match_WhenFirst_ShouldReturnFirstResult() public void Match_WhenSecond_ShouldReturnSecondResult() { Any any = "hello"; - var result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(5); } @@ -148,7 +148,7 @@ public void Match_WhenSecond_ShouldReturnSecondResult() public void Match_WhenThird_ShouldReturnThirdResult() { Any any = true; - var result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(1); } @@ -157,7 +157,7 @@ public void Match_WhenThird_ShouldReturnThirdResult() public void Match_WithNoMatchingFunction_ShouldReturnNotExecuted() { Any any = 42; - var result = any.Match(second: _ => 0); + AnyActionResult result = any.Match(second: _ => 0); result.Status.Should().Be(AnyActionStatus.NotExecuted); } @@ -180,7 +180,7 @@ public void JsonSerialization_First_ShouldRoundTrip() { Any original = 42; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFirst().Should().Be(42); } @@ -190,7 +190,7 @@ public void JsonSerialization_Second_ShouldRoundTrip() { Any original = "hello"; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSecond().Should().Be("hello"); } @@ -200,7 +200,7 @@ public void JsonSerialization_Third_ShouldRoundTrip() { Any original = true; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetThird().Should().BeTrue(); } diff --git a/CSharpEssentials.Tests/Any/AnyT4Tests.cs b/CSharpEssentials.Tests/Any/AnyT4Tests.cs index e684629..ad090e9 100644 --- a/CSharpEssentials.Tests/Any/AnyT4Tests.cs +++ b/CSharpEssentials.Tests/Any/AnyT4Tests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Any; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Any; @@ -115,7 +115,7 @@ public void Switch_WhenFirst_ShouldExecuteFirstAction() { Any any = 42; bool executed = false; - var status = any.Switch(first: _ => executed = true); + AnyActionStatus status = any.Switch(first: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -125,7 +125,7 @@ public void Switch_WhenSecond_ShouldExecuteSecondAction() { Any any = "hello"; bool executed = false; - var status = any.Switch(second: _ => executed = true); + AnyActionStatus status = any.Switch(second: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -135,7 +135,7 @@ public void Switch_WhenThird_ShouldExecuteThirdAction() { Any any = true; bool executed = false; - var status = any.Switch(third: _ => executed = true); + AnyActionStatus status = any.Switch(third: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -145,7 +145,7 @@ public void Switch_WhenFourth_ShouldExecuteFourthAction() { Any any = 3.14; bool executed = false; - var status = any.Switch(fourth: _ => executed = true); + AnyActionStatus status = any.Switch(fourth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -154,7 +154,7 @@ public void Switch_WhenFourth_ShouldExecuteFourthAction() public void Switch_WithNoMatchingAction_ShouldReturnNotExecuted() { Any any = 42; - var status = any.Switch(second: _ => { }); + AnyActionStatus status = any.Switch(second: _ => { }); status.Should().Be(AnyActionStatus.NotExecuted); } @@ -169,7 +169,7 @@ public void Switch_WithNullValue_ShouldThrowInvalidOperationException() public void Match_WhenFirst_ShouldReturnFirstResult() { Any any = 42; - var result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0); + AnyActionResult result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(84); } @@ -178,7 +178,7 @@ public void Match_WhenFirst_ShouldReturnFirstResult() public void Match_WhenSecond_ShouldReturnSecondResult() { Any any = "hello"; - var result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(5); } @@ -187,7 +187,7 @@ public void Match_WhenSecond_ShouldReturnSecondResult() public void Match_WhenThird_ShouldReturnThirdResult() { Any any = true; - var result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(1); } @@ -196,7 +196,7 @@ public void Match_WhenThird_ShouldReturnThirdResult() public void Match_WhenFourth_ShouldReturnFourthResult() { Any any = 3.14; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(3); } @@ -205,7 +205,7 @@ public void Match_WhenFourth_ShouldReturnFourthResult() public void Match_WithNoMatchingFunction_ShouldReturnNotExecuted() { Any any = 42; - var result = any.Match(second: _ => 0); + AnyActionResult result = any.Match(second: _ => 0); result.Status.Should().Be(AnyActionStatus.NotExecuted); } @@ -228,7 +228,7 @@ public void JsonSerialization_First_ShouldRoundTrip() { Any original = 42; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFirst().Should().Be(42); } @@ -238,7 +238,7 @@ public void JsonSerialization_Second_ShouldRoundTrip() { Any original = "hello"; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSecond().Should().Be("hello"); } @@ -248,7 +248,7 @@ public void JsonSerialization_Third_ShouldRoundTrip() { Any original = true; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetThird().Should().BeTrue(); } @@ -258,7 +258,7 @@ public void JsonSerialization_Fourth_ShouldRoundTrip() { Any original = 3.14; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFourth().Should().Be(3.14); } diff --git a/CSharpEssentials.Tests/Any/AnyT5Tests.cs b/CSharpEssentials.Tests/Any/AnyT5Tests.cs index 07ff1c2..afc2f9e 100644 --- a/CSharpEssentials.Tests/Any/AnyT5Tests.cs +++ b/CSharpEssentials.Tests/Any/AnyT5Tests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Any; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Any; @@ -146,7 +146,7 @@ public void Switch_WhenFirst_ShouldExecuteFirstAction() { Any any = 42; bool executed = false; - var status = any.Switch(first: _ => executed = true); + AnyActionStatus status = any.Switch(first: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -156,7 +156,7 @@ public void Switch_WhenSecond_ShouldExecuteSecondAction() { Any any = "hello"; bool executed = false; - var status = any.Switch(second: _ => executed = true); + AnyActionStatus status = any.Switch(second: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -166,7 +166,7 @@ public void Switch_WhenThird_ShouldExecuteThirdAction() { Any any = true; bool executed = false; - var status = any.Switch(third: _ => executed = true); + AnyActionStatus status = any.Switch(third: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -176,7 +176,7 @@ public void Switch_WhenFourth_ShouldExecuteFourthAction() { Any any = 3.14; bool executed = false; - var status = any.Switch(fourth: _ => executed = true); + AnyActionStatus status = any.Switch(fourth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -186,7 +186,7 @@ public void Switch_WhenFifth_ShouldExecuteFifthAction() { Any any = 99L; bool executed = false; - var status = any.Switch(fifth: _ => executed = true); + AnyActionStatus status = any.Switch(fifth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -195,7 +195,7 @@ public void Switch_WhenFifth_ShouldExecuteFifthAction() public void Switch_WithNoMatchingAction_ShouldReturnNotExecuted() { Any any = 42; - var status = any.Switch(second: _ => { }); + AnyActionStatus status = any.Switch(second: _ => { }); status.Should().Be(AnyActionStatus.NotExecuted); } @@ -210,7 +210,7 @@ public void Switch_WithNullValue_ShouldThrowInvalidOperationException() public void Match_WhenFirst_ShouldReturnFirstResult() { Any any = 42; - var result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0); + AnyActionResult result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(84); } @@ -219,7 +219,7 @@ public void Match_WhenFirst_ShouldReturnFirstResult() public void Match_WhenSecond_ShouldReturnSecondResult() { Any any = "hello"; - var result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0, fifth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0, fifth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(5); } @@ -228,7 +228,7 @@ public void Match_WhenSecond_ShouldReturnSecondResult() public void Match_WhenThird_ShouldReturnThirdResult() { Any any = true; - var result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0, fifth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0, fifth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(1); } @@ -237,7 +237,7 @@ public void Match_WhenThird_ShouldReturnThirdResult() public void Match_WhenFourth_ShouldReturnFourthResult() { Any any = 3.14; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x, fifth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x, fifth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(3); } @@ -246,7 +246,7 @@ public void Match_WhenFourth_ShouldReturnFourthResult() public void Match_WhenFifth_ShouldReturnFifthResult() { Any any = 99L; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: x => (int)x); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: x => (int)x); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(99); } @@ -255,7 +255,7 @@ public void Match_WhenFifth_ShouldReturnFifthResult() public void Match_WithNoMatchingFunction_ShouldReturnNotExecuted() { Any any = 42; - var result = any.Match(second: _ => 0); + AnyActionResult result = any.Match(second: _ => 0); result.Status.Should().Be(AnyActionStatus.NotExecuted); } @@ -278,7 +278,7 @@ public void JsonSerialization_First_ShouldRoundTrip() { Any original = 42; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFirst().Should().Be(42); } @@ -288,7 +288,7 @@ public void JsonSerialization_Second_ShouldRoundTrip() { Any original = "hello"; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSecond().Should().Be("hello"); } @@ -298,7 +298,7 @@ public void JsonSerialization_Third_ShouldRoundTrip() { Any original = true; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetThird().Should().BeTrue(); } @@ -308,7 +308,7 @@ public void JsonSerialization_Fourth_ShouldRoundTrip() { Any original = 3.14; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFourth().Should().Be(3.14); } @@ -318,7 +318,7 @@ public void JsonSerialization_Fifth_ShouldRoundTrip() { Any original = 99L; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFifth().Should().Be(99L); } diff --git a/CSharpEssentials.Tests/Any/AnyT6Tests.cs b/CSharpEssentials.Tests/Any/AnyT6Tests.cs index 9422e34..945cf0c 100644 --- a/CSharpEssentials.Tests/Any/AnyT6Tests.cs +++ b/CSharpEssentials.Tests/Any/AnyT6Tests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Any; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Any; @@ -180,7 +180,7 @@ public void Switch_WhenFirst_ShouldExecuteFirstAction() { Any any = 42; bool executed = false; - var status = any.Switch(first: _ => executed = true); + AnyActionStatus status = any.Switch(first: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -190,7 +190,7 @@ public void Switch_WhenSecond_ShouldExecuteSecondAction() { Any any = "hello"; bool executed = false; - var status = any.Switch(second: _ => executed = true); + AnyActionStatus status = any.Switch(second: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -200,7 +200,7 @@ public void Switch_WhenThird_ShouldExecuteThirdAction() { Any any = true; bool executed = false; - var status = any.Switch(third: _ => executed = true); + AnyActionStatus status = any.Switch(third: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -210,7 +210,7 @@ public void Switch_WhenFourth_ShouldExecuteFourthAction() { Any any = 3.14; bool executed = false; - var status = any.Switch(fourth: _ => executed = true); + AnyActionStatus status = any.Switch(fourth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -220,7 +220,7 @@ public void Switch_WhenFifth_ShouldExecuteFifthAction() { Any any = 99L; bool executed = false; - var status = any.Switch(fifth: _ => executed = true); + AnyActionStatus status = any.Switch(fifth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -230,7 +230,7 @@ public void Switch_WhenSixth_ShouldExecuteSixthAction() { Any any = 3.99m; bool executed = false; - var status = any.Switch(sixth: _ => executed = true); + AnyActionStatus status = any.Switch(sixth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -239,7 +239,7 @@ public void Switch_WhenSixth_ShouldExecuteSixthAction() public void Switch_WithNoMatchingAction_ShouldReturnNotExecuted() { Any any = 42; - var status = any.Switch(second: _ => { }); + AnyActionStatus status = any.Switch(second: _ => { }); status.Should().Be(AnyActionStatus.NotExecuted); } @@ -254,7 +254,7 @@ public void Switch_WithNullValue_ShouldThrowInvalidOperationException() public void Match_WhenFirst_ShouldReturnFirstResult() { Any any = 42; - var result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0); + AnyActionResult result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(84); } @@ -263,7 +263,7 @@ public void Match_WhenFirst_ShouldReturnFirstResult() public void Match_WhenSecond_ShouldReturnSecondResult() { Any any = "hello"; - var result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(5); } @@ -272,7 +272,7 @@ public void Match_WhenSecond_ShouldReturnSecondResult() public void Match_WhenThird_ShouldReturnThirdResult() { Any any = true; - var result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(1); } @@ -281,7 +281,7 @@ public void Match_WhenThird_ShouldReturnThirdResult() public void Match_WhenFourth_ShouldReturnFourthResult() { Any any = 3.14; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x, fifth: _ => 0, sixth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x, fifth: _ => 0, sixth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(3); } @@ -290,7 +290,7 @@ public void Match_WhenFourth_ShouldReturnFourthResult() public void Match_WhenFifth_ShouldReturnFifthResult() { Any any = 99L; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: x => (int)x, sixth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: x => (int)x, sixth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(99); } @@ -299,7 +299,7 @@ public void Match_WhenFifth_ShouldReturnFifthResult() public void Match_WhenSixth_ShouldReturnSixthResult() { Any any = 3.99m; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: x => (int)x); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: x => (int)x); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(3); } @@ -308,7 +308,7 @@ public void Match_WhenSixth_ShouldReturnSixthResult() public void Match_WithNoMatchingFunction_ShouldReturnNotExecuted() { Any any = 42; - var result = any.Match(second: _ => 0); + AnyActionResult result = any.Match(second: _ => 0); result.Status.Should().Be(AnyActionStatus.NotExecuted); } @@ -331,7 +331,7 @@ public void JsonSerialization_First_ShouldRoundTrip() { Any original = 42; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFirst().Should().Be(42); } @@ -341,7 +341,7 @@ public void JsonSerialization_Second_ShouldRoundTrip() { Any original = "hello"; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSecond().Should().Be("hello"); } @@ -351,7 +351,7 @@ public void JsonSerialization_Third_ShouldRoundTrip() { Any original = true; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetThird().Should().BeTrue(); } @@ -361,7 +361,7 @@ public void JsonSerialization_Fourth_ShouldRoundTrip() { Any original = 3.14; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFourth().Should().Be(3.14); } @@ -371,7 +371,7 @@ public void JsonSerialization_Fifth_ShouldRoundTrip() { Any original = 99L; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFifth().Should().Be(99L); } @@ -381,7 +381,7 @@ public void JsonSerialization_Sixth_ShouldRoundTrip() { Any original = 3.99m; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSixth().Should().Be(3.99m); } diff --git a/CSharpEssentials.Tests/Any/AnyT7Tests.cs b/CSharpEssentials.Tests/Any/AnyT7Tests.cs index 6cc3d68..b82c2b6 100644 --- a/CSharpEssentials.Tests/Any/AnyT7Tests.cs +++ b/CSharpEssentials.Tests/Any/AnyT7Tests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Any; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Any; @@ -216,7 +216,7 @@ public void Switch_WhenFirst_ShouldExecuteFirstAction() { Any any = 42; bool executed = false; - var status = any.Switch(first: _ => executed = true); + AnyActionStatus status = any.Switch(first: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -226,7 +226,7 @@ public void Switch_WhenSecond_ShouldExecuteSecondAction() { Any any = "hello"; bool executed = false; - var status = any.Switch(second: _ => executed = true); + AnyActionStatus status = any.Switch(second: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -236,7 +236,7 @@ public void Switch_WhenThird_ShouldExecuteThirdAction() { Any any = true; bool executed = false; - var status = any.Switch(third: _ => executed = true); + AnyActionStatus status = any.Switch(third: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -246,7 +246,7 @@ public void Switch_WhenFourth_ShouldExecuteFourthAction() { Any any = 3.14; bool executed = false; - var status = any.Switch(fourth: _ => executed = true); + AnyActionStatus status = any.Switch(fourth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -256,7 +256,7 @@ public void Switch_WhenFifth_ShouldExecuteFifthAction() { Any any = 99L; bool executed = false; - var status = any.Switch(fifth: _ => executed = true); + AnyActionStatus status = any.Switch(fifth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -266,7 +266,7 @@ public void Switch_WhenSixth_ShouldExecuteSixthAction() { Any any = 3.99m; bool executed = false; - var status = any.Switch(sixth: _ => executed = true); + AnyActionStatus status = any.Switch(sixth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -276,7 +276,7 @@ public void Switch_WhenSeventh_ShouldExecuteSeventhAction() { Any any = TestGuid; bool executed = false; - var status = any.Switch(seventh: _ => executed = true); + AnyActionStatus status = any.Switch(seventh: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -285,7 +285,7 @@ public void Switch_WhenSeventh_ShouldExecuteSeventhAction() public void Switch_WithNoMatchingAction_ShouldReturnNotExecuted() { Any any = 42; - var status = any.Switch(second: _ => { }); + AnyActionStatus status = any.Switch(second: _ => { }); status.Should().Be(AnyActionStatus.NotExecuted); } @@ -300,7 +300,7 @@ public void Switch_WithNullValue_ShouldThrowInvalidOperationException() public void Match_WhenFirst_ShouldReturnFirstResult() { Any any = 42; - var result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0); + AnyActionResult result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(84); } @@ -309,7 +309,7 @@ public void Match_WhenFirst_ShouldReturnFirstResult() public void Match_WhenSecond_ShouldReturnSecondResult() { Any any = "hello"; - var result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(5); } @@ -318,7 +318,7 @@ public void Match_WhenSecond_ShouldReturnSecondResult() public void Match_WhenThird_ShouldReturnThirdResult() { Any any = true; - var result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(1); } @@ -327,7 +327,7 @@ public void Match_WhenThird_ShouldReturnThirdResult() public void Match_WhenFourth_ShouldReturnFourthResult() { Any any = 3.14; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(3); } @@ -336,7 +336,7 @@ public void Match_WhenFourth_ShouldReturnFourthResult() public void Match_WhenFifth_ShouldReturnFifthResult() { Any any = 99L; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: x => (int)x, sixth: _ => 0, seventh: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: x => (int)x, sixth: _ => 0, seventh: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(99); } @@ -345,7 +345,7 @@ public void Match_WhenFifth_ShouldReturnFifthResult() public void Match_WhenSixth_ShouldReturnSixthResult() { Any any = 3.99m; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: x => (int)x, seventh: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: x => (int)x, seventh: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(3); } @@ -354,7 +354,7 @@ public void Match_WhenSixth_ShouldReturnSixthResult() public void Match_WhenSeventh_ShouldReturnSeventhResult() { Any any = TestGuid; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: x => x.GetHashCode()); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: x => x.GetHashCode()); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(TestGuid.GetHashCode()); } @@ -363,7 +363,7 @@ public void Match_WhenSeventh_ShouldReturnSeventhResult() public void Match_WithNoMatchingFunction_ShouldReturnNotExecuted() { Any any = 42; - var result = any.Match(second: _ => 0); + AnyActionResult result = any.Match(second: _ => 0); result.Status.Should().Be(AnyActionStatus.NotExecuted); } @@ -386,7 +386,7 @@ public void JsonSerialization_First_ShouldRoundTrip() { Any original = 42; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFirst().Should().Be(42); } @@ -396,7 +396,7 @@ public void JsonSerialization_Second_ShouldRoundTrip() { Any original = "hello"; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSecond().Should().Be("hello"); } @@ -406,7 +406,7 @@ public void JsonSerialization_Third_ShouldRoundTrip() { Any original = true; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetThird().Should().BeTrue(); } @@ -416,7 +416,7 @@ public void JsonSerialization_Fourth_ShouldRoundTrip() { Any original = 3.14; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFourth().Should().Be(3.14); } @@ -426,7 +426,7 @@ public void JsonSerialization_Fifth_ShouldRoundTrip() { Any original = 99L; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFifth().Should().Be(99L); } @@ -436,7 +436,7 @@ public void JsonSerialization_Sixth_ShouldRoundTrip() { Any original = 3.99m; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSixth().Should().Be(3.99m); } @@ -446,7 +446,7 @@ public void JsonSerialization_Seventh_ShouldRoundTrip() { Any original = TestGuid; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSeventh().Should().Be(TestGuid); } diff --git a/CSharpEssentials.Tests/Any/AnyT8Tests.cs b/CSharpEssentials.Tests/Any/AnyT8Tests.cs index eea24a2..2a0986a 100644 --- a/CSharpEssentials.Tests/Any/AnyT8Tests.cs +++ b/CSharpEssentials.Tests/Any/AnyT8Tests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Any; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Any; @@ -253,7 +253,7 @@ public void Switch_WhenFirst_ShouldExecuteFirstAction() { Any any = 42; bool executed = false; - var status = any.Switch(first: _ => executed = true); + AnyActionStatus status = any.Switch(first: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -263,7 +263,7 @@ public void Switch_WhenSecond_ShouldExecuteSecondAction() { Any any = "hello"; bool executed = false; - var status = any.Switch(second: _ => executed = true); + AnyActionStatus status = any.Switch(second: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -273,7 +273,7 @@ public void Switch_WhenThird_ShouldExecuteThirdAction() { Any any = true; bool executed = false; - var status = any.Switch(third: _ => executed = true); + AnyActionStatus status = any.Switch(third: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -283,7 +283,7 @@ public void Switch_WhenFourth_ShouldExecuteFourthAction() { Any any = 3.14; bool executed = false; - var status = any.Switch(fourth: _ => executed = true); + AnyActionStatus status = any.Switch(fourth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -293,7 +293,7 @@ public void Switch_WhenFifth_ShouldExecuteFifthAction() { Any any = 99L; bool executed = false; - var status = any.Switch(fifth: _ => executed = true); + AnyActionStatus status = any.Switch(fifth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -303,7 +303,7 @@ public void Switch_WhenSixth_ShouldExecuteSixthAction() { Any any = 3.99m; bool executed = false; - var status = any.Switch(sixth: _ => executed = true); + AnyActionStatus status = any.Switch(sixth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -313,7 +313,7 @@ public void Switch_WhenSeventh_ShouldExecuteSeventhAction() { Any any = TestGuid; bool executed = false; - var status = any.Switch(seventh: _ => executed = true); + AnyActionStatus status = any.Switch(seventh: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -323,7 +323,7 @@ public void Switch_WhenEighth_ShouldExecuteEighthAction() { Any any = 'x'; bool executed = false; - var status = any.Switch(eighth: _ => executed = true); + AnyActionStatus status = any.Switch(eighth: _ => executed = true); executed.Should().BeTrue(); status.Should().Be(AnyActionStatus.Executed); } @@ -332,7 +332,7 @@ public void Switch_WhenEighth_ShouldExecuteEighthAction() public void Switch_WithNoMatchingAction_ShouldReturnNotExecuted() { Any any = 42; - var status = any.Switch(second: _ => { }); + AnyActionStatus status = any.Switch(second: _ => { }); status.Should().Be(AnyActionStatus.NotExecuted); } @@ -347,7 +347,7 @@ public void Switch_WithNullValue_ShouldThrowInvalidOperationException() public void Match_WhenFirst_ShouldReturnFirstResult() { Any any = 42; - var result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); + AnyActionResult result = any.Match(first: x => x * 2, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(84); } @@ -356,7 +356,7 @@ public void Match_WhenFirst_ShouldReturnFirstResult() public void Match_WhenSecond_ShouldReturnSecondResult() { Any any = "hello"; - var result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: x => x.Length, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(5); } @@ -365,7 +365,7 @@ public void Match_WhenSecond_ShouldReturnSecondResult() public void Match_WhenThird_ShouldReturnThirdResult() { Any any = true; - var result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: x => x ? 1 : 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(1); } @@ -374,7 +374,7 @@ public void Match_WhenThird_ShouldReturnThirdResult() public void Match_WhenFourth_ShouldReturnFourthResult() { Any any = 3.14; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: x => (int)x, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(3); } @@ -383,7 +383,7 @@ public void Match_WhenFourth_ShouldReturnFourthResult() public void Match_WhenFifth_ShouldReturnFifthResult() { Any any = 99L; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: x => (int)x, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: x => (int)x, sixth: _ => 0, seventh: _ => 0, eighth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(99); } @@ -392,7 +392,7 @@ public void Match_WhenFifth_ShouldReturnFifthResult() public void Match_WhenSixth_ShouldReturnSixthResult() { Any any = 3.99m; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: x => (int)x, seventh: _ => 0, eighth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: x => (int)x, seventh: _ => 0, eighth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(3); } @@ -401,7 +401,7 @@ public void Match_WhenSixth_ShouldReturnSixthResult() public void Match_WhenSeventh_ShouldReturnSeventhResult() { Any any = TestGuid; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: x => x.GetHashCode(), eighth: _ => 0); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: x => x.GetHashCode(), eighth: _ => 0); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(TestGuid.GetHashCode()); } @@ -410,7 +410,7 @@ public void Match_WhenSeventh_ShouldReturnSeventhResult() public void Match_WhenEighth_ShouldReturnEighthResult() { Any any = 'x'; - var result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: x => (int)x); + AnyActionResult result = any.Match(first: _ => 0, second: _ => 0, third: _ => 0, fourth: _ => 0, fifth: _ => 0, sixth: _ => 0, seventh: _ => 0, eighth: x => (int)x); result.Status.Should().Be(AnyActionStatus.Executed); result.Result.Should().Be(120); } @@ -419,7 +419,7 @@ public void Match_WhenEighth_ShouldReturnEighthResult() public void Match_WithNoMatchingFunction_ShouldReturnNotExecuted() { Any any = 42; - var result = any.Match(second: _ => 0); + AnyActionResult result = any.Match(second: _ => 0); result.Status.Should().Be(AnyActionStatus.NotExecuted); } @@ -442,7 +442,7 @@ public void JsonSerialization_First_ShouldRoundTrip() { Any original = 42; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFirst().Should().Be(42); } @@ -452,7 +452,7 @@ public void JsonSerialization_Second_ShouldRoundTrip() { Any original = "hello"; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSecond().Should().Be("hello"); } @@ -462,7 +462,7 @@ public void JsonSerialization_Third_ShouldRoundTrip() { Any original = true; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetThird().Should().BeTrue(); } @@ -472,7 +472,7 @@ public void JsonSerialization_Fourth_ShouldRoundTrip() { Any original = 3.14; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFourth().Should().Be(3.14); } @@ -482,7 +482,7 @@ public void JsonSerialization_Fifth_ShouldRoundTrip() { Any original = 99L; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetFifth().Should().Be(99L); } @@ -492,7 +492,7 @@ public void JsonSerialization_Sixth_ShouldRoundTrip() { Any original = 3.99m; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSixth().Should().Be(3.99m); } @@ -502,7 +502,7 @@ public void JsonSerialization_Seventh_ShouldRoundTrip() { Any original = TestGuid; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetSeventh().Should().Be(TestGuid); } @@ -512,7 +512,7 @@ public void JsonSerialization_Eighth_ShouldRoundTrip() { Any original = 'x'; string json = JsonSerializer.Serialize(original); - var deserialized = JsonSerializer.Deserialize>(json); + Any deserialized = JsonSerializer.Deserialize>(json); deserialized.Index.Should().Be(original.Index); deserialized.GetEighth().Should().Be('x'); } diff --git a/CSharpEssentials.Tests/Any/AnyTests.cs b/CSharpEssentials.Tests/Any/AnyTests.cs index ed64814..ec8ec1f 100644 --- a/CSharpEssentials.Tests/Any/AnyTests.cs +++ b/CSharpEssentials.Tests/Any/AnyTests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Any; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Any; diff --git a/CSharpEssentials.Tests/AspNetCore/EnumSchemaFilterTests.cs b/CSharpEssentials.Tests/AspNetCore/EnumSchemaFilterTests.cs index 1aef070..dd99562 100644 --- a/CSharpEssentials.Tests/AspNetCore/EnumSchemaFilterTests.cs +++ b/CSharpEssentials.Tests/AspNetCore/EnumSchemaFilterTests.cs @@ -1,7 +1,6 @@ using CSharpEssentials.AspNetCore.Swagger.Filters; using CSharpEssentials.Enums; using FluentAssertions; -using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -28,7 +27,7 @@ public void Apply_ForStringEnum_ShouldSetSchemaTypeToString() { var filter = new EnumSchemaFilter(); var schema = new OpenApiSchema(); - var context = CreateContext(typeof(TestString)); + SchemaFilterContext context = CreateContext(typeof(TestString)); filter.Apply(schema, context); @@ -42,7 +41,7 @@ public void Apply_ForIntEnum_ShouldNotModifySchema() { var filter = new EnumSchemaFilter(); var schema = new OpenApiSchema { Type = "integer" }; - var context = CreateContext(typeof(TestInt)); + SchemaFilterContext context = CreateContext(typeof(TestInt)); filter.Apply(schema, context); diff --git a/CSharpEssentials.Tests/AspNetCore/GlobalExceptionHandlerTests.cs b/CSharpEssentials.Tests/AspNetCore/GlobalExceptionHandlerTests.cs index 34dcf8a..7ce8d93 100644 --- a/CSharpEssentials.Tests/AspNetCore/GlobalExceptionHandlerTests.cs +++ b/CSharpEssentials.Tests/AspNetCore/GlobalExceptionHandlerTests.cs @@ -1,6 +1,5 @@ using CSharpEssentials.AspNetCore; using FluentAssertions; -using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; diff --git a/CSharpEssentials.Tests/AspNetCore/ResultEndpointFilterTests.cs b/CSharpEssentials.Tests/AspNetCore/ResultEndpointFilterTests.cs new file mode 100644 index 0000000..48c9559 --- /dev/null +++ b/CSharpEssentials.Tests/AspNetCore/ResultEndpointFilterTests.cs @@ -0,0 +1,80 @@ +using CSharpEssentials.AspNetCore; +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; + +namespace CSharpEssentials.Tests.AspNetCore; + +public class ResultEndpointFilterTests +{ + [Fact] + public async Task InvokeAsync_WithSuccessResultT_Should_Return_Ok() + { + var filter = new ResultEndpointFilter(); + var context = new DefaultEndpointFilterInvocationContext(new DefaultHttpContext()); + + object result = (await filter.InvokeAsync(context, _ => new ValueTask((Result)42)))!; + + var okResult = (Ok)result; + okResult.Value!.Should().Be(42); + } + + [Fact] + public async Task InvokeAsync_WithFailureResultT_Should_Return_BadRequest() + { + var filter = new ResultEndpointFilter(); + var context = new DefaultEndpointFilterInvocationContext(new DefaultHttpContext()); + + object result = (await filter.InvokeAsync(context, _ => new ValueTask(Result.Failure(Error.NotFound("X", "Missing")))))!; + + var badRequest = (BadRequest)result; + badRequest.Value![0].Type.Should().Be(ErrorType.NotFound); + } + + [Fact] + public async Task InvokeAsync_WithSuccessResult_Should_Return_Ok() + { + var filter = new ResultEndpointFilter(); + var context = new DefaultEndpointFilterInvocationContext(new DefaultHttpContext()); + + object result = (await filter.InvokeAsync(context, _ => new ValueTask(Result.Success())))!; + + result.Should().BeAssignableTo(); + } + + [Fact] + public async Task InvokeAsync_WithFailureResult_Should_Return_BadRequest() + { + var filter = new ResultEndpointFilter(); + var context = new DefaultEndpointFilterInvocationContext(new DefaultHttpContext()); + + object result = (await filter.InvokeAsync(context, _ => new ValueTask(Result.Failure(Error.Validation("V", "Invalid")))))!; + + var badRequest = (BadRequest)result; + badRequest.Value![0].Type.Should().Be(ErrorType.Validation); + } + + [Fact] + public async Task InvokeAsync_WithNull_Should_Return_Null() + { + var filter = new ResultEndpointFilter(); + var context = new DefaultEndpointFilterInvocationContext(new DefaultHttpContext()); + + object? result = await filter.InvokeAsync(context, _ => new ValueTask((object?)null)); + + result.Should().BeNull(); + } + + [Fact] + public async Task InvokeAsync_WithPlainObject_Should_Return_Object() + { + var filter = new ResultEndpointFilter(); + var context = new DefaultEndpointFilterInvocationContext(new DefaultHttpContext()); + + object result = (await filter.InvokeAsync(context, _ => new ValueTask("hello")))!; + + result.Should().Be("hello"); + } +} diff --git a/CSharpEssentials.Tests/AspNetCore/SecuritySchemesTests.cs b/CSharpEssentials.Tests/AspNetCore/SecuritySchemesTests.cs index a639c9d..fe80b31 100644 --- a/CSharpEssentials.Tests/AspNetCore/SecuritySchemesTests.cs +++ b/CSharpEssentials.Tests/AspNetCore/SecuritySchemesTests.cs @@ -9,7 +9,7 @@ public class SecuritySchemesTests [Fact] public void JwtBearerTokenSecurity_ShouldHaveCorrectConfiguration() { - var scheme = SecuritySchemes.JwtBearerTokenSecurity; + OpenApiSecurityScheme scheme = SecuritySchemes.JwtBearerTokenSecurity; scheme.Should().NotBeNull(); scheme.Scheme.Should().Be("bearer"); diff --git a/CSharpEssentials.Tests/AspNetCore/ValidateModelAttributeTests.cs b/CSharpEssentials.Tests/AspNetCore/ValidateModelAttributeTests.cs index d5af10e..e0bcfdf 100644 --- a/CSharpEssentials.Tests/AspNetCore/ValidateModelAttributeTests.cs +++ b/CSharpEssentials.Tests/AspNetCore/ValidateModelAttributeTests.cs @@ -84,7 +84,7 @@ private static ActionExecutingContext CreateActionExecutingContext(bool isValid) return new ActionExecutingContext( actionContext, - new List(), + [], new Dictionary(), controller: null!); } diff --git a/CSharpEssentials.Tests/CSharpEssentials.Tests.csproj b/CSharpEssentials.Tests/CSharpEssentials.Tests.csproj index c083486..ff5400c 100644 --- a/CSharpEssentials.Tests/CSharpEssentials.Tests.csproj +++ b/CSharpEssentials.Tests/CSharpEssentials.Tests.csproj @@ -28,6 +28,10 @@ + diff --git a/CSharpEssentials.Tests/Clone/ICloneableTests.cs b/CSharpEssentials.Tests/Clone/ICloneableTests.cs index 8053b26..ecedcbd 100644 --- a/CSharpEssentials.Tests/Clone/ICloneableTests.cs +++ b/CSharpEssentials.Tests/Clone/ICloneableTests.cs @@ -123,7 +123,7 @@ public void Clone_WithComplexHierarchy_ShouldCloneAllLevels() } }; - var cloned = original.Clone(); + DeepHierarchy cloned = original.Clone(); cloned.Should().NotBeSameAs(original); cloned.Level.Should().Be(1); diff --git a/CSharpEssentials.Tests/Core/CollectionExtensionsTests.cs b/CSharpEssentials.Tests/Core/CollectionExtensionsTests.cs index ea8794c..447f370 100644 --- a/CSharpEssentials.Tests/Core/CollectionExtensionsTests.cs +++ b/CSharpEssentials.Tests/Core/CollectionExtensionsTests.cs @@ -1,6 +1,6 @@ +using CSharpEssentials.Core; using FluentAssertions; using static CSharpEssentials.Tests.TestData; -using CSharpEssentials.Core; namespace CSharpEssentials.Tests.Core; diff --git a/CSharpEssentials.Tests/Core/EnumerableExtensionMembersTests.cs b/CSharpEssentials.Tests/Core/EnumerableExtensionMembersTests.cs new file mode 100644 index 0000000..9359bbb --- /dev/null +++ b/CSharpEssentials.Tests/Core/EnumerableExtensionMembersTests.cs @@ -0,0 +1,22 @@ +#if NET10_0_OR_GREATER +using FluentAssertions; + +namespace CSharpEssentials.Tests.Core; + +public sealed class EnumerableExtensionMembersTests +{ + [Fact] + public void IsEmpty_Should_Be_True_For_Empty_Collection() + { + int[] arr = []; + arr.IsEmpty.Should().BeTrue(); + } + + [Fact] + public void IsEmpty_Should_Be_False_For_NonEmpty_Collection() + { + int[] arr = [1, 2, 3]; + arr.IsEmpty.Should().BeFalse(); + } +} +#endif diff --git a/CSharpEssentials.Tests/Core/ExceptionExtensionsTests.cs b/CSharpEssentials.Tests/Core/ExceptionExtensionsTests.cs index b487b9f..7750554 100644 --- a/CSharpEssentials.Tests/Core/ExceptionExtensionsTests.cs +++ b/CSharpEssentials.Tests/Core/ExceptionExtensionsTests.cs @@ -1,5 +1,5 @@ -using FluentAssertions; using CSharpEssentials.Core; +using FluentAssertions; namespace CSharpEssentials.Tests.Core; diff --git a/CSharpEssentials.Tests/Core/GeneralExtensionsTests.cs b/CSharpEssentials.Tests/Core/GeneralExtensionsTests.cs index f1e450c..8936f61 100644 --- a/CSharpEssentials.Tests/Core/GeneralExtensionsTests.cs +++ b/CSharpEssentials.Tests/Core/GeneralExtensionsTests.cs @@ -1,6 +1,6 @@ +using CSharpEssentials.Core; using FluentAssertions; using static CSharpEssentials.Tests.TestData; -using CSharpEssentials.Core; namespace CSharpEssentials.Tests.Core; @@ -186,7 +186,7 @@ public async Task WithCancellation_WithTask_WhenCancelled_ShouldThrow() [Fact] public async Task WithCancellation_WithTaskT_ShouldWork() { - var task = Task.FromResult(42); + Task task = Task.FromResult(42); var result = await task.WithCancellation(CancellationToken.None); result.Should().Be(42); @@ -197,7 +197,7 @@ public async Task WithCancellation_WithTaskT_WhenCancelled_ShouldThrow() { using CancellationTokenSource cts = new(); cts.CancelAfter(10); - var task = Task.Delay(1000).ContinueWith(_ => 42, TaskScheduler.Default); + Task task = Task.Delay(1000).ContinueWith(_ => 42, TaskScheduler.Default); await Assert.ThrowsAsync(() => task.WithCancellation(cts.Token)); } @@ -205,7 +205,7 @@ public async Task WithCancellation_WithTaskT_WhenCancelled_ShouldThrow() [Fact] public async Task WithCancellation_WithValueTask_ShouldWork() { - var valueTask = ValueTask.FromResult(42); + ValueTask valueTask = ValueTask.FromResult(42); var result = await valueTask.WithCancellation(CancellationToken.None); result.Should().Be(42); diff --git a/CSharpEssentials.Tests/Core/GuiderTests.cs b/CSharpEssentials.Tests/Core/GuiderTests.cs index 5037156..8896e8a 100644 --- a/CSharpEssentials.Tests/Core/GuiderTests.cs +++ b/CSharpEssentials.Tests/Core/GuiderTests.cs @@ -1,6 +1,6 @@ +using CSharpEssentials.Core; using FluentAssertions; using static CSharpEssentials.Tests.TestData; -using CSharpEssentials.Core; namespace CSharpEssentials.Tests.Core; diff --git a/CSharpEssentials.Tests/Core/HttpCodesTests.cs b/CSharpEssentials.Tests/Core/HttpCodesTests.cs index 5cd7997..8e252bc 100644 --- a/CSharpEssentials.Tests/Core/HttpCodesTests.cs +++ b/CSharpEssentials.Tests/Core/HttpCodesTests.cs @@ -1,5 +1,5 @@ -using FluentAssertions; using CSharpEssentials.Core; +using FluentAssertions; namespace CSharpEssentials.Tests.Core; diff --git a/CSharpEssentials.Tests/Core/RandomItemsExtensionsTests.cs b/CSharpEssentials.Tests/Core/RandomItemsExtensionsTests.cs index db0e330..889d279 100644 --- a/CSharpEssentials.Tests/Core/RandomItemsExtensionsTests.cs +++ b/CSharpEssentials.Tests/Core/RandomItemsExtensionsTests.cs @@ -1,6 +1,6 @@ +using CSharpEssentials.Core; using FluentAssertions; using static CSharpEssentials.Tests.TestData; -using CSharpEssentials.Core; namespace CSharpEssentials.Tests.Core; diff --git a/CSharpEssentials.Tests/Core/StringExtensionMembersTests.cs b/CSharpEssentials.Tests/Core/StringExtensionMembersTests.cs new file mode 100644 index 0000000..8c13002 --- /dev/null +++ b/CSharpEssentials.Tests/Core/StringExtensionMembersTests.cs @@ -0,0 +1,36 @@ +#if NET10_0_OR_GREATER +using FluentAssertions; + +namespace CSharpEssentials.Tests.Core; + +public sealed class StringExtensionMembersTests +{ + [Fact] + public void IsPalindrome_Should_Be_True_For_Empty() + { + string s = string.Empty; + s.IsPalindrome.Should().BeTrue(); + } + + [Fact] + public void IsPalindrome_Should_Be_True_For_Single_Char() + { + string s = "a"; + s.IsPalindrome.Should().BeTrue(); + } + + [Fact] + public void IsPalindrome_Should_Be_True_For_Radar() + { + string s = "radar"; + s.IsPalindrome.Should().BeTrue(); + } + + [Fact] + public void IsPalindrome_Should_Be_False_For_Hello() + { + string s = "hello"; + s.IsPalindrome.Should().BeFalse(); + } +} +#endif diff --git a/CSharpEssentials.Tests/Entity/DomainEventTests.cs b/CSharpEssentials.Tests/Entity/DomainEventTests.cs new file mode 100644 index 0000000..b2dce5a --- /dev/null +++ b/CSharpEssentials.Tests/Entity/DomainEventTests.cs @@ -0,0 +1,39 @@ +using CSharpEssentials.Entity.Interfaces; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Entity; + +public sealed class DomainEventTests +{ + private sealed record OrderCreated(Guid OrderId) : IDomainEvent + { + public DateTimeOffset OccurredOn { get; init; } = DateTimeOffset.UtcNow; + } + + [Fact] + public void DomainEvent_Should_Set_OccurredOn_To_UtcNow_By_Default() + { + DateTimeOffset before = DateTimeOffset.UtcNow.AddMilliseconds(-100); + var evt = new OrderCreated(Guid.NewGuid()); + DateTimeOffset after = DateTimeOffset.UtcNow.AddMilliseconds(100); + + evt.OccurredOn.Should().BeOnOrAfter(before); + evt.OccurredOn.Should().BeOnOrBefore(after); + } + + [Fact] + public void DomainEvent_Should_Allow_Custom_OccurredOn() + { + var custom = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero); + OrderCreated evt = new OrderCreated(Guid.NewGuid()) with { OccurredOn = custom }; + + evt.OccurredOn.Should().Be(custom); + } + + [Fact] + public void DomainEvent_Should_Implement_IDomainEvent() + { + var evt = new OrderCreated(Guid.NewGuid()); + evt.Should().BeAssignableTo(); + } +} diff --git a/CSharpEssentials.Tests/Entity/EntityBaseFieldKeywordTests.cs b/CSharpEssentials.Tests/Entity/EntityBaseFieldKeywordTests.cs new file mode 100644 index 0000000..997774e --- /dev/null +++ b/CSharpEssentials.Tests/Entity/EntityBaseFieldKeywordTests.cs @@ -0,0 +1,26 @@ +using CSharpEssentials.Entity; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Entity; + +public sealed class EntityBaseFieldKeywordTests +{ + private sealed class TestEntity : EntityBase; + + [Fact] + public void SetCreatedInfo_With_MinValue_Should_Throw_ArgumentOutOfRangeException() + { + var entity = new TestEntity(); + Action act = () => entity.SetCreatedInfo(DateTimeOffset.MinValue, "user"); + act.Should().Throw().WithParameterName("value"); + } + + [Fact] + public void SetCreatedInfo_With_Valid_Date_Should_Set_CreatedAt() + { + var entity = new TestEntity(); + DateTimeOffset now = DateTimeOffset.UtcNow; + entity.SetCreatedInfo(now, "user"); + entity.CreatedAt.Should().Be(now); + } +} diff --git a/CSharpEssentials.Tests/EntityFrameworkCore/AuditInterceptorTests.cs b/CSharpEssentials.Tests/EntityFrameworkCore/AuditInterceptorTests.cs index 0871c0f..7f9398b 100644 --- a/CSharpEssentials.Tests/EntityFrameworkCore/AuditInterceptorTests.cs +++ b/CSharpEssentials.Tests/EntityFrameworkCore/AuditInterceptorTests.cs @@ -148,7 +148,7 @@ public void AddAuditUserIdProvider_WithServiceProviderFactory_ShouldResolve() [Fact] public void AddAuditUserIdProvider_GenericWithGuid_ShouldResolveAsString() { - Guid userId = Guid.NewGuid(); + var userId = Guid.NewGuid(); ServiceCollection services = new(); services.AddAuditUserIdProvider(() => userId); ServiceProvider provider = services.BuildServiceProvider(); diff --git a/CSharpEssentials.Tests/EntityFrameworkCore/BaseDbContextTests.cs b/CSharpEssentials.Tests/EntityFrameworkCore/BaseDbContextTests.cs index b27086a..d488dd6 100644 --- a/CSharpEssentials.Tests/EntityFrameworkCore/BaseDbContextTests.cs +++ b/CSharpEssentials.Tests/EntityFrameworkCore/BaseDbContextTests.cs @@ -15,7 +15,7 @@ public ConcreteDbContext(DbContextOptions options, IServiceSc private sealed class ListLoggerProvider : ILoggerProvider { - public List Logs { get; } = new(); + public List Logs { get; } = []; public ILogger CreateLogger(string categoryName) => new ListLogger(Logs); public void Dispose() { } private sealed class ListLogger : ILogger @@ -37,9 +37,9 @@ private static (ConcreteDbContext context, List logs) CreateContext() var services = new ServiceCollection(); services.AddSingleton(loggerProvider); services.AddLogging(); - var provider = services.BuildServiceProvider(); - var scopeFactory = provider.GetRequiredService(); - var options = new DbContextOptionsBuilder() + ServiceProvider provider = services.BuildServiceProvider(); + IServiceScopeFactory scopeFactory = provider.GetRequiredService(); + DbContextOptions options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var context = new ConcreteDbContext(options, scopeFactory); @@ -49,7 +49,7 @@ private static (ConcreteDbContext context, List logs) CreateContext() [Fact] public void Constructor_ShouldCreateContextSuccessfully() { - var (context, logs) = CreateContext(); + (ConcreteDbContext? context, List? logs) = CreateContext(); logs.Should().ContainSingle(s => s.Contains("created")); context.Dispose(); } @@ -57,7 +57,7 @@ public void Constructor_ShouldCreateContextSuccessfully() [Fact] public void Dispose_ShouldLogDisposal() { - var (context, logs) = CreateContext(); + (ConcreteDbContext? context, List? logs) = CreateContext(); context.Dispose(); logs.Should().Contain(s => s.Contains("disposed")); } @@ -65,7 +65,7 @@ public void Dispose_ShouldLogDisposal() [Fact] public void Dispose_ShouldNotThrow() { - var (context, _) = CreateContext(); + (ConcreteDbContext? context, List _) = CreateContext(); Action act = () => context.Dispose(); act.Should().NotThrow(); } diff --git a/CSharpEssentials.Tests/EntityFrameworkCore/DbContextExtensionMethodsTests.cs b/CSharpEssentials.Tests/EntityFrameworkCore/DbContextExtensionMethodsTests.cs index 5f23745..b9bac78 100644 --- a/CSharpEssentials.Tests/EntityFrameworkCore/DbContextExtensionMethodsTests.cs +++ b/CSharpEssentials.Tests/EntityFrameworkCore/DbContextExtensionMethodsTests.cs @@ -89,7 +89,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) private sealed class ChangeTrackerDbContext : DbContext { public DbSet SoftMigrates { get; set; } = null!; - public List EntriesBeforeSave { get; } = new(); + public List EntriesBeforeSave { get; } = []; public ChangeTrackerDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -220,7 +220,7 @@ public void Delete_DbSetEntities_ShouldMarkHardDeletedAndRemoveRange() public async Task MigrateDataAsync_Simple_PreConditionTrue_ShouldNotAdd() { using var context = new MigrateDbContext(CreateOptions()); - var seeds = new[] { new SimpleSeed { Key = 1, Name = "A" } }; + SimpleSeed[] seeds = new[] { new SimpleSeed { Key = 1, Name = "A" } }; await context.MigrateDataAsync( seeds, (dbSet, list) => true, @@ -233,7 +233,7 @@ await context.MigrateDataAsync( public async Task MigrateDataAsync_Simple_PreConditionFalse_ShouldAddAll() { using var context = new MigrateDbContext(CreateOptions()); - var seeds = new[] { new SimpleSeed { Key = 1, Name = "A" }, new SimpleSeed { Key = 2, Name = "B" } }; + SimpleSeed[] seeds = new[] { new SimpleSeed { Key = 1, Name = "A" }, new SimpleSeed { Key = 2, Name = "B" } }; await context.MigrateDataAsync( seeds, (dbSet, list) => false, @@ -249,7 +249,7 @@ public async Task MigrateDataAsync_WithPreCondition_PreConditionTrue_ShouldNotMo context.SimpleEntities.Add(new SimpleEntity { Id = 1, Name = "Existing" }); await context.SaveChangesAsync(); - var seeds = new[] { new SimpleSeed { Key = 2, Name = "B" } }; + SimpleSeed[] seeds = new[] { new SimpleSeed { Key = 2, Name = "B" } }; await context.MigrateDataAsync( seeds, new MigrateDataOptions @@ -270,7 +270,7 @@ await context.MigrateDataAsync( public async Task MigrateDataAsync_Core_ShouldAddNewEntities() { using var context = new MigrateDbContext(CreateOptions()); - var seeds = new[] { new SimpleSeed { Key = 1, Name = "A" }, new SimpleSeed { Key = 2, Name = "B" } }; + SimpleSeed[] seeds = new[] { new SimpleSeed { Key = 1, Name = "A" }, new SimpleSeed { Key = 2, Name = "B" } }; await context.MigrateDataAsync( seeds, new MigrateDataOptions @@ -293,7 +293,7 @@ public async Task MigrateDataAsync_Core_ShouldUpdateExistingEntities() context.SimpleEntities.Add(new SimpleEntity { Id = 1, Name = "Old" }); await context.SaveChangesAsync(); - var seeds = new[] { new SimpleSeed { Key = 1, Name = "New" } }; + SimpleSeed[] seeds = new[] { new SimpleSeed { Key = 1, Name = "New" } }; await context.MigrateDataAsync( seeds, new MigrateDataOptions @@ -306,7 +306,7 @@ await context.MigrateDataAsync( Converter = s => new SimpleEntity { Id = s.Key, Name = s.Name } }); - var entity = await context.SimpleEntities.FindAsync(1); + SimpleEntity? entity = await context.SimpleEntities.FindAsync(1); entity!.Name.Should().Be("New"); } @@ -318,7 +318,7 @@ public async Task MigrateDataAsync_Core_ShouldDeleteMissingEntities() context.SimpleEntities.Add(new SimpleEntity { Id = 2, Name = "B" }); await context.SaveChangesAsync(); - var seeds = new[] { new SimpleSeed { Key = 1, Name = "A" } }; + SimpleSeed[] seeds = new[] { new SimpleSeed { Key = 1, Name = "A" } }; await context.MigrateDataAsync( seeds, new MigrateDataOptions @@ -342,7 +342,7 @@ public async Task MigrateDataAsync_Core_HardDeleteMode_ShouldRemoveEntity() context.SoftMigrates.Add(new SoftMigrateEntity { Id = 1, Name = "A" }); await context.SaveChangesAsync(); - var seeds = Array.Empty(); + SoftMigrateSeed[] seeds = Array.Empty(); await context.MigrateDataAsync( seeds, new MigrateDataOptions diff --git a/CSharpEssentials.Tests/EntityFrameworkCore/DomainEventInterceptorTests.cs b/CSharpEssentials.Tests/EntityFrameworkCore/DomainEventInterceptorTests.cs index 77cafcf..448ab69 100644 --- a/CSharpEssentials.Tests/EntityFrameworkCore/DomainEventInterceptorTests.cs +++ b/CSharpEssentials.Tests/EntityFrameworkCore/DomainEventInterceptorTests.cs @@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Moq; namespace CSharpEssentials.Tests.EntityFrameworkCore; @@ -146,7 +145,7 @@ public void SavingChanges_ShouldPreservePerEntityEventOrder() publisher.Published.Should().HaveCount(5); // Entity1's events should maintain their relative order - List names = publisher.Published.Cast().Select(e => e.Name).ToList(); + var names = publisher.Published.Cast().Select(e => e.Name).ToList(); int e1First = names.IndexOf("E1-First"); int e1Second = names.IndexOf("E1-Second"); int e1Third = names.IndexOf("E1-Third"); diff --git a/CSharpEssentials.Tests/EntityFrameworkCore/EntityBaseExtensionsTests.cs b/CSharpEssentials.Tests/EntityFrameworkCore/EntityBaseExtensionsTests.cs index 69ced9a..eb075f3 100644 --- a/CSharpEssentials.Tests/EntityFrameworkCore/EntityBaseExtensionsTests.cs +++ b/CSharpEssentials.Tests/EntityFrameworkCore/EntityBaseExtensionsTests.cs @@ -20,10 +20,7 @@ private static DbContextOptions> CreateOptions() private sealed class TestDbContext : DbContext where T : class { private readonly Action? _configure; - public TestDbContext(DbContextOptions> options, Action? configure = null) : base(options) - { - _configure = configure; - } + public TestDbContext(DbContextOptions> options, Action? configure = null) : base(options) => _configure = configure; protected override void OnModelCreating(ModelBuilder modelBuilder) { _configure?.Invoke(modelBuilder); @@ -88,13 +85,10 @@ private sealed class SoftDeleteMarker { } [Fact] public void EntityBaseGuidIdMap_ShouldConfigureKeyAndProperties() { - var options = CreateOptions(); - using var context = new TestDbContext(options, mb => - { - mb.Entity().EntityBaseGuidIdMap(); - }); + DbContextOptions> options = CreateOptions(); + using var context = new TestDbContext(options, mb => mb.Entity().EntityBaseGuidIdMap()); - var entityType = context.Model.FindEntityType(typeof(GuidIdEntity)); + IEntityType? entityType = context.Model.FindEntityType(typeof(GuidIdEntity)); entityType.Should().NotBeNull(); entityType!.FindPrimaryKey()!.Properties.Should().ContainSingle(p => p.Name == nameof(GuidIdEntity.Id)); GetProperty(entityType, nameof(GuidIdEntity.Id)).IsNullable.Should().BeFalse(); @@ -109,13 +103,10 @@ public void EntityBaseGuidIdMap_ShouldConfigureKeyAndProperties() [Fact] public void SoftDeletableEntityBaseGuidIdMap_ShouldConfigureSoftDeleteProperties() { - var options = CreateOptions(); - using var context = new TestDbContext(options, mb => - { - mb.Entity().SoftDeletableEntityBaseGuidIdMap(); - }); + DbContextOptions> options = CreateOptions(); + using var context = new TestDbContext(options, mb => mb.Entity().SoftDeletableEntityBaseGuidIdMap()); - var entityType = context.Model.FindEntityType(typeof(SoftGuidIdEntity))!; + IEntityType entityType = context.Model.FindEntityType(typeof(SoftGuidIdEntity))!; entityType.FindPrimaryKey()!.Properties.Should().ContainSingle(p => p.Name == nameof(SoftGuidIdEntity.Id)); GetProperty(entityType, nameof(SoftGuidIdEntity.DeletedAt)).IsNullable.Should().BeTrue(); GetProperty(entityType, nameof(SoftGuidIdEntity.DeletedBy)).IsNullable.Should().BeTrue(); @@ -127,14 +118,14 @@ public void SoftDeletableEntityBaseGuidIdMap_ShouldConfigureSoftDeleteProperties [Fact] public void EntityBaseMap_ShouldConfigureAuditProperties() { - var options = CreateOptions(); + DbContextOptions> options = CreateOptions(); using var context = new TestDbContext(options, mb => { mb.Entity().HasKey(x => x.Id); mb.Entity().EntityBaseMap(); }); - var entityType = context.Model.FindEntityType(typeof(AuditEntity))!; + IEntityType entityType = context.Model.FindEntityType(typeof(AuditEntity))!; GetProperty(entityType, nameof(AuditEntity.CreatedAt)).IsNullable.Should().BeFalse(); GetProperty(entityType, nameof(AuditEntity.CreatedBy)).IsNullable.Should().BeFalse(); GetProperty(entityType, nameof(AuditEntity.CreatedBy)).GetMaxLength().Should().Be(40); @@ -147,14 +138,14 @@ public void EntityBaseMap_ShouldConfigureAuditProperties() [Fact] public void SoftDeletableEntityBaseMap_ShouldConfigureSoftDeleteAndAuditProperties() { - var options = CreateOptions(); + DbContextOptions> options = CreateOptions(); using var context = new TestDbContext(options, mb => { mb.Entity().HasKey(x => x.Id); mb.Entity().SoftDeletableEntityBaseMap(); }); - var entityType = context.Model.FindEntityType(typeof(SoftAuditEntity))!; + IEntityType entityType = context.Model.FindEntityType(typeof(SoftAuditEntity))!; GetProperty(entityType, nameof(SoftAuditEntity.DeletedAt)).IsNullable.Should().BeTrue(); GetProperty(entityType, nameof(SoftAuditEntity.DeletedBy)).IsNullable.Should().BeTrue(); GetProperty(entityType, nameof(SoftAuditEntity.DeletedBy)).GetMaxLength().Should().Be(40); @@ -167,13 +158,10 @@ public void SoftDeletableEntityBaseMap_ShouldConfigureSoftDeleteAndAuditProperti [Fact] public void EntityBaseMap_Generic_ShouldConfigureKeyAndAuditProperties() { - var options = CreateOptions(); - using var context = new TestDbContext(options, mb => - { - mb.Entity().EntityBaseMap(); - }); + DbContextOptions> options = CreateOptions(); + using var context = new TestDbContext(options, mb => mb.Entity().EntityBaseMap()); - var entityType = context.Model.FindEntityType(typeof(IntEntity))!; + IEntityType entityType = context.Model.FindEntityType(typeof(IntEntity))!; entityType.FindPrimaryKey()!.Properties.Should().ContainSingle(p => p.Name == nameof(IntEntity.Id)); GetProperty(entityType, nameof(IntEntity.Id)).IsNullable.Should().BeFalse(); GetProperty(entityType, nameof(IntEntity.CreatedAt)).IsNullable.Should().BeFalse(); @@ -182,13 +170,10 @@ public void EntityBaseMap_Generic_ShouldConfigureKeyAndAuditProperties() [Fact] public void SoftDeletableEntityBaseMap_Generic_ShouldConfigureKeyAndSoftDeleteProperties() { - var options = CreateOptions(); - using var context = new TestDbContext(options, mb => - { - mb.Entity().SoftDeletableEntityBaseMap(); - }); + DbContextOptions> options = CreateOptions(); + using var context = new TestDbContext(options, mb => mb.Entity().SoftDeletableEntityBaseMap()); - var entityType = context.Model.FindEntityType(typeof(SoftIntEntity))!; + IEntityType entityType = context.Model.FindEntityType(typeof(SoftIntEntity))!; entityType.FindPrimaryKey()!.Properties.Should().ContainSingle(p => p.Name == nameof(SoftIntEntity.Id)); GetProperty(entityType, nameof(SoftIntEntity.Id)).IsNullable.Should().BeFalse(); GetProperty(entityType, nameof(SoftIntEntity.IsDeleted)).IsNullable.Should().BeFalse(); @@ -198,15 +183,15 @@ public void SoftDeletableEntityBaseMap_Generic_ShouldConfigureKeyAndSoftDeletePr [Fact] public void OptimisticConcurrencyVersionMap_ShouldConfigureRowVersion() { - var options = CreateOptions(); + DbContextOptions> options = CreateOptions(); using var context = new TestDbContext(options, mb => { mb.Entity().HasKey(x => x.Id); mb.Entity().OptimisticConcurrencyVersionMap(); }); - var entityType = context.Model.FindEntityType(typeof(VersionedEntity))!; - var property = entityType.FindProperty("RowVersion")!; + IEntityType entityType = context.Model.FindEntityType(typeof(VersionedEntity))!; + IProperty property = entityType.FindProperty("RowVersion")!; property.IsConcurrencyToken.Should().BeTrue(); property.ValueGenerated.Should().Be(ValueGenerated.OnAddOrUpdate); property.ClrType.Should().Be(); @@ -216,15 +201,15 @@ public void OptimisticConcurrencyVersionMap_ShouldConfigureRowVersion() [Fact] public void AddQueryFilter_ShouldApplyFilter() { - var options = CreateOptions(); + DbContextOptions> options = CreateOptions(); using var context = new TestDbContext(options, mb => { mb.Entity().HasKey(x => x.Id); mb.Entity().AddQueryFilter(e => e.IsActive); }); - var entityType = context.Model.FindEntityType(typeof(FilteredEntity))!; - var filter = entityType.GetQueryFilter(); + IEntityType entityType = context.Model.FindEntityType(typeof(FilteredEntity))!; + LambdaExpression? filter = entityType.GetQueryFilter(); filter.Should().NotBeNull(); var compiled = (Expression>)filter!; compiled.Compile()(new FilteredEntity { Id = 1, IsActive = true }).Should().BeTrue(); @@ -234,7 +219,7 @@ public void AddQueryFilter_ShouldApplyFilter() [Fact] public void AddQueryFilter_ShouldCombineFilters() { - var options = CreateOptions(); + DbContextOptions> options = CreateOptions(); using var context = new TestDbContext(options, mb => { mb.Entity().HasKey(x => x.Id); @@ -242,8 +227,8 @@ public void AddQueryFilter_ShouldCombineFilters() mb.Entity().AddQueryFilter(e => e.Name != null); }); - var entityType = context.Model.FindEntityType(typeof(FilteredEntity))!; - var filter = entityType.GetQueryFilter(); + IEntityType entityType = context.Model.FindEntityType(typeof(FilteredEntity))!; + LambdaExpression? filter = entityType.GetQueryFilter(); filter.Should().NotBeNull(); var compiled = (Expression>)filter!; compiled.Compile()(new FilteredEntity { Id = 1, IsActive = true, Name = "A" }).Should().BeTrue(); @@ -254,7 +239,7 @@ public void AddQueryFilter_ShouldCombineFilters() [Fact] public void ApplySoftDeleteQueryFilter_ShouldApplyToSoftDeletableEntitiesOnly() { - var options = CreateOptions(); + DbContextOptions> options = CreateOptions(); using var context = new TestDbContext(options, mb => { mb.Entity().HasKey(x => x.Id); @@ -262,8 +247,8 @@ public void ApplySoftDeleteQueryFilter_ShouldApplyToSoftDeletableEntitiesOnly() mb.ApplySoftDeleteQueryFilter(); }); - var softType = context.Model.FindEntityType(typeof(DirectSoftEntity))!; - var nonSoftType = context.Model.FindEntityType(typeof(NonSoftEntity))!; + IEntityType softType = context.Model.FindEntityType(typeof(DirectSoftEntity))!; + IEntityType nonSoftType = context.Model.FindEntityType(typeof(NonSoftEntity))!; softType.GetQueryFilter().Should().NotBeNull(); nonSoftType.GetQueryFilter().Should().BeNull(); _ = new DirectSoftEntity { Id = Guid.NewGuid() }; diff --git a/CSharpEssentials.Tests/EntityFrameworkCore/ModelConfigurationExtensionsTests.cs b/CSharpEssentials.Tests/EntityFrameworkCore/ModelConfigurationExtensionsTests.cs index 536a0e8..bd66e62 100644 --- a/CSharpEssentials.Tests/EntityFrameworkCore/ModelConfigurationExtensionsTests.cs +++ b/CSharpEssentials.Tests/EntityFrameworkCore/ModelConfigurationExtensionsTests.cs @@ -1,7 +1,8 @@ -using CSharpEssentials.Enums; using CSharpEssentials.EntityFrameworkCore; +using CSharpEssentials.Enums; using FluentAssertions; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; namespace CSharpEssentials.Tests.EntityFrameworkCore; @@ -38,13 +39,13 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura [Fact] public void ConfigureEnumConventions_ShouldApplyConverterAndMaxLength() { - var options = new DbContextOptionsBuilder() + DbContextOptions options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; using var context = new EnumConventionDbContext(options); - var entityType = context.Model.FindEntityType(typeof(EnumEntity))!; - var property = entityType.FindProperty(nameof(EnumEntity.Status))!; + IEntityType entityType = context.Model.FindEntityType(typeof(EnumEntity))!; + IProperty property = entityType.FindProperty(nameof(EnumEntity.Status))!; property.GetMaxLength().Should().Be(8); // "inactive".Length property.GetValueConverter().Should().NotBeNull(); diff --git a/CSharpEssentials.Tests/EntityFrameworkCore/PaginationExtensionsTests.cs b/CSharpEssentials.Tests/EntityFrameworkCore/PaginationExtensionsTests.cs index f6d2c25..f4cd136 100644 --- a/CSharpEssentials.Tests/EntityFrameworkCore/PaginationExtensionsTests.cs +++ b/CSharpEssentials.Tests/EntityFrameworkCore/PaginationExtensionsTests.cs @@ -1,5 +1,6 @@ using CSharpEssentials.EntityFrameworkCore.Pagination; using CSharpEssentials.EntityFrameworkCore.Pagination.Requests; +using CSharpEssentials.EntityFrameworkCore.Pagination.Responses; using FluentAssertions; using Microsoft.EntityFrameworkCore; @@ -54,7 +55,7 @@ public async Task PaginateAsync_ShouldReturnCorrectPage() await SeedAsync(context); var request = new PaginationRequest { PageNumber = 2, PageSize = 3 }; - var result = await context.PaginatedEntities.PaginateAsync(request, null, includeTotalCount: true); + PaginationResponse result = await context.PaginatedEntities.PaginateAsync(request, null, includeTotalCount: true); result.Items.Should().HaveCount(3); result.PageNumber.Should().Be(2); @@ -69,7 +70,7 @@ public async Task PaginateAsync_ShouldApplySearch() await SeedAsync(context); var request = new PaginationRequest { PageNumber = 1, PageSize = 10, Search = "Item05" }; - var result = await context.PaginatedEntities.PaginateAsync( + PaginationResponse result = await context.PaginatedEntities.PaginateAsync( request, term => e => e.Name.Contains(term), includeTotalCount: true); @@ -86,7 +87,7 @@ public async Task PaginateAsync_ShouldNormalizeRequest() await SeedAsync(context); var request = new PaginationRequest { PageNumber = 0, PageSize = -1, Search = " Item01 " }; - var result = await context.PaginatedEntities.PaginateAsync( + PaginationResponse result = await context.PaginatedEntities.PaginateAsync( request, term => e => e.Name.Contains(term), includeTotalCount: true); @@ -103,7 +104,7 @@ public async Task PaginateAsync_WithoutTotalCount_ShouldReturnNegativeOne() await SeedAsync(context); var request = new PaginationRequest { PageNumber = 1, PageSize = 5 }; - var result = await context.PaginatedEntities.PaginateAsync(request, null, includeTotalCount: false); + PaginationResponse result = await context.PaginatedEntities.PaginateAsync(request, null, includeTotalCount: false); result.TotalCount.Should().Be(-1); } @@ -119,7 +120,7 @@ public async Task PaginateAsync_Cursor_Ascending_ShouldReturnNextPage() await SeedAsync(context); var request = new CursorPaginationRequest { Cursor = 3, Limit = 3 }; - var result = await context.PaginatedEntities + CursorPaginationResponse result = await context.PaginatedEntities .PaginateAsync(request, e => e.Id, isAscending: true); result.Items.Should().HaveCount(3); @@ -135,7 +136,7 @@ public async Task PaginateAsync_Cursor_Descending_ShouldReturnPreviousPage() await SeedAsync(context); var request = new CursorPaginationRequest { Cursor = 7, Limit = 3 }; - var result = await context.PaginatedEntities + CursorPaginationResponse result = await context.PaginatedEntities .PaginateAsync(request, e => e.Id, isAscending: false); result.Items.Should().HaveCount(3); @@ -151,12 +152,12 @@ public async Task PaginateAsync_Cursor_NoMoreItems_ShouldReturnHasMoreFalse() await SeedAsync(context); var request = new CursorPaginationRequest { Cursor = 8, Limit = 5 }; - var result = await context.PaginatedEntities + CursorPaginationResponse result = await context.PaginatedEntities .PaginateAsync(request, e => e.Id, isAscending: true); result.Items.Should().HaveCount(2); result.HasMore.Should().BeFalse(); - result.Next.Should().Be(default(int)); + result.Next.Should().Be(default); } [Fact] @@ -166,7 +167,7 @@ public async Task PaginateAsync_Cursor_WithSearch_ShouldFilter() await SeedAsync(context); var request = new CursorPaginationRequest { Cursor = 0, Limit = 10, Search = "Item09" }; - var result = await context.PaginatedEntities + CursorPaginationResponse result = await context.PaginatedEntities .PaginateAsync( request, e => e.Id, @@ -194,7 +195,7 @@ public async Task PaginateAsync_Cursor_WithThenBy_ShouldApplySecondarySort() await context.SaveChangesAsync(); var request = new CursorPaginationRequest { Cursor = 0, Limit = 3 }; - var result = await context.PaginatedEntities + CursorPaginationResponse result = await context.PaginatedEntities .PaginateAsync( request, e => e.Id, diff --git a/CSharpEssentials.Tests/EntityFrameworkCore/PropertyBuilderExtensionsTests.cs b/CSharpEssentials.Tests/EntityFrameworkCore/PropertyBuilderExtensionsTests.cs index 6c968c5..f0bd6ef 100644 --- a/CSharpEssentials.Tests/EntityFrameworkCore/PropertyBuilderExtensionsTests.cs +++ b/CSharpEssentials.Tests/EntityFrameworkCore/PropertyBuilderExtensionsTests.cs @@ -2,6 +2,7 @@ using CSharpEssentials.Maybe; using FluentAssertions; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; namespace CSharpEssentials.Tests.EntityFrameworkCore; @@ -50,7 +51,7 @@ public void MaybeConversion_ShouldConvertValue() context.SaveChanges(); context.ChangeTracker.Clear(); - var loaded = context.JsonEntities.Find(entity.Id); + JsonEntity? loaded = context.JsonEntities.Find(entity.Id); loaded!.MaybeName.HasValue.Should().BeTrue(); loaded.MaybeName.Value.Should().Be("hello"); } @@ -66,7 +67,7 @@ public void HasJsonConversion_ShouldConvertObjectToJson() context.SaveChanges(); context.ChangeTracker.Clear(); - var loaded = context.JsonEntities.Find(entity.Id); + JsonEntity? loaded = context.JsonEntities.Find(entity.Id); loaded!.JsonData.Should().NotBeNull(); loaded.JsonData.Value.Should().Be("test"); } @@ -74,13 +75,13 @@ public void HasJsonConversion_ShouldConvertObjectToJson() [Fact] public void HasJsonConversion_ShouldSetColumnTypeAnnotation() { - var options = new DbContextOptionsBuilder() + DbContextOptions options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; using var context = new PropertyDbContext(options); - var entityType = context.Model.FindEntityType(typeof(JsonEntity))!; - var property = entityType.FindProperty(nameof(JsonEntity.JsonData))!; + IEntityType entityType = context.Model.FindEntityType(typeof(JsonEntity))!; + IProperty property = entityType.FindProperty(nameof(JsonEntity.JsonData))!; property.FindAnnotation("Relational:ColumnType")?.Value.Should().Be("jsonb"); } } diff --git a/CSharpEssentials.Tests/Enums/StringEnumGeneratorTests.cs b/CSharpEssentials.Tests/Enums/StringEnumGeneratorTests.cs new file mode 100644 index 0000000..a02ae3d --- /dev/null +++ b/CSharpEssentials.Tests/Enums/StringEnumGeneratorTests.cs @@ -0,0 +1,203 @@ +using CSharpEssentials.Enums; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Enums; + +public class StringEnumGeneratorTests +{ + [Fact] + public void ToOptimizedString_Should_Return_Name() + { + Color c = Color.Red; + c.ToOptimizedString().Should().Be("Red"); + } + + [Fact] + public void ToOptimizedString_Should_Return_Name_For_Last_Member() + { + Color c = Color.Blue; + c.ToOptimizedString().Should().Be("Blue"); + } + + [Fact] + public void ToOptimizedString_Should_Return_Name_For_CamelCase_Member() + { + Status s = Status.InProgress; + s.ToOptimizedString().Should().Be("InProgress"); + } + + [Fact] + public void ToSnakeCase_Should_Return_SnakeCase() + { + Status s = Status.InProgress; + s.ToSnakeCase().Should().Be("in_progress"); + } + + [Fact] + public void ToSnakeCase_Should_Return_SnakeCase_For_Simple_Name() + { + Color c = Color.Red; + c.ToSnakeCase().Should().Be("red"); + } + + [Fact] + public void ToKebabCase_Should_Return_KebabCase() + { + Status s = Status.InProgress; + s.ToKebabCase().Should().Be("in-progress"); + } + + [Fact] + public void ToLowerCase_Should_Return_LowerCase() + { + Color c = Color.Red; + c.ToLowerCase().Should().Be("red"); + } + + [Fact] + public void ToUpperCase_Should_Return_UpperCase() + { + Color c = Color.Red; + c.ToUpperCase().Should().Be("RED"); + } + + [Fact] + public void IsDefined_Should_Return_True_For_Known() + { + ColorExtensions.IsDefined("Green").Should().BeTrue(); + } + + [Fact] + public void IsDefined_Should_Return_False_For_Unknown() + { + ColorExtensions.IsDefined("Purple").Should().BeFalse(); + } + + [Fact] + public void TryParse_Should_Return_True_And_Value_For_Valid_Name() + { + bool result = ColorExtensions.TryParse("Green", out Color value); + result.Should().BeTrue(); + value.Should().Be(Color.Green); + } + + [Fact] + public void TryParse_Should_Return_False_For_Invalid_Name() + { + bool result = ColorExtensions.TryParse("Purple", out Color value); + result.Should().BeFalse(); + value.Should().Be(Color.Red); // default(int) cast to enum = first value (0) + } + + [Fact] + public void TryParse_Should_Parse_Numeric_Value() + { + bool result = ColorExtensions.TryParse("2", out Color value); + result.Should().BeTrue(); + value.Should().Be(Color.Blue); + } + + [Fact] + public void Parse_Should_Return_Value_For_Valid_Name() + { + Color value = ColorExtensions.Parse("Green"); + value.Should().Be(Color.Green); + } + + [Fact] + public void Parse_Should_Throw_For_Invalid_Name() + { + Action act = () => ColorExtensions.Parse("Purple"); + act.Should().Throw(); + } + + [Fact] + public void GetNames_Should_Return_All_Names() + { + string[] names = ColorExtensions.GetNames(); + names.Should().ContainInOrder("Red", "Green", "Blue"); + } + + [Fact] + public void GetValues_Should_Return_All_Values() + { + Color[] values = ColorExtensions.GetValues(); + values.Should().ContainInOrder(Color.Red, Color.Green, Color.Blue); + } + + [Fact] + public void AsUnderlyingType_Should_Return_Numeric_Value() + { + Color c = Color.Blue; + c.AsUnderlyingType().Should().Be(2); + } + + [Fact] + public void Constants_Should_Have_Correct_SnakeCase() + { + StatusExtensions.InProgressSnakeCase.Should().Be("in_progress"); + StatusExtensions.NotStartedSnakeCase.Should().Be("not_started"); + } + + [Fact] + public void Constants_Should_Have_Correct_KebabCase() + { + StatusExtensions.InProgressKebabCase.Should().Be("in-progress"); + StatusExtensions.NotStartedKebabCase.Should().Be("not-started"); + } + + [Fact] + public void ToSnakeCase_Should_Handle_Consecutive_Uppercase() + { + HttpStatus h = HttpStatus.HTTPResponse; + h.ToSnakeCase().Should().Be("httpresponse"); + } + + [Fact] + public void ToKebabCase_Should_Handle_Consecutive_Uppercase() + { + HttpStatus h = HttpStatus.HTTPResponse; + h.ToKebabCase().Should().Be("httpresponse"); + } + + [Fact] + public void Fallback_Should_Handle_Unknown_Value() + { + var unknown = (HttpStatus)999; + unknown.ToSnakeCase().Should().Be("999"); + unknown.ToKebabCase().Should().Be("999"); + } + + [Fact] + public void Constants_Should_Handle_Consecutive_Uppercase() + { + HttpStatusExtensions.OKSnakeCase.Should().Be("ok"); + HttpStatusExtensions.NotFoundSnakeCase.Should().Be("not_found"); + HttpStatusExtensions.HTTPResponseSnakeCase.Should().Be("httpresponse"); + HttpStatusExtensions.HTTPResponseKebabCase.Should().Be("httpresponse"); + } +} + +[StringEnum] +internal enum Color +{ + Red, + Green, + Blue +} + +[StringEnum] +internal enum Status +{ + NotStarted, + InProgress, + Completed +} + +[StringEnum] +internal enum HttpStatus +{ + OK, + NotFound, + HTTPResponse +} diff --git a/CSharpEssentials.Tests/Errors/DomainExceptionTests.cs b/CSharpEssentials.Tests/Errors/DomainExceptionTests.cs index fa01da6..0efecd6 100644 --- a/CSharpEssentials.Tests/Errors/DomainExceptionTests.cs +++ b/CSharpEssentials.Tests/Errors/DomainExceptionTests.cs @@ -9,7 +9,7 @@ public class DomainExceptionTests [Fact] public void Constructor_WithError_ShouldSetProperties() { - Error error = Error.Failure("Domain.Invalid", "Domain logic failed"); + var error = Error.Failure("Domain.Invalid", "Domain logic failed"); var exception = new DomainException(error); @@ -20,7 +20,7 @@ public void Constructor_WithError_ShouldSetProperties() [Fact] public void Constructor_WithError_ShouldBeAssignableToException() { - Error error = Error.Validation("Domain.Validation", "Invalid state"); + var error = Error.Validation("Domain.Validation", "Invalid state"); var exception = new DomainException(error); diff --git a/CSharpEssentials.Tests/Errors/ErrorExtensionsTests.cs b/CSharpEssentials.Tests/Errors/ErrorExtensionsTests.cs index 3af0503..e18a872 100644 --- a/CSharpEssentials.Tests/Errors/ErrorExtensionsTests.cs +++ b/CSharpEssentials.Tests/Errors/ErrorExtensionsTests.cs @@ -1,6 +1,6 @@ +using CSharpEssentials.Core; using CSharpEssentials.Errors; using FluentAssertions; -using CSharpEssentials.Core; namespace CSharpEssentials.Tests.Errors; diff --git a/CSharpEssentials.Tests/Errors/ErrorFieldKeywordTests.cs b/CSharpEssentials.Tests/Errors/ErrorFieldKeywordTests.cs new file mode 100644 index 0000000..46c5e7b --- /dev/null +++ b/CSharpEssentials.Tests/Errors/ErrorFieldKeywordTests.cs @@ -0,0 +1,29 @@ +using CSharpEssentials.Errors; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Errors; + +public sealed class ErrorFieldKeywordTests +{ + [Fact] + public void Factory_Code_Null_Should_Throw_ArgumentNullException() + { + Action act = () => Error.Failure(null!, "desc"); + act.Should().Throw().WithParameterName("code"); + } + + [Fact] + public void Factory_Description_Null_Should_Throw_ArgumentNullException() + { + Action act = () => Error.Failure("code", null!); + act.Should().Throw().WithParameterName("description"); + } + + [Fact] + public void Factory_Valid_Should_Create_Error() + { + var error = Error.Failure("E1", "Something failed"); + error.Code.Should().Be("E1"); + error.Description.Should().Be("Something failed"); + } +} diff --git a/CSharpEssentials.Tests/Errors/ErrorMetadataTests.cs b/CSharpEssentials.Tests/Errors/ErrorMetadataTests.cs index 8dadd37..16b6c8b 100644 --- a/CSharpEssentials.Tests/Errors/ErrorMetadataTests.cs +++ b/CSharpEssentials.Tests/Errors/ErrorMetadataTests.cs @@ -98,9 +98,10 @@ public void Combine_WithOtherMetadata_ShouldMerge() [Fact] public void Add_ShouldAddKeyValue() { - ErrorMetadata metadata = new(); - - metadata.Add("key", "value"); + ErrorMetadata metadata = new() + { + { "key", "value" } + }; metadata.Should().ContainKey("key"); } @@ -116,7 +117,7 @@ public void ContainsKey_WithExistingKey_ShouldReturnTrue() [Fact] public void ContainsKey_WithNonExistingKey_ShouldReturnFalse() { - ErrorMetadata metadata = new(); + ErrorMetadata metadata = []; metadata.ContainsKey("key").Should().BeFalse(); } diff --git a/CSharpEssentials.Tests/Errors/ErrorOperatorTests.cs b/CSharpEssentials.Tests/Errors/ErrorOperatorTests.cs index 4a7f7a6..50e0b37 100644 --- a/CSharpEssentials.Tests/Errors/ErrorOperatorTests.cs +++ b/CSharpEssentials.Tests/Errors/ErrorOperatorTests.cs @@ -34,8 +34,8 @@ public void ImplicitConversion_ShouldWorkInMethodParameter() [Fact] public void PlusOperator_TwoErrors_ShouldReturnArrayWithBoth() { - Error error1 = Error.Validation("ERR1", "First"); - Error error2 = Error.Validation("ERR2", "Second"); + var error1 = Error.Validation("ERR1", "First"); + var error2 = Error.Validation("ERR2", "Second"); Error[] result = error1 + error2; @@ -57,8 +57,8 @@ public void PlusOperator_SameError_ShouldReturnArrayWithDuplicates() [Fact] public void PlusOperator_DifferentTypes_ShouldReturnArrayWithBoth() { - Error validation = Error.Validation("VAL", "Validation"); - Error notFound = Error.NotFound("NF", "Not found"); + var validation = Error.Validation("VAL", "Validation"); + var notFound = Error.NotFound("NF", "Not found"); Error[] result = validation + notFound; diff --git a/CSharpEssentials.Tests/Errors/ErrorTests.cs b/CSharpEssentials.Tests/Errors/ErrorTests.cs index d020ac1..98f7beb 100644 --- a/CSharpEssentials.Tests/Errors/ErrorTests.cs +++ b/CSharpEssentials.Tests/Errors/ErrorTests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Errors; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Errors; diff --git a/CSharpEssentials.Tests/GcpSecretManager/DefaultSecretManagerConfigurationLoaderTests.cs b/CSharpEssentials.Tests/GcpSecretManager/DefaultSecretManagerConfigurationLoaderTests.cs index 8cd1390..f0a6c55 100644 --- a/CSharpEssentials.Tests/GcpSecretManager/DefaultSecretManagerConfigurationLoaderTests.cs +++ b/CSharpEssentials.Tests/GcpSecretManager/DefaultSecretManagerConfigurationLoaderTests.cs @@ -17,7 +17,7 @@ private static Secret CreateSecret(string projectId, string secretId) [Fact] public void GetKey_FromSecret_ShouldReturnSecretId() { - var secret = CreateSecret("project", "my-secret"); + Secret secret = CreateSecret("project", "my-secret"); string key = _loader.GetKey(secret); @@ -51,7 +51,7 @@ public void GetKey_FromString_WithMultipleDoubleUnderscores_ShouldReplaceAll() [Fact] public void ShouldLoadSecret_WithNoFilters_ShouldReturnTrue() { - var secret = CreateSecret("project", "any-secret"); + Secret secret = CreateSecret("project", "any-secret"); var config = new ProjectSecretConfiguration { ProjectId = "project" }; bool result = _loader.ShouldLoadSecret(secret, config); @@ -62,7 +62,7 @@ public void ShouldLoadSecret_WithNoFilters_ShouldReturnTrue() [Fact] public void ShouldLoadSecret_WithMatchingPrefix_ShouldReturnTrue() { - var secret = CreateSecret("project", "APP_SETTING_1"); + Secret secret = CreateSecret("project", "APP_SETTING_1"); var config = new ProjectSecretConfiguration { ProjectId = "project", @@ -77,7 +77,7 @@ public void ShouldLoadSecret_WithMatchingPrefix_ShouldReturnTrue() [Fact] public void ShouldLoadSecret_WithNonMatchingPrefix_ShouldReturnFalse() { - var secret = CreateSecret("project", "DB_SETTING_1"); + Secret secret = CreateSecret("project", "DB_SETTING_1"); var config = new ProjectSecretConfiguration { ProjectId = "project", @@ -92,7 +92,7 @@ public void ShouldLoadSecret_WithNonMatchingPrefix_ShouldReturnFalse() [Fact] public void ShouldLoadSecret_WithMatchingSecretId_ShouldReturnTrue() { - var secret = CreateSecret("project", "specific-secret"); + Secret secret = CreateSecret("project", "specific-secret"); var config = new ProjectSecretConfiguration { ProjectId = "project", @@ -107,7 +107,7 @@ public void ShouldLoadSecret_WithMatchingSecretId_ShouldReturnTrue() [Fact] public void ShouldLoadSecret_WithNonMatchingSecretId_ShouldReturnFalse() { - var secret = CreateSecret("project", "other-secret"); + Secret secret = CreateSecret("project", "other-secret"); var config = new ProjectSecretConfiguration { ProjectId = "project", @@ -122,9 +122,9 @@ public void ShouldLoadSecret_WithNonMatchingSecretId_ShouldReturnFalse() [Fact] public void ShouldLoadSecret_WithPrefixAndSecretId_ShouldReturnTrueForEitherMatch() { - var secret1 = CreateSecret("project", "APP_SETTING"); - var secret2 = CreateSecret("project", "specific-secret"); - var secret3 = CreateSecret("project", "other-secret"); + Secret secret1 = CreateSecret("project", "APP_SETTING"); + Secret secret2 = CreateSecret("project", "specific-secret"); + Secret secret3 = CreateSecret("project", "other-secret"); var config = new ProjectSecretConfiguration { ProjectId = "project", @@ -151,7 +151,7 @@ public void ShouldLoadSecret_WithNullSecret_ShouldThrowArgumentNullException() [Fact] public void ShouldLoadSecret_WithNullConfig_ShouldThrowArgumentNullException() { - var secret = CreateSecret("project", "secret"); + Secret secret = CreateSecret("project", "secret"); Action action = () => _loader.ShouldLoadSecret(secret, null!); diff --git a/CSharpEssentials.Tests/GcpSecretManager/ExtensionsTests.cs b/CSharpEssentials.Tests/GcpSecretManager/ExtensionsTests.cs index 6079726..91e7990 100644 --- a/CSharpEssentials.Tests/GcpSecretManager/ExtensionsTests.cs +++ b/CSharpEssentials.Tests/GcpSecretManager/ExtensionsTests.cs @@ -12,10 +12,7 @@ public void AddGcpSecretManager_WithManualProjects_ShouldAddSource() { using var configuration = new ConfigurationManager(); - configuration.AddGcpSecretManager(options => - { - options.AddProject(new() { ProjectId = "test-project" }); - }); + configuration.AddGcpSecretManager(options => options.AddProject(new() { ProjectId = "test-project" })); configuration.Sources.Should().Contain(x => x is SecretManagerConfigurationSource); } @@ -43,10 +40,7 @@ public void AddGcpSecretManager_WithLoadFromAppSettings_ShouldLoadFromAppSetting ["GoogleSecretManager:Projects:0:ProjectId"] = "test-project" }); - configuration.AddGcpSecretManager(options => - { - options.LoadFromAppSettings = true; - }); + configuration.AddGcpSecretManager(options => options.LoadFromAppSettings = true); configuration.Sources.Should().Contain(x => x is SecretManagerConfigurationSource); } @@ -78,10 +72,7 @@ public void AddGcpSecretManager_ShouldReturnSameConfigurationManager() { using var configuration = new ConfigurationManager(); - var result = configuration.AddGcpSecretManager(options => - { - options.AddProject(new() { ProjectId = "test-project" }); - }); + IConfigurationManager result = configuration.AddGcpSecretManager(options => options.AddProject(new() { ProjectId = "test-project" })); result.Should().BeSameAs(configuration); } diff --git a/CSharpEssentials.Tests/GcpSecretManager/ProjectSecretLoadContextTests.cs b/CSharpEssentials.Tests/GcpSecretManager/ProjectSecretLoadContextTests.cs index 5e7656a..843d970 100644 --- a/CSharpEssentials.Tests/GcpSecretManager/ProjectSecretLoadContextTests.cs +++ b/CSharpEssentials.Tests/GcpSecretManager/ProjectSecretLoadContextTests.cs @@ -59,7 +59,7 @@ public void ProjectSecretLoadContext_Deconstruct_ShouldReturnValues() var config = new ProjectSecretConfiguration { ProjectId = "test-project" }; var context = new ProjectSecretLoadContext(mockClient.Object, projectName, config); - var (client, proj, cfg) = context; + (SecretManagerServiceClient? client, ProjectName? proj, ProjectSecretConfiguration? cfg) = context; client.Should().Be(mockClient.Object); proj.Should().Be(projectName); diff --git a/CSharpEssentials.Tests/GcpSecretManager/SecretLoadResultTests.cs b/CSharpEssentials.Tests/GcpSecretManager/SecretLoadResultTests.cs index cf63f32..a55ebe1 100644 --- a/CSharpEssentials.Tests/GcpSecretManager/SecretLoadResultTests.cs +++ b/CSharpEssentials.Tests/GcpSecretManager/SecretLoadResultTests.cs @@ -47,7 +47,7 @@ public void SecretLoadResult_Deconstruct_ShouldReturnValues() { var result = new SecretLoadResult("path", "value", "key"); - var (path, value, key) = result; + (string? path, string? value, string? key) = result; path.Should().Be("path"); value.Should().Be("value"); diff --git a/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationOptionsTests.cs b/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationOptionsTests.cs index a1ea169..e147e4a 100644 --- a/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationOptionsTests.cs +++ b/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationOptionsTests.cs @@ -123,7 +123,7 @@ public void LoadFromConfiguration_WhenLoadFromAppSettingsIsFalse_ShouldNotLoad() { LoadFromAppSettings = false }; - var configuration = new ConfigurationBuilder().Build(); + IConfigurationRoot configuration = new ConfigurationBuilder().Build(); options.LoadFromConfiguration(configuration); @@ -137,7 +137,7 @@ public void LoadFromConfiguration_WhenSectionMissing_ShouldThrowInvalidOperation { LoadFromAppSettings = true }; - var configuration = new ConfigurationBuilder().Build(); + IConfigurationRoot configuration = new ConfigurationBuilder().Build(); Action action = () => options.LoadFromConfiguration(configuration); @@ -152,7 +152,7 @@ public void LoadFromConfiguration_WhenProjectIdEmpty_ShouldThrowInvalidOperation { LoadFromAppSettings = true }; - var configuration = new ConfigurationBuilder() + IConfigurationRoot configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["GoogleSecretManager:Projects:0:ProjectId"] = "", @@ -173,7 +173,7 @@ public void LoadFromConfiguration_WhenProjectIdMissing_ShouldThrowInvalidOperati { LoadFromAppSettings = true }; - var configuration = new ConfigurationBuilder() + IConfigurationRoot configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["GoogleSecretManager:Projects:0:Region"] = "us-central1" @@ -193,7 +193,7 @@ public void LoadFromConfiguration_WithValidSection_ShouldLoadProjects() { LoadFromAppSettings = true }; - var configuration = new ConfigurationBuilder() + IConfigurationRoot configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["GoogleSecretManager:Projects:0:ProjectId"] = "project1", @@ -218,7 +218,7 @@ public void LoadFromConfiguration_WithCustomSectionName_ShouldUseCustomName() LoadFromAppSettings = true, ConfigurationSectionName = "CustomSecrets" }; - var configuration = new ConfigurationBuilder() + IConfigurationRoot configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["CustomSecrets:Projects:0:ProjectId"] = "project1" diff --git a/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationProviderTests.cs b/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationProviderTests.cs index b6d0ceb..d934ae9 100644 --- a/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationProviderTests.cs +++ b/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationProviderTests.cs @@ -1,4 +1,3 @@ -using System.Collections; using System.Text.Json; using CSharpEssentials.GcpSecretManager; using CSharpEssentials.GcpSecretManager.Configuration; @@ -9,7 +8,6 @@ using Google.Api.Gax.ResourceNames; using Google.Cloud.SecretManager.V1; using Google.Protobuf; -using Grpc.Core; using Moq; namespace CSharpEssentials.Tests.GcpSecretManager; @@ -70,7 +68,7 @@ private static SecretManagerConfigurationProvider CreateProvider( public void Load_WithNoProjects_ShouldReturnEmptyData() { var mockClient = new Mock(); - var provider = CreateProvider(mockClient, []); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, []); provider.Load(); @@ -88,13 +86,13 @@ public void Load_WithSingleRawSecret_ShouldLoadRawValue() { ["projects/project/secrets/raw-secret/versions/latest"] = "plain-text-value" }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project", RawSecretIds = new[] { "raw-secret" } }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); @@ -114,9 +112,9 @@ public void Load_WithJsonSecret_ShouldFlattenJson() { ["projects/project/secrets/app-settings/versions/latest"] = json }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project" }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); @@ -137,9 +135,9 @@ public void Load_WithInvalidJson_ShouldKeepRawValue() { ["projects/project/secrets/bad-json/versions/latest"] = "not-json-at-all" }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project" }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); @@ -159,13 +157,13 @@ public void Load_WithRawPrefix_ShouldNotFlattenJson() { ["projects/project/secrets/RAW_config/versions/latest"] = json }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project", RawSecretPrefixes = new[] { "RAW_" } }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); @@ -189,13 +187,13 @@ public void Load_WithPrefixFilter_ShouldOnlyLoadMatchingSecrets() ["projects/project/secrets/DB_setting1/versions/latest"] = "db1", ["projects/project/secrets/APP_setting2/versions/latest"] = "app2" }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project", PrefixFilters = new[] { "APP_" } }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); @@ -219,13 +217,13 @@ public void Load_WithSecretIdFilter_ShouldOnlyLoadMatchingSecrets() ["projects/project/secrets/secret2/versions/latest"] = "val2", ["projects/project/secrets/secret3/versions/latest"] = "val3" }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project", SecretIds = new[] { "secret1", "secret3" } }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); @@ -248,8 +246,8 @@ public void Load_WithMultipleProjects_ShouldLoadAll() ["projects/project2/secrets/secret2/versions/latest"] = "value2" }; - var mockClient1 = CreateMockClient(secrets1, values1); - var mockClient2 = CreateMockClient(secrets2, values2); + Mock mockClient1 = CreateMockClient(secrets1, values1); + Mock mockClient2 = CreateMockClient(secrets2, values2); var loader = new DefaultSecretManagerConfigurationLoader(); var options = new SecretManagerConfigurationOptions(); @@ -278,9 +276,9 @@ public void Load_WithArrayJson_ShouldFlattenWithIndices() { ["projects/project/secrets/array-secret/versions/latest"] = json }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project" }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); @@ -304,9 +302,9 @@ public void Load_WithNullJsonValue_ShouldStoreNull() { ["projects/project/secrets/null-secret/versions/latest"] = json }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project" }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); @@ -326,9 +324,9 @@ public void Load_WithNestedObject_ShouldFlattenRecursively() { ["projects/project/secrets/nested-secret/versions/latest"] = json }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project" }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); @@ -348,9 +346,9 @@ public void Load_ShouldAlsoStoreOriginalSecretKey() { ["projects/project/secrets/my-secret/versions/latest"] = json }; - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); var projectConfig = new ProjectSecretConfiguration { ProjectId = "project" }; - var provider = CreateProvider(mockClient, [projectConfig]); + SecretManagerConfigurationProvider provider = CreateProvider(mockClient, [projectConfig]); provider.Load(); diff --git a/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationSourceTests.cs b/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationSourceTests.cs index c37cb9b..08d72f6 100644 --- a/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationSourceTests.cs +++ b/CSharpEssentials.Tests/GcpSecretManager/SecretManagerConfigurationSourceTests.cs @@ -1,14 +1,11 @@ -using System.Text.Json; using CSharpEssentials.GcpSecretManager; using CSharpEssentials.GcpSecretManager.Configuration; using CSharpEssentials.GcpSecretManager.Infrastructure; using FluentAssertions; using Google.Api.Gax; using Google.Api.Gax.Grpc; -using Google.Api.Gax.ResourceNames; using Google.Cloud.SecretManager.V1; using Google.Protobuf; -using Grpc.Core; using Microsoft.Extensions.Configuration; using Moq; @@ -37,7 +34,7 @@ public void Build_WithNoProjects_ShouldReturnProviderWithEmptyData() mockHelper.Setup(x => x.Create()).Returns(mockClient.Object); var source = new SecretManagerConfigurationSource(options, mockHelper.Object); - var provider = source.Build(new ConfigurationBuilder()); + IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); provider.Should().NotBeNull(); provider.Load(); @@ -60,7 +57,7 @@ public void Build_WithNoLoader_ShouldUseDefaultLoader() mockHelper.Setup(x => x.Create()).Returns(mockClient.Object); var source = new SecretManagerConfigurationSource(options, mockHelper.Object); - var provider = source.Build(new ConfigurationBuilder()); + IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); provider.Should().NotBeNull(); } @@ -85,7 +82,7 @@ public void Build_WithCustomLoader_ShouldUseCustomLoader() mockHelper.Setup(x => x.Create()).Returns(mockClient.Object); var source = new SecretManagerConfigurationSource(options, mockHelper.Object); - var provider = source.Build(new ConfigurationBuilder()); + IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); provider.Should().NotBeNull(); } @@ -105,7 +102,7 @@ public void Build_WithRegionAndNoCredentials_ShouldCreateWithRegion() mockHelper.Setup(x => x.CreateWithRegion("us-central1")).Returns(mockClient.Object); var source = new SecretManagerConfigurationSource(options, mockHelper.Object); - var provider = source.Build(new ConfigurationBuilder()); + IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); mockHelper.Verify(x => x.CreateWithRegion("us-central1"), Times.Once); provider.Should().NotBeNull(); @@ -125,7 +122,7 @@ public void Build_WithCredentialsAndNoRegion_ShouldCreateWithCredentials() mockHelper.Setup(x => x.Create("/path/to/creds.json")).Returns(mockClient.Object); var source = new SecretManagerConfigurationSource(options, mockHelper.Object); - var provider = source.Build(new ConfigurationBuilder()); + IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); mockHelper.Verify(x => x.Create("/path/to/creds.json"), Times.Once); provider.Should().NotBeNull(); @@ -149,7 +146,7 @@ public void Build_WithRegionAndCredentials_ShouldCreateWithRegionAndCredentials( mockHelper.Setup(x => x.CreateWithRegion("/path/to/creds.json", "europe-west1")).Returns(mockClient.Object); var source = new SecretManagerConfigurationSource(options, mockHelper.Object); - var provider = source.Build(new ConfigurationBuilder()); + IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); mockHelper.Verify(x => x.CreateWithRegion("/path/to/creds.json", "europe-west1"), Times.Once); provider.Should().NotBeNull(); @@ -166,7 +163,7 @@ public void Build_WithNoRegionAndNoCredentials_ShouldCreateDefaultClient() mockHelper.Setup(x => x.Create()).Returns(mockClient.Object); var source = new SecretManagerConfigurationSource(options, mockHelper.Object); - var provider = source.Build(new ConfigurationBuilder()); + IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); mockHelper.Verify(x => x.Create(), Times.Once); provider.Should().NotBeNull(); @@ -185,7 +182,7 @@ public void Build_WithMultipleProjects_ShouldCreateClientForEach() mockHelper.Setup(x => x.CreateWithRegion("us-central1")).Returns(mockClient.Object); var source = new SecretManagerConfigurationSource(options, mockHelper.Object); - var provider = source.Build(new ConfigurationBuilder()); + IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); mockHelper.Verify(x => x.Create(), Times.Once); mockHelper.Verify(x => x.CreateWithRegion("us-central1"), Times.Once); @@ -205,11 +202,11 @@ public void Build_ReturnedProvider_ShouldLoadSecrets() }; var mockHelper = new Mock(); - var mockClient = CreateMockClient(secrets, values); + Mock mockClient = CreateMockClient(secrets, values); mockHelper.Setup(x => x.Create()).Returns(mockClient.Object); var source = new SecretManagerConfigurationSource(options, mockHelper.Object); - var provider = source.Build(new ConfigurationBuilder()); + IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); provider.Load(); provider.TryGet("secret1", out string? loadedValue).Should().BeTrue(); diff --git a/CSharpEssentials.Tests/Http/HttpClientResilienceExtensionsTests.cs b/CSharpEssentials.Tests/Http/HttpClientResilienceExtensionsTests.cs new file mode 100644 index 0000000..6c191d2 --- /dev/null +++ b/CSharpEssentials.Tests/Http/HttpClientResilienceExtensionsTests.cs @@ -0,0 +1,219 @@ +using CSharpEssentials.Errors; +using CSharpEssentials.Http; +using CSharpEssentials.ResultPattern; +using FluentAssertions; +using Polly; +using Polly.CircuitBreaker; + +namespace CSharpEssentials.Tests.Http; + +public class HttpClientResilienceExtensionsTests +{ + [Fact] + public async Task ExecuteAsResultAsync_Should_Return_Success_When_No_Failure() + { + ResiliencePipeline pipeline = HttpClientResilienceExtensions.CreateRetryPipeline(maxRetryAttempts: 1); + + Result result = await pipeline.ExecuteAsResultAsync(_ => Task.FromResult(Result.Success())); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task ExecuteAsResultAsync_Generic_Should_Return_Value() + { + ResiliencePipeline pipeline = HttpClientResilienceExtensions.CreateRetryPipeline(maxRetryAttempts: 1); + + Result result = await pipeline.ExecuteAsResultAsync(_ => Task.FromResult(Result.Success(42))); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be(42); + } + + [Fact] + public async Task ExecuteAsResultAsync_Should_Retry_On_HttpRequestException() + { + int attempts = 0; + ResiliencePipeline pipeline = HttpClientResilienceExtensions.CreateRetryPipeline(maxRetryAttempts: 2, delay: TimeSpan.FromMilliseconds(10)); + + Result result = await pipeline.ExecuteAsResultAsync(_ => + { + attempts++; + if (attempts < 2) + throw new HttpRequestException("Transient failure"); + return Task.FromResult(Result.Success()); + }); + + result.IsSuccess.Should().BeTrue(); + attempts.Should().Be(2); + } + + [Fact] + public async Task ExecuteAsResultAsync_Should_Return_Failure_On_Persistent_Exception() + { + ResiliencePipeline pipeline = HttpClientResilienceExtensions.CreateRetryPipeline(maxRetryAttempts: 1, delay: TimeSpan.FromMilliseconds(10)); + + Result result = await pipeline.ExecuteAsResultAsync(_ => throw new HttpRequestException("Persistent failure")); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Type.Should().Be(ErrorType.Unexpected); + } + + [Fact] + public async Task CreateTimeoutPipeline_Should_Throw_On_Timeout() + { + ResiliencePipeline pipeline = HttpClientResilienceExtensions.CreateTimeoutPipeline(TimeSpan.FromSeconds(1)); + + Func act = async () => await pipeline.ExecuteAsync(async token => await Task.Delay(TimeSpan.FromSeconds(5), token)); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task ExecuteAsResultAsync_With_GenericPipeline_Should_Return_Value() + { + ResiliencePipeline> pipeline = HttpClientResilienceExtensions.CreateRetryPipeline(maxRetryAttempts: 2, delay: TimeSpan.FromMilliseconds(10)); + + int attempts = 0; + Result result = await pipeline.ExecuteAsResultAsync(_ => + { + attempts++; + if (attempts < 2) + return Task.FromResult(Result.Failure(Error.Unexpected())); + return Task.FromResult(Result.Success(99)); + }); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be(99); + attempts.Should().Be(2); + } + + [Fact] + public async Task CreateRetryPipeline_Generic_Should_Not_Retry_On_Success() + { + ResiliencePipeline> pipeline = HttpClientResilienceExtensions.CreateRetryPipeline(maxRetryAttempts: 2, delay: TimeSpan.FromMilliseconds(10)); + + int attempts = 0; + Result result = await pipeline.ExecuteAsResultAsync(_ => + { + attempts++; + return Task.FromResult(Result.Success(42)); + }); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be(42); + attempts.Should().Be(1); + } + + [Fact] + public async Task CreateResiliencePipeline_Generic_Should_Retry_And_Timeout() + { + ResiliencePipeline> pipeline = HttpClientResilienceExtensions.CreateResiliencePipeline( + maxRetryAttempts: 1, + timeout: TimeSpan.FromSeconds(1), + retryDelay: TimeSpan.FromMilliseconds(10)); + + Result result = await pipeline.ExecuteAsResultAsync(_ => Task.FromResult(Result.Success(7))); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be(7); + } + + [Theory] + [InlineData(ErrorType.Unauthorized)] + [InlineData(ErrorType.Forbidden)] + [InlineData(ErrorType.NotFound)] + [InlineData(ErrorType.Validation)] + public async Task CreateRetryPipeline_Generic_Should_Not_Retry_NonRetryable_Errors(ErrorType errorType) + { + ResiliencePipeline> pipeline = HttpClientResilienceExtensions.CreateRetryPipeline( + maxRetryAttempts: 2, + delay: TimeSpan.FromMilliseconds(10)); + + int attempts = 0; + Result result = await pipeline.ExecuteAsResultAsync(_ => + { + attempts++; + return Task.FromResult(Result.Failure(CreateError(errorType))); + }); + + result.IsFailure.Should().BeTrue(); + attempts.Should().Be(1); + } + + [Fact] + public async Task CreateRetryPipeline_Generic_Should_Retry_On_Conflict() + { + ResiliencePipeline> pipeline = HttpClientResilienceExtensions.CreateRetryPipeline( + maxRetryAttempts: 2, + delay: TimeSpan.FromMilliseconds(10)); + + int attempts = 0; + Result result = await pipeline.ExecuteAsResultAsync(_ => + { + attempts++; + if (attempts < 2) + return Task.FromResult(Result.Failure(Error.Conflict())); + return Task.FromResult(Result.Success(42)); + }); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be(42); + attempts.Should().Be(2); + } + + [Fact] + public void CreateCircuitBreakerPipeline_Should_Have_Sensible_Defaults() + { + ResiliencePipeline pipeline = HttpClientResilienceExtensions.CreateCircuitBreakerPipeline(minimumThroughput: 5); + + pipeline.Should().NotBeNull(); + } + + [Fact] + public async Task CreateCircuitBreakerPipeline_NonGeneric_Should_Open_On_Exceptions() + { + ResiliencePipeline pipeline = HttpClientResilienceExtensions.CreateCircuitBreakerPipeline( + minimumThroughput: 3, + samplingDuration: TimeSpan.FromSeconds(1), + breakDuration: TimeSpan.FromSeconds(1)); + + for (int i = 0; i < 3; i++) + { + Result result = await pipeline.ExecuteAsResultAsync(_ => throw new HttpRequestException("failure")); + result.IsFailure.Should().BeTrue(); + } + + Func act = async () => await pipeline.ExecuteAsResultAsync(_ => Task.FromResult(Result.Success())); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task CreateCircuitBreakerPipeline_Generic_Should_Open_On_Failures() + { + ResiliencePipeline> pipeline = HttpClientResilienceExtensions.CreateCircuitBreakerPipeline( + minimumThroughput: 3, + samplingDuration: TimeSpan.FromSeconds(1), + breakDuration: TimeSpan.FromSeconds(1)); + + for (int i = 0; i < 3; i++) + { + Result result = await pipeline.ExecuteAsResultAsync(_ => Task.FromResult(Result.Failure(Error.Unexpected()))); + result.IsFailure.Should().BeTrue(); + } + + Func act = async () => await pipeline.ExecuteAsResultAsync(_ => Task.FromResult(Result.Success(1))); + await act.Should().ThrowAsync(); + } + + private static Error CreateError(ErrorType type) => type switch + { + ErrorType.Unauthorized => Error.Unauthorized(), + ErrorType.Forbidden => Error.Forbidden(), + ErrorType.NotFound => Error.NotFound(), + ErrorType.Validation => Error.Validation(), + _ => Error.Failure() + }; + + +} diff --git a/CSharpEssentials.Tests/Http/HttpClientResultExtensionsTests.cs b/CSharpEssentials.Tests/Http/HttpClientResultExtensionsTests.cs new file mode 100644 index 0000000..ed9f4f3 --- /dev/null +++ b/CSharpEssentials.Tests/Http/HttpClientResultExtensionsTests.cs @@ -0,0 +1,207 @@ +using System.Net; +using CSharpEssentials.Errors; +using CSharpEssentials.Http; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Http; + +public class HttpClientResultExtensionsTests +{ + [Fact] + public async Task GetFromJsonAsResultAsync_WithSuccess_Should_Return_Value() + { + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("""{"id":1,"name":"Test"}""") + }; + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await client.GetFromJsonAsResultAsync(new Uri("https://test.com")); + + result.IsSuccess.Should().BeTrue(); + result.Value.Id.Should().Be(1); + result.Value.Name.Should().Be("Test"); + } + + [Fact] + public async Task GetFromJsonAsResultAsync_WithNotFound_Should_Return_Failure() + { + using var response = new HttpResponseMessage(HttpStatusCode.NotFound); + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await client.GetFromJsonAsResultAsync(new Uri("https://test.com")); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Type.Should().Be(ErrorType.NotFound); + } + + [Fact] + public async Task GetFromJsonAsResultAsync_WithEmptyBody_Should_Return_NotFoundError() + { + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("null") + }; + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await client.GetFromJsonAsResultAsync(new Uri("https://test.com")); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Type.Should().Be(ErrorType.NotFound); + } + + [Fact] + public async Task PostAsJsonAsResultAsync_WithSuccess_Should_Return_Value() + { + using var response = new HttpResponseMessage(HttpStatusCode.Created) + { + Content = new StringContent("""{"id":2,"name":"Created"}""") + }; + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await client.PostAsJsonAsResultAsync(new Uri("https://test.com"), new { Name = "Created" }); + + result.IsSuccess.Should().BeTrue(); + result.Value.Name.Should().Be("Created"); + } + + [Fact] + public async Task SendAsResultAsync_WithSuccess_Should_Return_Success() + { + using var response = new HttpResponseMessage(HttpStatusCode.NoContent); + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + using var request = new HttpRequestMessage(HttpMethod.Delete, new Uri("https://test.com")); + + Result result = await client.SendAsResultAsync(request); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task SendAsResultAsync_WithFailure_Should_Return_Failure() + { + using var response = new HttpResponseMessage(HttpStatusCode.BadRequest); + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + using var request = new HttpRequestMessage(HttpMethod.Post, new Uri("https://test.com")); + + Result result = await client.SendAsResultAsync(request); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Type.Should().Be(ErrorType.Validation); + } + + [Fact] + public async Task PutAsJsonAsResultAsync_WithSuccess_Should_Return_Value() + { + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("""{"id":3,"name":"Updated"}""") + }; + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await client.PutAsJsonAsResultAsync(new Uri("https://test.com"), new { Name = "Updated" }); + + result.IsSuccess.Should().BeTrue(); + result.Value.Name.Should().Be("Updated"); + } + + [Fact] + public async Task PatchAsJsonAsResultAsync_WithSuccess_Should_Return_Value() + { + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("""{"id":4,"name":"Patched"}""") + }; + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await client.PatchAsJsonAsResultAsync(new Uri("https://test.com"), new { Name = "Patched" }); + + result.IsSuccess.Should().BeTrue(); + result.Value.Name.Should().Be("Patched"); + } + + [Fact] + public async Task DeleteAsResultAsync_WithSuccess_Should_Return_Success() + { + using var response = new HttpResponseMessage(HttpStatusCode.NoContent); + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await client.DeleteAsResultAsync(new Uri("https://test.com")); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task DeleteAsResultAsync_WithFailure_Should_Return_Failure() + { + using var response = new HttpResponseMessage(HttpStatusCode.NotFound); + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await client.DeleteAsResultAsync(new Uri("https://test.com")); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Type.Should().Be(ErrorType.NotFound); + } + + [Fact] + public async Task PostAsResultAsync_WithSuccess_Should_Return_Success() + { + using var response = new HttpResponseMessage(HttpStatusCode.OK); + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + using var content = new StringContent("test"); + Result result = await client.PostAsResultAsync(new Uri("https://test.com"), content); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task PutAsResultAsync_WithSuccess_Should_Return_Success() + { + using var response = new HttpResponseMessage(HttpStatusCode.NoContent); + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + using var content = new StringContent("test"); + Result result = await client.PutAsResultAsync(new Uri("https://test.com"), content); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task SendAsResultAsync_Generic_WithSuccess_Should_Return_Value() + { + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("""{"id":5,"name":"Sent"}""") + }; + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://test.com")); + + Result result = await client.SendAsResultAsync(request); + + result.IsSuccess.Should().BeTrue(); + result.Value.Name.Should().Be("Sent"); + } + + private sealed record TestDto(int Id, string Name); + + private sealed class MockHandler(HttpResponseMessage response) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => Task.FromResult(response); + } +} diff --git a/CSharpEssentials.Tests/Http/HttpContentExtensionsTests.cs b/CSharpEssentials.Tests/Http/HttpContentExtensionsTests.cs new file mode 100644 index 0000000..976dfe4 --- /dev/null +++ b/CSharpEssentials.Tests/Http/HttpContentExtensionsTests.cs @@ -0,0 +1,56 @@ +using CSharpEssentials.Errors; +using CSharpEssentials.Http; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Http; + +public class HttpContentExtensionsTests +{ + [Fact] + public async Task ReadAsStringAsResultAsync_Should_Return_Content() + { + using var content = new StringContent("Hello World"); + + Result result = await content.ReadAsStringAsResultAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be("Hello World"); + } + + [Fact] + public async Task ReadFromJsonAsResultAsync_WithValidJson_Should_Return_Value() + { + using var content = new StringContent("""{"id":1,"name":"Test"}"""); + + Result result = await content.ReadFromJsonAsResultAsync(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Id.Should().Be(1); + result.Value.Name.Should().Be("Test"); + } + + [Fact] + public async Task ReadFromJsonAsResultAsync_WithNull_Should_Return_NotFoundError() + { + using var content = new StringContent("null"); + + Result result = await content.ReadFromJsonAsResultAsync(); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Type.Should().Be(ErrorType.NotFound); + } + + [Fact] + public async Task ReadFromJsonAsResultAsync_WithInvalidJson_Should_Return_ValidationError() + { + using var content = new StringContent("invalid json"); + + Result result = await content.ReadFromJsonAsResultAsync(); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Type.Should().Be(ErrorType.Validation); + } + + private sealed record TestDto(int Id, string Name); +} diff --git a/CSharpEssentials.Tests/Http/HttpRequestBuilderTests.cs b/CSharpEssentials.Tests/Http/HttpRequestBuilderTests.cs new file mode 100644 index 0000000..4c8024f --- /dev/null +++ b/CSharpEssentials.Tests/Http/HttpRequestBuilderTests.cs @@ -0,0 +1,120 @@ +using System.Net; +using CSharpEssentials.Errors; +using CSharpEssentials.Http; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Http; + +public class HttpRequestBuilderTests +{ + [Fact] + public void Build_Should_Create_Request_With_Method_And_Uri() + { + Result result = HttpRequestBuilder.Get("https://test.com/api").Build(); + + result.IsSuccess.Should().BeTrue(); + using HttpRequestMessage request = result.Value; + request.Method.Should().Be(HttpMethod.Get); + request.RequestUri!.ToString().Should().Be("https://test.com/api"); + } + + [Fact] + public void Build_WithQuery_Should_Append_QueryString() + { + Result result = HttpRequestBuilder.Get("https://test.com/api") + .WithQuery("page", "1") + .Build(); + + result.IsSuccess.Should().BeTrue(); + using HttpRequestMessage request = result.Value; + request.RequestUri!.Query.Should().Contain("page=1"); + } + + [Fact] + public void Build_WithHeader_Should_Add_Header() + { + Result result = HttpRequestBuilder.Get("https://test.com/api") + .WithHeader("x-api-key", "secret") + .Build(); + + result.IsSuccess.Should().BeTrue(); + using HttpRequestMessage request = result.Value; + request.Headers.Contains("x-api-key").Should().BeTrue(); + } + + [Fact] + public void Build_WithJsonContent_Should_Set_Content() + { + Result result = HttpRequestBuilder.Post("https://test.com/api") + .WithJsonContent(new { Name = "Alice" }) + .Build(); + + result.IsSuccess.Should().BeTrue(); + using HttpRequestMessage request = result.Value; + request.Content.Should().NotBeNull(); + } + + [Fact] + public async Task AsResultAsync_Should_Execute_And_Return_Success() + { + using var response = new HttpResponseMessage(HttpStatusCode.NoContent); + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await HttpRequestBuilder.Delete("https://test.com/api/1") + .AsResultAsync(client); + + result.IsSuccess.Should().BeTrue(); + } + + [Fact] + public async Task AsResultAsync_Generic_Should_Execute_And_Return_Value() + { + using var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("""{"id":1,"name":"Test"}""") + }; + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await HttpRequestBuilder + .Get("https://test.com/api/1") + .AsResultAsync(client); + + result.IsSuccess.Should().BeTrue(); + result.Value.Name.Should().Be("Test"); + } + + [Fact] + public async Task AsResultAsync_WithFailure_Should_Return_Error() + { + using var response = new HttpResponseMessage(HttpStatusCode.BadRequest); + using var handler = new MockHandler(response); + using var client = new HttpClient(handler); + + Result result = await HttpRequestBuilder.Post("https://test.com/api") + .WithJsonContent(new { }) + .AsResultAsync(client); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Type.Should().Be(ErrorType.Validation); + } + + [Fact] + public async Task AsResultAsync_WithNullClient_Should_Return_ValidationError() + { + Result result = await HttpRequestBuilder.Get("https://test.com/api") + .AsResultAsync(null!); + + result.IsFailure.Should().BeTrue(); + } + + private sealed record TestDto(int Id, string Name); + + private sealed class MockHandler(HttpResponseMessage response) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => Task.FromResult(response); + } +} diff --git a/CSharpEssentials.Tests/Http/HttpStatusCodeMapperTests.cs b/CSharpEssentials.Tests/Http/HttpStatusCodeMapperTests.cs new file mode 100644 index 0000000..0ff9eb3 --- /dev/null +++ b/CSharpEssentials.Tests/Http/HttpStatusCodeMapperTests.cs @@ -0,0 +1,58 @@ +using System.Net; +using CSharpEssentials.Errors; +using CSharpEssentials.Http; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Http; + +public class HttpStatusCodeMapperTests +{ + [Theory] + [InlineData(HttpStatusCode.BadRequest, ErrorType.Validation)] + [InlineData(HttpStatusCode.Unauthorized, ErrorType.Unauthorized)] + [InlineData(HttpStatusCode.PaymentRequired, ErrorType.Failure)] + [InlineData(HttpStatusCode.Forbidden, ErrorType.Forbidden)] + [InlineData(HttpStatusCode.NotFound, ErrorType.NotFound)] + [InlineData(HttpStatusCode.MethodNotAllowed, ErrorType.Failure)] + [InlineData(HttpStatusCode.NotAcceptable, ErrorType.Failure)] + [InlineData(HttpStatusCode.ProxyAuthenticationRequired, ErrorType.Unauthorized)] + [InlineData(HttpStatusCode.RequestTimeout, ErrorType.Unexpected)] + [InlineData(HttpStatusCode.Conflict, ErrorType.Conflict)] + [InlineData(HttpStatusCode.Gone, ErrorType.NotFound)] + [InlineData(HttpStatusCode.LengthRequired, ErrorType.Validation)] + [InlineData(HttpStatusCode.PreconditionFailed, ErrorType.Conflict)] + [InlineData(HttpStatusCode.RequestEntityTooLarge, ErrorType.Validation)] + [InlineData(HttpStatusCode.RequestUriTooLong, ErrorType.Validation)] + [InlineData(HttpStatusCode.UnsupportedMediaType, ErrorType.Validation)] + [InlineData(HttpStatusCode.UnprocessableEntity, ErrorType.Validation)] + [InlineData(HttpStatusCode.TooManyRequests, ErrorType.Conflict)] + [InlineData(HttpStatusCode.InternalServerError, ErrorType.Unexpected)] + [InlineData(HttpStatusCode.OK, ErrorType.Failure)] + public void ToErrorType_Should_Map_Correctly(HttpStatusCode code, ErrorType expected) + { + HttpStatusCodeMapper.ToErrorType(code).Should().Be(expected); + } + + [Fact] + public void ToError_Should_Create_Structured_Error() + { + var error = HttpStatusCodeMapper.ToError(HttpStatusCode.NotFound); + + error.Type.Should().Be(ErrorType.NotFound); + error.Code.Should().Be("Http.404"); + } + + [Fact] + public void ToError_WithDescription_Should_Use_Custom_Description() + { + var error = HttpStatusCodeMapper.ToError(HttpStatusCode.BadRequest, "Custom bad request"); + + error.Description.Should().Be("Custom bad request"); + } + + [Fact] + public void ToErrorType_UnknownClientError_Should_Map_To_Failure() + { + HttpStatusCodeMapper.ToErrorType((HttpStatusCode)418).Should().Be(ErrorType.Failure); + } +} diff --git a/CSharpEssentials.Tests/Http/QueryStringExtensionsTests.cs b/CSharpEssentials.Tests/Http/QueryStringExtensionsTests.cs new file mode 100644 index 0000000..183ba68 --- /dev/null +++ b/CSharpEssentials.Tests/Http/QueryStringExtensionsTests.cs @@ -0,0 +1,114 @@ +using CSharpEssentials.Http; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Http; + +public class QueryStringExtensionsTests +{ + [Fact] + public void ToQueryString_FromDictionary_Should_Return_Encoded_String() + { + var parameters = new Dictionary + { + { "name", "Alice" }, + { "age", "30" }, + { "nullKey", null } + }; + + Result result = parameters.ToQueryString(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Contain("name=Alice"); + result.Value.Should().Contain("age=30"); + result.Value.Should().NotContain("nullKey"); + } + + [Fact] + public void ToQueryString_FromObject_Should_Return_Encoded_String() + { + var obj = new { Name = "Bob", Age = 25 }; + + Result result = obj.ToQueryString(); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Contain("Name=Bob"); + result.Value.Should().Contain("Age=25"); + } + + [Fact] + public void WithQueryString_Dictionary_Should_Append_To_Uri() + { + var uri = new Uri("https://test.com/api"); + var parameters = new Dictionary { { "page", "1" } }; + + Result result = uri.WithQueryString(parameters); + + result.IsSuccess.Should().BeTrue(); + result.Value.Query.Should().Contain("page=1"); + } + + [Fact] + public void WithQueryString_Object_Should_Append_To_Uri() + { + var uri = new Uri("https://test.com/api"); + + Result result = uri.WithQueryString(new { limit = "10" }); + + result.IsSuccess.Should().BeTrue(); + result.Value.Query.Should().Contain("limit=10"); + } + + [Fact] + public void WithQueryString_Single_Should_Append_To_Uri() + { + var uri = new Uri("https://test.com/api"); + + Result result = uri.WithQueryString("sort", "desc"); + + result.IsSuccess.Should().BeTrue(); + result.Value.Query.Should().Contain("sort=desc"); + } + + [Fact] + public void WithQueryString_Should_Merge_Existing_Query() + { + var uri = new Uri("https://test.com/api?existing=true"); + + Result result = uri.WithQueryString("new", "value"); + + result.IsSuccess.Should().BeTrue(); + result.Value.Query.Should().Contain("existing=true"); + result.Value.Query.Should().Contain("new=value"); + } + + [Fact] + public void WithQueryString_NullUri_Should_Return_Failure() + { + Uri? uri = null; + + Result result = uri!.WithQueryString("key", "value"); + + result.IsFailure.Should().BeTrue(); + } + + [Fact] + public void WithQueryString_EmptyName_Should_Return_Failure() + { + var uri = new Uri("https://test.com/api"); + + Result result = uri.WithQueryString("", "value"); + + result.IsFailure.Should().BeTrue(); + } + + [Fact] + public void ToQueryString_EmptyKey_Should_Return_Failure() + { + var parameters = new Dictionary { { "", "value" } }; + + Result result = parameters.ToQueryString(); + + result.IsFailure.Should().BeTrue(); + } +} diff --git a/CSharpEssentials.Tests/Json/ConditionalStringEnumConverterTests.cs b/CSharpEssentials.Tests/Json/ConditionalStringEnumConverterTests.cs index a0bcdfe..2713e6c 100644 --- a/CSharpEssentials.Tests/Json/ConditionalStringEnumConverterTests.cs +++ b/CSharpEssentials.Tests/Json/ConditionalStringEnumConverterTests.cs @@ -1,7 +1,7 @@ +using System.Text.Json; using CSharpEssentials.Enums; using CSharpEssentials.Json; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Json; diff --git a/CSharpEssentials.Tests/Json/EnhancedJsonSerializerOptionsTests.cs b/CSharpEssentials.Tests/Json/EnhancedJsonSerializerOptionsTests.cs index 915b270..0c5d442 100644 --- a/CSharpEssentials.Tests/Json/EnhancedJsonSerializerOptionsTests.cs +++ b/CSharpEssentials.Tests/Json/EnhancedJsonSerializerOptionsTests.cs @@ -51,10 +51,7 @@ public void Create_ShouldCloneAndApplyConfiguration() { JsonSerializerOptions baseOptions = EnhancedJsonSerializerOptions.DefaultOptionsWithoutConverters; - JsonSerializerOptions newOptions = baseOptions.Create(opts => - { - opts.WriteIndented = true; - }); + JsonSerializerOptions newOptions = baseOptions.Create(opts => opts.WriteIndented = true); newOptions.WriteIndented.Should().BeTrue(); newOptions.PropertyNameCaseInsensitive.Should().BeTrue(); diff --git a/CSharpEssentials.Tests/Json/JsonExtensionsTests.cs b/CSharpEssentials.Tests/Json/JsonExtensionsTests.cs index a1eac12..61e7cbf 100644 --- a/CSharpEssentials.Tests/Json/JsonExtensionsTests.cs +++ b/CSharpEssentials.Tests/Json/JsonExtensionsTests.cs @@ -1,7 +1,7 @@ +using System.Text.Json; using CSharpEssentials.Json; using CSharpEssentials.ResultPattern; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Json; diff --git a/CSharpEssentials.Tests/Json/MultiFormatDateTimeConverterTests.cs b/CSharpEssentials.Tests/Json/MultiFormatDateTimeConverterTests.cs index 59f1f7e..6c3a86f 100644 --- a/CSharpEssentials.Tests/Json/MultiFormatDateTimeConverterTests.cs +++ b/CSharpEssentials.Tests/Json/MultiFormatDateTimeConverterTests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Json; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Json; diff --git a/CSharpEssentials.Tests/Json/PolymorphicJsonConverterFactoryTests.cs b/CSharpEssentials.Tests/Json/PolymorphicJsonConverterFactoryTests.cs index 0cd596b..065f7d4 100644 --- a/CSharpEssentials.Tests/Json/PolymorphicJsonConverterFactoryTests.cs +++ b/CSharpEssentials.Tests/Json/PolymorphicJsonConverterFactoryTests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Json; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Json; diff --git a/CSharpEssentials.Tests/Maybe/MaybeAdvancedModulesTests.cs b/CSharpEssentials.Tests/Maybe/MaybeAdvancedModulesTests.cs index 35eb90e..3acd216 100644 --- a/CSharpEssentials.Tests/Maybe/MaybeAdvancedModulesTests.cs +++ b/CSharpEssentials.Tests/Maybe/MaybeAdvancedModulesTests.cs @@ -1,6 +1,6 @@ +using System.Globalization; using CSharpEssentials.Maybe; using FluentAssertions; -using System.Globalization; namespace CSharpEssentials.Tests.Maybe; @@ -23,7 +23,7 @@ public void Choose_WithSelector_ShouldProjectValuesOnly() var result = source.Choose(x => x.ToString(CultureInfo.InvariantCulture)).ToList(); result.Should().HaveCount(3); - result.Should().Contain("1", "2", "3"); + result.Should().BeEquivalentTo("1", "2", "3"); } [Fact] diff --git a/CSharpEssentials.Tests/Maybe/MaybeBindIfTests.cs b/CSharpEssentials.Tests/Maybe/MaybeBindIfTests.cs index e47d462..6dd6305 100644 --- a/CSharpEssentials.Tests/Maybe/MaybeBindIfTests.cs +++ b/CSharpEssentials.Tests/Maybe/MaybeBindIfTests.cs @@ -1,6 +1,5 @@ using CSharpEssentials.Maybe; using FluentAssertions; -using System.Globalization; namespace CSharpEssentials.Tests.Maybe; diff --git a/CSharpEssentials.Tests/Maybe/MaybeExtensionMembersTests.cs b/CSharpEssentials.Tests/Maybe/MaybeExtensionMembersTests.cs new file mode 100644 index 0000000..e487c99 --- /dev/null +++ b/CSharpEssentials.Tests/Maybe/MaybeExtensionMembersTests.cs @@ -0,0 +1,23 @@ +#if NET10_0_OR_GREATER +using CSharpEssentials.Maybe; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Maybe; + +public sealed class MaybeExtensionMembersTests +{ + [Fact] + public void IsNone_Should_Be_True_When_Empty() + { + var maybe = Maybe.None; + maybe.IsNone.Should().BeTrue(); + } + + [Fact] + public void IsNone_Should_Be_False_When_Has_Value() + { + var maybe = Maybe.From(42); + maybe.IsNone.Should().BeFalse(); + } +} +#endif diff --git a/CSharpEssentials.Tests/Maybe/MaybeModulesTests.cs b/CSharpEssentials.Tests/Maybe/MaybeModulesTests.cs index a8b19ea..6123702 100644 --- a/CSharpEssentials.Tests/Maybe/MaybeModulesTests.cs +++ b/CSharpEssentials.Tests/Maybe/MaybeModulesTests.cs @@ -1,6 +1,6 @@ +using System.Globalization; using CSharpEssentials.Maybe; using FluentAssertions; -using System.Globalization; namespace CSharpEssentials.Tests.Maybe; diff --git a/CSharpEssentials.Tests/Maybe/MaybeTests.cs b/CSharpEssentials.Tests/Maybe/MaybeTests.cs index 50702c7..797b1a4 100644 --- a/CSharpEssentials.Tests/Maybe/MaybeTests.cs +++ b/CSharpEssentials.Tests/Maybe/MaybeTests.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using CSharpEssentials.Maybe; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Maybe; diff --git a/CSharpEssentials.Tests/Maybe/MaybeToResultAliasTests.cs b/CSharpEssentials.Tests/Maybe/MaybeToResultAliasTests.cs index 26ba016..1f6afbb 100644 --- a/CSharpEssentials.Tests/Maybe/MaybeToResultAliasTests.cs +++ b/CSharpEssentials.Tests/Maybe/MaybeToResultAliasTests.cs @@ -16,7 +16,7 @@ public void ToResult_WithValue_ShouldReturnSuccess() { Maybe maybe = 42; - Result result = maybe.ToResult(); + var result = maybe.ToResult(); result.IsSuccess.Should().BeTrue(); result.Value.Should().Be(42); @@ -27,7 +27,7 @@ public void ToResult_WithoutValue_ShouldReturnDefaultError() { Maybe maybe = Maybe.None; - Result result = maybe.ToResult(); + var result = maybe.ToResult(); result.IsFailure.Should().BeTrue(); result.FirstError.Code.Should().Be("Maybe.Result"); @@ -38,7 +38,7 @@ public void ToResult_WithoutValue_WithCustomError_ShouldReturnCustomError() { Maybe maybe = Maybe.None; - Result result = maybe.ToResult(TestError); + var result = maybe.ToResult(TestError); result.IsFailure.Should().BeTrue(); result.FirstError.Should().Be(TestError); diff --git a/CSharpEssentials.Tests/Meta/JsonExtensionsTests.cs b/CSharpEssentials.Tests/Meta/JsonExtensionsTests.cs index 599cf69..8909a80 100644 --- a/CSharpEssentials.Tests/Meta/JsonExtensionsTests.cs +++ b/CSharpEssentials.Tests/Meta/JsonExtensionsTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using CSharpEssentials.Errors; using CSharpEssentials.Json; +using CSharpEssentials.ResultPattern; using FluentAssertions; namespace CSharpEssentials.Tests.Meta; @@ -11,9 +12,9 @@ public class JsonExtensionsTests public void TryGetProperty_ShouldReturnProperty_WhenFound() { string json = """{"name":"Alice","age":30}"""; - using JsonDocument doc = JsonDocument.Parse(json); + using var doc = JsonDocument.Parse(json); - var result = doc.RootElement.TryGetProperty("name"); + Result result = doc.RootElement.TryGetProperty("name"); result.IsSuccess.Should().BeTrue(); result.Value.GetString().Should().Be("Alice"); @@ -23,9 +24,9 @@ public void TryGetProperty_ShouldReturnProperty_WhenFound() public void TryGetProperty_ShouldReturnError_WhenNotFound() { string json = """{"name":"Alice"}"""; - using JsonDocument doc = JsonDocument.Parse(json); + using var doc = JsonDocument.Parse(json); - var result = doc.RootElement.TryGetProperty("missing"); + Result result = doc.RootElement.TryGetProperty("missing"); result.IsFailure.Should().BeTrue(); result.Errors[0].Type.Should().Be(ErrorType.NotFound); @@ -35,9 +36,9 @@ public void TryGetProperty_ShouldReturnError_WhenNotFound() public void TryGetProperty_ShouldReturnError_WhenNoNamesProvided() { string json = """{"name":"Alice"}"""; - using JsonDocument doc = JsonDocument.Parse(json); + using var doc = JsonDocument.Parse(json); - var result = doc.RootElement.TryGetProperty(); + Result result = doc.RootElement.TryGetProperty(); result.IsFailure.Should().BeTrue(); result.Errors[0].Type.Should().Be(ErrorType.Validation); @@ -47,9 +48,9 @@ public void TryGetProperty_ShouldReturnError_WhenNoNamesProvided() public void TryGetNestedProperty_ShouldReturnNestedValue_WhenPathExists() { string json = """{"user":{"profile":{"name":"Alice"}}}"""; - using JsonDocument doc = JsonDocument.Parse(json); + using var doc = JsonDocument.Parse(json); - var result = doc.TryGetNestedProperty("user", "profile", "name"); + Result result = doc.TryGetNestedProperty("user", "profile", "name"); result.IsSuccess.Should().BeTrue(); result.Value!.Value.GetString().Should().Be("Alice"); @@ -59,9 +60,9 @@ public void TryGetNestedProperty_ShouldReturnNestedValue_WhenPathExists() public void TryGetNestedProperty_ShouldReturnError_WhenPathMissing() { string json = """{"user":{"profile":{"name":"Alice"}}}"""; - using JsonDocument doc = JsonDocument.Parse(json); + using var doc = JsonDocument.Parse(json); - var result = doc.TryGetNestedProperty("user", "profile", "missing"); + Result result = doc.TryGetNestedProperty("user", "profile", "missing"); result.IsFailure.Should().BeTrue(); result.Errors[0].Type.Should().Be(ErrorType.NotFound); @@ -71,9 +72,9 @@ public void TryGetNestedProperty_ShouldReturnError_WhenPathMissing() public void TryGetNestedProperty_ShouldReturnError_WhenNoNamesProvided() { string json = """{"name":"Alice"}"""; - using JsonDocument doc = JsonDocument.Parse(json); + using var doc = JsonDocument.Parse(json); - var result = doc.TryGetNestedProperty(); + Result result = doc.TryGetNestedProperty(); result.IsFailure.Should().BeTrue(); result.Errors[0].Type.Should().Be(ErrorType.Validation); diff --git a/CSharpEssentials.Tests/Meta/StringExtensionsTests.cs b/CSharpEssentials.Tests/Meta/StringExtensionsTests.cs index b77cacf..c6741f4 100644 --- a/CSharpEssentials.Tests/Meta/StringExtensionsTests.cs +++ b/CSharpEssentials.Tests/Meta/StringExtensionsTests.cs @@ -1,6 +1,5 @@ using CSharpEssentials.ResultPattern; using FluentAssertions; -using CSharpEssentials.Core; namespace CSharpEssentials.Tests.Meta; diff --git a/CSharpEssentials.Tests/MockHelpers.cs b/CSharpEssentials.Tests/MockHelpers.cs index ce9de73..0cfb678 100644 --- a/CSharpEssentials.Tests/MockHelpers.cs +++ b/CSharpEssentials.Tests/MockHelpers.cs @@ -26,6 +26,7 @@ public static DbContextOptions CreateInMemoryOptions(string? .Options; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1873", Justification = "False positive: Moq verification expression, not an actual ILogger.Log call")] public static void VerifyLog(Mock> loggerMock, LogLevel level, string? messageSubstring = null, Times? times = null) { times ??= Times.Once(); diff --git a/CSharpEssentials.Tests/RequestResponseLogging/ApplicationBuilderExtensionsTests.cs b/CSharpEssentials.Tests/RequestResponseLogging/ApplicationBuilderExtensionsTests.cs index 430a684..00aeff6 100644 --- a/CSharpEssentials.Tests/RequestResponseLogging/ApplicationBuilderExtensionsTests.cs +++ b/CSharpEssentials.Tests/RequestResponseLogging/ApplicationBuilderExtensionsTests.cs @@ -30,10 +30,7 @@ public void AddRequestResponseLogging_WithLoggerFactory_ShouldConfigureOptions() ILoggerFactory loggerFactory = Mock.Of(); - IApplicationBuilder result = mockBuilder.Object.AddRequestResponseLogging(options => - { - options.UseLogger(loggerFactory, _ => { }); - }); + IApplicationBuilder result = mockBuilder.Object.AddRequestResponseLogging(options => options.UseLogger(loggerFactory, _ => { })); result.Should().Be(mockBuilder.Object); } diff --git a/CSharpEssentials.Tests/RequestResponseLogging/NullLogWriterTests.cs b/CSharpEssentials.Tests/RequestResponseLogging/NullLogWriterTests.cs index 4e8d51c..22e1e13 100644 --- a/CSharpEssentials.Tests/RequestResponseLogging/NullLogWriterTests.cs +++ b/CSharpEssentials.Tests/RequestResponseLogging/NullLogWriterTests.cs @@ -20,7 +20,7 @@ public async Task Write_ShouldReturnCompletedTask() var writer = new NullLogWriter(); var context = new RequestResponseContext(new DefaultHttpContext()); - var task = writer.Write(context); + Task task = writer.Write(context); task.IsCompletedSuccessfully.Should().BeTrue(); await task; diff --git a/CSharpEssentials.Tests/Results/ResultCombineTests.cs b/CSharpEssentials.Tests/Results/ResultCombineTests.cs new file mode 100644 index 0000000..c78848d --- /dev/null +++ b/CSharpEssentials.Tests/Results/ResultCombineTests.cs @@ -0,0 +1,152 @@ +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Results; + +public class ResultCombineTests +{ + [Fact] + public void Combine_TwoSuccessResults_ShouldReturnTuple() + { + Result first = 42; + Result second = "hello"; + + Result<(int, string)> combined = Result.Combine(first, second); + + combined.IsSuccess.Should().BeTrue(); + combined.Value.Item1.Should().Be(42); + combined.Value.Item2.Should().Be("hello"); + } + + [Fact] + public void Combine_FirstFailure_ShouldReturnFailureWithErrors() + { + Result first = Error.Failure("First.Error", "First error"); + Result second = "hello"; + + Result<(int, string)> combined = Result.Combine(first, second); + + combined.IsFailure.Should().BeTrue(); + combined.Errors.Should().ContainSingle(e => e.Code == "First.Error"); + } + + [Fact] + public void Combine_SecondFailure_ShouldReturnFailureWithErrors() + { + Result first = 42; + Result second = Error.Failure("Second.Error", "Second error"); + + Result<(int, string)> combined = Result.Combine(first, second); + + combined.IsFailure.Should().BeTrue(); + combined.Errors.Should().ContainSingle(e => e.Code == "Second.Error"); + } + + [Fact] + public void Combine_BothFailure_ShouldReturnFailureWithAllErrors() + { + Result first = Error.Failure("First.Error", "First error"); + Result second = Error.Failure("Second.Error", "Second error"); + + Result<(int, string)> combined = Result.Combine(first, second); + + combined.IsFailure.Should().BeTrue(); + combined.Errors.Should().HaveCount(2); + combined.Errors.Should().Contain(e => e.Code == "First.Error"); + combined.Errors.Should().Contain(e => e.Code == "Second.Error"); + } + + [Fact] + public void Combine_ThreeSuccessResults_ShouldReturnTuple() + { + Result first = 1; + Result second = "two"; + Result third = true; + + Result<(int, string, bool)> combined = Result.Combine(first, second, third); + + combined.IsSuccess.Should().BeTrue(); + combined.Value.Should().Be((1, "two", true)); + } + + [Fact] + public void Combine_FourSuccessResults_ShouldReturnTuple() + { + Result r1 = 1; + Result r2 = 2; + Result r3 = 3; + Result r4 = 4; + + Result<(int, int, int, int)> combined = Result.Combine(r1, r2, r3, r4); + + combined.IsSuccess.Should().BeTrue(); + combined.Value.Should().Be((1, 2, 3, 4)); + } + + [Fact] + public void Combine_FiveSuccessResults_ShouldReturnTuple() + { + Result r1 = 1; + Result r2 = 2; + Result r3 = 3; + Result r4 = 4; + Result r5 = 5; + + Result<(int, int, int, int, int)> combined = Result.Combine(r1, r2, r3, r4, r5); + + combined.IsSuccess.Should().BeTrue(); + combined.Value.Should().Be((1, 2, 3, 4, 5)); + } + + [Fact] + public void Combine_SixSuccessResults_ShouldReturnTuple() + { + Result r1 = 1; + Result r2 = 2; + Result r3 = 3; + Result r4 = 4; + Result r5 = 5; + Result r6 = 6; + + Result<(int, int, int, int, int, int)> combined = Result.Combine(r1, r2, r3, r4, r5, r6); + + combined.IsSuccess.Should().BeTrue(); + combined.Value.Should().Be((1, 2, 3, 4, 5, 6)); + } + + [Fact] + public void Combine_SevenSuccessResults_ShouldReturnTuple() + { + Result r1 = 1; + Result r2 = 2; + Result r3 = 3; + Result r4 = 4; + Result r5 = 5; + Result r6 = 6; + Result r7 = 7; + + Result<(int, int, int, int, int, int, int)> combined = Result.Combine(r1, r2, r3, r4, r5, r6, r7); + + combined.IsSuccess.Should().BeTrue(); + combined.Value.Should().Be((1, 2, 3, 4, 5, 6, 7)); + } + + [Fact] + public void Combine_EightSuccessResults_ShouldReturnTuple() + { + Result r1 = 1; + Result r2 = 2; + Result r3 = 3; + Result r4 = 4; + Result r5 = 5; + Result r6 = 6; + Result r7 = 7; + Result r8 = 8; + + Result<(int, int, int, int, int, int, int, int)> combined = Result.Combine(r1, r2, r3, r4, r5, r6, r7, r8); + + combined.IsSuccess.Should().BeTrue(); + combined.Value.Should().Be((1, 2, 3, 4, 5, 6, 7, 8)); + } +} diff --git a/CSharpEssentials.Tests/Results/ResultExtensionMembersTests.cs b/CSharpEssentials.Tests/Results/ResultExtensionMembersTests.cs new file mode 100644 index 0000000..4384645 --- /dev/null +++ b/CSharpEssentials.Tests/Results/ResultExtensionMembersTests.cs @@ -0,0 +1,55 @@ +#if NET10_0_OR_GREATER +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Results; + +public sealed class ResultExtensionMembersTests +{ + [Fact] + public void ValueOrDefault_Should_Return_Value_When_Success() + { + var result = 42; + result.ValueOrDefault.Should().Be(42); + } + + [Fact] + public void ValueOrDefault_Should_Return_Default_When_Failure() + { + var result = Result.Failure(Error.Failure("E1")); + result.ValueOrDefault.Should().Be(0); + } + + [Fact] + public void ValueOrDefault_Should_Return_Null_For_Reference_Type_When_Failure() + { + var result = Result.Failure(Error.Failure("E1")); + result.ValueOrDefault.Should().BeNull(); + } + + [Fact] + public void OrOperator_Should_Return_Left_When_Success() + { + var left = 1; + var right = 2; + (left | right).Value.Should().Be(1); + } + + [Fact] + public void OrOperator_Should_Return_Right_When_Left_Fails() + { + var left = Result.Failure(Error.Failure("E1")); + var right = 2; + (left | right).Value.Should().Be(2); + } + + [Fact] + public void OrOperator_Should_Return_Right_When_Both_Fail() + { + var left = Result.Failure(Error.Failure("E1")); + var right = Result.Failure(Error.Failure("E2")); + (left | right).IsFailure.Should().BeTrue(); + } +} +#endif diff --git a/CSharpEssentials.Tests/Results/ResultFailIfTests.cs b/CSharpEssentials.Tests/Results/ResultFailIfTests.cs index 85b5662..2632bda 100644 --- a/CSharpEssentials.Tests/Results/ResultFailIfTests.cs +++ b/CSharpEssentials.Tests/Results/ResultFailIfTests.cs @@ -142,7 +142,7 @@ public async Task ResultT_FailIfAsync_WithErrorFunc_ConditionFalse_ShouldReturnO [Fact] public async Task TaskResultT_FailIf_ConditionTrue_WithSuccess_ShouldReturnFailure() { - var task = Task.FromResult(Result.Success(10)); + Task> task = Task.FromResult(Result.Success(10)); Result failIfResult = await task.FailIf( value => value > 5, @@ -155,7 +155,7 @@ public async Task TaskResultT_FailIf_ConditionTrue_WithSuccess_ShouldReturnFailu [Fact] public async Task TaskResultT_FailIf_ConditionFalse_WithSuccess_ShouldReturnOriginal() { - var task = Task.FromResult(Result.Success(3)); + Task> task = Task.FromResult(Result.Success(3)); Result failIfResult = await task.FailIf( value => value > 5, @@ -168,7 +168,7 @@ public async Task TaskResultT_FailIf_ConditionFalse_WithSuccess_ShouldReturnOrig [Fact] public async Task TaskResultT_FailIf_WithFailure_ShouldReturnOriginalFailure() { - var task = Task.FromResult(Result.Failure(TestError)); + Task> task = Task.FromResult(Result.Failure(TestError)); Result failIfResult = await task.FailIf( value => value > 5, @@ -181,7 +181,7 @@ public async Task TaskResultT_FailIf_WithFailure_ShouldReturnOriginalFailure() [Fact] public async Task TaskResultT_FailIf_WithErrorFunc_ConditionTrue_ShouldReturnComputedError() { - var task = Task.FromResult(Result.Success(10)); + Task> task = Task.FromResult(Result.Success(10)); Result failIfResult = await task.FailIf( value => value > 5, @@ -194,7 +194,7 @@ public async Task TaskResultT_FailIf_WithErrorFunc_ConditionTrue_ShouldReturnCom [Fact] public async Task TaskResultT_FailIfAsync_ConditionTrue_WithSuccess_ShouldReturnFailure() { - var task = Task.FromResult(Result.Success(10)); + Task> task = Task.FromResult(Result.Success(10)); Result failIfResult = await task.FailIfAsync( value => Task.FromResult(value > 5), @@ -207,7 +207,7 @@ public async Task TaskResultT_FailIfAsync_ConditionTrue_WithSuccess_ShouldReturn [Fact] public async Task TaskResultT_FailIfAsync_ConditionFalse_WithSuccess_ShouldReturnOriginal() { - var task = Task.FromResult(Result.Success(3)); + Task> task = Task.FromResult(Result.Success(3)); Result failIfResult = await task.FailIfAsync( value => Task.FromResult(value > 5), @@ -220,7 +220,7 @@ public async Task TaskResultT_FailIfAsync_ConditionFalse_WithSuccess_ShouldRetur [Fact] public async Task TaskResultT_FailIfAsync_WithFailure_ShouldReturnOriginalFailure() { - var task = Task.FromResult(Result.Failure(TestError)); + Task> task = Task.FromResult(Result.Failure(TestError)); Result failIfResult = await task.FailIfAsync( value => Task.FromResult(value > 5), @@ -233,7 +233,7 @@ public async Task TaskResultT_FailIfAsync_WithFailure_ShouldReturnOriginalFailur [Fact] public async Task TaskResultT_FailIfAsync_WithErrorFunc_ConditionTrue_ShouldReturnComputedError() { - var task = Task.FromResult(Result.Success(10)); + Task> task = Task.FromResult(Result.Success(10)); Result failIfResult = await task.FailIfAsync( value => Task.FromResult(value > 5), diff --git a/CSharpEssentials.Tests/Results/ResultFailureIfTests.cs b/CSharpEssentials.Tests/Results/ResultFailureIfTests.cs index 236477b..fae6d3e 100644 --- a/CSharpEssentials.Tests/Results/ResultFailureIfTests.cs +++ b/CSharpEssentials.Tests/Results/ResultFailureIfTests.cs @@ -13,7 +13,7 @@ public class ResultFailureIfTests [Fact] public void Result_FailureIf_Bool_True_ShouldReturnFailure() { - Result result = Result.FailureIf(true, TestError); + var result = Result.FailureIf(true, TestError); result.IsFailure.Should().BeTrue(); result.FirstError.Should().Be(TestError); @@ -22,7 +22,7 @@ public void Result_FailureIf_Bool_True_ShouldReturnFailure() [Fact] public void Result_FailureIf_Bool_False_ShouldReturnSuccess() { - Result result = Result.FailureIf(false, TestError); + var result = Result.FailureIf(false, TestError); result.IsSuccess.Should().BeTrue(); } @@ -34,7 +34,7 @@ public void Result_FailureIf_Bool_False_ShouldReturnSuccess() [Fact] public void Result_FailureIf_Func_True_ShouldReturnFailure() { - Result result = Result.FailureIf(() => true, TestError); + var result = Result.FailureIf(() => true, TestError); result.IsFailure.Should().BeTrue(); result.FirstError.Should().Be(TestError); @@ -43,7 +43,7 @@ public void Result_FailureIf_Func_True_ShouldReturnFailure() [Fact] public void Result_FailureIf_Func_False_ShouldReturnSuccess() { - Result result = Result.FailureIf(() => false, TestError); + var result = Result.FailureIf(() => false, TestError); result.IsSuccess.Should().BeTrue(); } @@ -55,7 +55,7 @@ public void Result_FailureIf_Func_False_ShouldReturnSuccess() [Fact] public void Result_FailureIfT_Bool_True_ShouldReturnFailure() { - Result result = Result.FailureIf(true, TestError); + var result = Result.FailureIf(true, TestError); result.IsFailure.Should().BeTrue(); result.FirstError.Should().Be(TestError); @@ -64,7 +64,7 @@ public void Result_FailureIfT_Bool_True_ShouldReturnFailure() [Fact] public void Result_FailureIfT_Bool_False_ShouldReturnSuccessWithDefaultValue() { - Result result = Result.FailureIf(false, TestError); + var result = Result.FailureIf(false, TestError); result.IsSuccess.Should().BeTrue(); result.Value.Should().Be(0); diff --git a/CSharpEssentials.Tests/Results/ResultRecoverTests.cs b/CSharpEssentials.Tests/Results/ResultRecoverTests.cs new file mode 100644 index 0000000..4332baa --- /dev/null +++ b/CSharpEssentials.Tests/Results/ResultRecoverTests.cs @@ -0,0 +1,96 @@ +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Results; + +public class ResultRecoverTests +{ + [Fact] + public void Recover_ErrorTypeMatch_ShouldReturnRecoveredResult() + { + Result result = Error.NotFound("Test.NotFound", "Not found"); + + Result recovered = result.Recover(ErrorType.NotFound, _ => 42); + + recovered.IsSuccess.Should().BeTrue(); + recovered.Value.Should().Be(42); + } + + [Fact] + public void Recover_ErrorTypeMismatch_ShouldReturnOriginalFailure() + { + Result result = Error.Validation("Test.Validation", "Invalid"); + + Result recovered = result.Recover(ErrorType.NotFound, _ => 42); + + recovered.IsFailure.Should().BeTrue(); + recovered.FirstError.Code.Should().Be("Test.Validation"); + } + + [Fact] + public void Recover_SuccessResult_ShouldReturnOriginalSuccess() + { + Result result = 100; + + Result recovered = result.Recover(ErrorType.NotFound, _ => 42); + + recovered.IsSuccess.Should().BeTrue(); + recovered.Value.Should().Be(100); + } + + [Fact] + public void Recover_WithResultRecovery_ShouldReturnRecoveredResult() + { + Result result = Error.NotFound("Test.NotFound", "Not found"); + + Result recovered = result.Recover(ErrorType.NotFound, _ => Result.Success(42)); + + recovered.IsSuccess.Should().BeTrue(); + recovered.Value.Should().Be(42); + } + + [Fact] + public void RecoverFirst_ErrorTypeMatch_ShouldReturnRecoveredResult() + { + Result result = Error.NotFound("Test.NotFound", "Not found"); + + Result recovered = result.RecoverFirst(ErrorType.NotFound, _ => 42); + + recovered.IsSuccess.Should().BeTrue(); + recovered.Value.Should().Be(42); + } + + [Fact] + public void RecoverFirst_ErrorTypeMismatch_ShouldReturnOriginalFailure() + { + Result result = Error.Validation("Test.Validation", "Invalid"); + + Result recovered = result.RecoverFirst(ErrorType.NotFound, _ => 42); + + recovered.IsFailure.Should().BeTrue(); + recovered.FirstError.Code.Should().Be("Test.Validation"); + } + + [Fact] + public void Recover_PredicateMatch_ShouldReturnRecoveredResult() + { + Result result = Error.NotFound("Test.NotFound", "Not found"); + + Result recovered = result.Recover(e => e.Type == ErrorType.NotFound, _ => 42); + + recovered.IsSuccess.Should().BeTrue(); + recovered.Value.Should().Be(42); + } + + [Fact] + public void Recover_PredicateMismatch_ShouldReturnOriginalFailure() + { + Result result = Error.Validation("Test.Validation", "Invalid"); + + Result recovered = result.Recover(e => e.Type == ErrorType.NotFound, _ => 42); + + recovered.IsFailure.Should().BeTrue(); + recovered.FirstError.Code.Should().Be("Test.Validation"); + } +} diff --git a/CSharpEssentials.Tests/Results/ResultSuccessIfTests.cs b/CSharpEssentials.Tests/Results/ResultSuccessIfTests.cs index e5cc08a..e208143 100644 --- a/CSharpEssentials.Tests/Results/ResultSuccessIfTests.cs +++ b/CSharpEssentials.Tests/Results/ResultSuccessIfTests.cs @@ -13,7 +13,7 @@ public class ResultSuccessIfTests [Fact] public void Result_SuccessIf_Bool_True_ShouldReturnSuccess() { - Result result = Result.SuccessIf(true, TestError); + var result = Result.SuccessIf(true, TestError); result.IsSuccess.Should().BeTrue(); } @@ -21,7 +21,7 @@ public void Result_SuccessIf_Bool_True_ShouldReturnSuccess() [Fact] public void Result_SuccessIf_Bool_False_ShouldReturnFailure() { - Result result = Result.SuccessIf(false, TestError); + var result = Result.SuccessIf(false, TestError); result.IsFailure.Should().BeTrue(); result.FirstError.Should().Be(TestError); @@ -34,7 +34,7 @@ public void Result_SuccessIf_Bool_False_ShouldReturnFailure() [Fact] public void Result_SuccessIf_Func_True_ShouldReturnSuccess() { - Result result = Result.SuccessIf(() => true, TestError); + var result = Result.SuccessIf(() => true, TestError); result.IsSuccess.Should().BeTrue(); } @@ -42,7 +42,7 @@ public void Result_SuccessIf_Func_True_ShouldReturnSuccess() [Fact] public void Result_SuccessIf_Func_False_ShouldReturnFailure() { - Result result = Result.SuccessIf(() => false, TestError); + var result = Result.SuccessIf(() => false, TestError); result.IsFailure.Should().BeTrue(); result.FirstError.Should().Be(TestError); @@ -55,7 +55,7 @@ public void Result_SuccessIf_Func_False_ShouldReturnFailure() [Fact] public void Result_SuccessIfT_Bool_True_ShouldReturnSuccessWithValue() { - Result result = Result.SuccessIf(true, 42, TestError); + var result = Result.SuccessIf(true, 42, TestError); result.IsSuccess.Should().BeTrue(); result.Value.Should().Be(42); @@ -64,7 +64,7 @@ public void Result_SuccessIfT_Bool_True_ShouldReturnSuccessWithValue() [Fact] public void Result_SuccessIfT_Bool_False_ShouldReturnFailure() { - Result result = Result.SuccessIf(false, 42, TestError); + var result = Result.SuccessIf(false, 42, TestError); result.IsFailure.Should().BeTrue(); result.FirstError.Should().Be(TestError); diff --git a/CSharpEssentials.Tests/Results/ResultTModulesTests.cs b/CSharpEssentials.Tests/Results/ResultTModulesTests.cs index 1ceda42..aae8d2a 100644 --- a/CSharpEssentials.Tests/Results/ResultTModulesTests.cs +++ b/CSharpEssentials.Tests/Results/ResultTModulesTests.cs @@ -1,7 +1,7 @@ +using System.Globalization; using CSharpEssentials.Errors; using CSharpEssentials.ResultPattern; using FluentAssertions; -using System.Globalization; namespace CSharpEssentials.Tests.Results; diff --git a/CSharpEssentials.Tests/Results/ResultTTests.cs b/CSharpEssentials.Tests/Results/ResultTTests.cs index 6bc522f..85f276f 100644 --- a/CSharpEssentials.Tests/Results/ResultTTests.cs +++ b/CSharpEssentials.Tests/Results/ResultTTests.cs @@ -1,7 +1,7 @@ +using System.Text.Json; using CSharpEssentials.Errors; using CSharpEssentials.ResultPattern; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Results; diff --git a/CSharpEssentials.Tests/Results/ResultTests.cs b/CSharpEssentials.Tests/Results/ResultTests.cs index 5768d99..e8edd8b 100644 --- a/CSharpEssentials.Tests/Results/ResultTests.cs +++ b/CSharpEssentials.Tests/Results/ResultTests.cs @@ -1,7 +1,7 @@ +using System.Text.Json; using CSharpEssentials.Errors; using CSharpEssentials.ResultPattern; using FluentAssertions; -using System.Text.Json; namespace CSharpEssentials.Tests.Results; diff --git a/CSharpEssentials.Tests/Results/ResultThenTests.cs b/CSharpEssentials.Tests/Results/ResultThenTests.cs index 4a1cc4f..0549426 100644 --- a/CSharpEssentials.Tests/Results/ResultThenTests.cs +++ b/CSharpEssentials.Tests/Results/ResultThenTests.cs @@ -127,7 +127,7 @@ public async Task Result_ThenDoAsync_WithFailure_ShouldNotExecuteAction() [Fact] public async Task TaskResult_Then_WithSuccess_ShouldExecuteFunction() { - var task = Task.FromResult(Result.Success()); + Task task = Task.FromResult(Result.Success()); bool called = false; Result thenResult = await task.Then(() => { called = true; return Result.Success(); }, CancellationToken.None); @@ -139,7 +139,7 @@ public async Task TaskResult_Then_WithSuccess_ShouldExecuteFunction() [Fact] public async Task TaskResult_Then_WithFailure_ShouldNotExecuteFunction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.Then(() => { called = true; return Result.Success(); }, CancellationToken.None); @@ -151,7 +151,7 @@ public async Task TaskResult_Then_WithFailure_ShouldNotExecuteFunction() [Fact] public async Task TaskResult_ThenDo_WithSuccess_ShouldExecuteAction() { - var task = Task.FromResult(Result.Success()); + Task task = Task.FromResult(Result.Success()); bool called = false; Result thenResult = await task.ThenDo(() => called = true, CancellationToken.None); @@ -163,7 +163,7 @@ public async Task TaskResult_ThenDo_WithSuccess_ShouldExecuteAction() [Fact] public async Task TaskResult_ThenDo_WithFailure_ShouldNotExecuteAction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.ThenDo(() => called = true, CancellationToken.None); @@ -175,7 +175,7 @@ public async Task TaskResult_ThenDo_WithFailure_ShouldNotExecuteAction() [Fact] public async Task TaskResult_ThenAsync_WithSuccess_ShouldExecuteFunction() { - var task = Task.FromResult(Result.Success()); + Task task = Task.FromResult(Result.Success()); bool called = false; Result thenResult = await task.ThenAsync(() => { called = true; return Task.FromResult(Result.Success()); }, CancellationToken.None); @@ -187,7 +187,7 @@ public async Task TaskResult_ThenAsync_WithSuccess_ShouldExecuteFunction() [Fact] public async Task TaskResult_ThenAsync_WithFailure_ShouldNotExecuteFunction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.ThenAsync(() => { called = true; return Task.FromResult(Result.Success()); }, CancellationToken.None); @@ -199,7 +199,7 @@ public async Task TaskResult_ThenAsync_WithFailure_ShouldNotExecuteFunction() [Fact] public async Task TaskResult_ThenDoAsync_WithSuccess_ShouldExecuteAction() { - var task = Task.FromResult(Result.Success()); + Task task = Task.FromResult(Result.Success()); bool called = false; Result thenResult = await task.ThenDoAsync(() => { called = true; return Task.CompletedTask; }, CancellationToken.None); @@ -211,7 +211,7 @@ public async Task TaskResult_ThenDoAsync_WithSuccess_ShouldExecuteAction() [Fact] public async Task TaskResult_ThenDoAsync_WithFailure_ShouldNotExecuteAction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.ThenDoAsync(() => { called = true; return Task.CompletedTask; }, CancellationToken.None); @@ -422,7 +422,7 @@ public async Task ResultT_ThenDoAsync_WithFailure_ShouldNotExecuteAction() [Fact] public async Task TaskResultT_Then_ToResult_WithSuccess_ShouldExecuteFunction() { - var task = Task.FromResult(Result.Success(10)); + Task> task = Task.FromResult(Result.Success(10)); Result thenResult = await task.Then( value => Result.Success(value.ToString(System.Globalization.CultureInfo.InvariantCulture)), @@ -435,7 +435,7 @@ public async Task TaskResultT_Then_ToResult_WithSuccess_ShouldExecuteFunction() [Fact] public async Task TaskResultT_Then_ToResult_WithFailure_ShouldNotExecuteFunction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task> task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.Then( @@ -453,7 +453,7 @@ public async Task TaskResultT_Then_ToResult_WithFailure_ShouldNotExecuteFunction [Fact] public async Task TaskResultT_Then_ToValue_WithSuccess_ShouldExecuteFunction() { - var task = Task.FromResult(Result.Success(10)); + Task> task = Task.FromResult(Result.Success(10)); Result thenResult = await task.Then( value => value.ToString(System.Globalization.CultureInfo.InvariantCulture), @@ -466,7 +466,7 @@ public async Task TaskResultT_Then_ToValue_WithSuccess_ShouldExecuteFunction() [Fact] public async Task TaskResultT_Then_ToValue_WithFailure_ShouldNotExecuteFunction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task> task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.Then( @@ -484,7 +484,7 @@ public async Task TaskResultT_Then_ToValue_WithFailure_ShouldNotExecuteFunction( [Fact] public async Task TaskResultT_ThenDo_WithSuccess_ShouldExecuteAction() { - var task = Task.FromResult(Result.Success(42)); + Task> task = Task.FromResult(Result.Success(42)); int captured = 0; Result thenResult = await task.ThenDo(value => captured = value, CancellationToken.None); @@ -496,7 +496,7 @@ public async Task TaskResultT_ThenDo_WithSuccess_ShouldExecuteAction() [Fact] public async Task TaskResultT_ThenDo_WithFailure_ShouldNotExecuteAction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task> task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.ThenDo(_ => called = true, CancellationToken.None); @@ -508,7 +508,7 @@ public async Task TaskResultT_ThenDo_WithFailure_ShouldNotExecuteAction() [Fact] public async Task TaskResultT_ThenAsync_ToResult_WithSuccess_ShouldExecuteFunction() { - var task = Task.FromResult(Result.Success(10)); + Task> task = Task.FromResult(Result.Success(10)); Result thenResult = await task.ThenAsync( value => Task.FromResult(Result.Success(value.ToString(System.Globalization.CultureInfo.InvariantCulture))), @@ -521,7 +521,7 @@ public async Task TaskResultT_ThenAsync_ToResult_WithSuccess_ShouldExecuteFuncti [Fact] public async Task TaskResultT_ThenAsync_ToResult_WithFailure_ShouldNotExecuteFunction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task> task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.ThenAsync( @@ -539,7 +539,7 @@ public async Task TaskResultT_ThenAsync_ToResult_WithFailure_ShouldNotExecuteFun [Fact] public async Task TaskResultT_ThenAsync_ToValue_WithSuccess_ShouldExecuteFunction() { - var task = Task.FromResult(Result.Success(10)); + Task> task = Task.FromResult(Result.Success(10)); Result thenResult = await task.ThenAsync( value => Task.FromResult(value.ToString(System.Globalization.CultureInfo.InvariantCulture)), @@ -552,7 +552,7 @@ public async Task TaskResultT_ThenAsync_ToValue_WithSuccess_ShouldExecuteFunctio [Fact] public async Task TaskResultT_ThenAsync_ToValue_WithFailure_ShouldNotExecuteFunction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task> task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.ThenAsync( @@ -570,7 +570,7 @@ public async Task TaskResultT_ThenAsync_ToValue_WithFailure_ShouldNotExecuteFunc [Fact] public async Task TaskResultT_ThenDoAsync_WithSuccess_ShouldExecuteAction() { - var task = Task.FromResult(Result.Success(42)); + Task> task = Task.FromResult(Result.Success(42)); int captured = 0; Result thenResult = await task.ThenDoAsync( @@ -588,7 +588,7 @@ public async Task TaskResultT_ThenDoAsync_WithSuccess_ShouldExecuteAction() [Fact] public async Task TaskResultT_ThenDoAsync_WithFailure_ShouldNotExecuteAction() { - var task = Task.FromResult(Result.Failure(TestError)); + Task> task = Task.FromResult(Result.Failure(TestError)); bool called = false; Result thenResult = await task.ThenDoAsync( diff --git a/CSharpEssentials.Tests/Results/ResultTryGetTests.cs b/CSharpEssentials.Tests/Results/ResultTryGetTests.cs new file mode 100644 index 0000000..1bda0da --- /dev/null +++ b/CSharpEssentials.Tests/Results/ResultTryGetTests.cs @@ -0,0 +1,60 @@ +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Results; + +public class ResultTryGetTests +{ + [Fact] + public void TryGet_WithSuccessResult_ShouldReturnTrueAndValue() + { + Result result = 42; + + bool success = result.TryGet(out int value, out Error[]? errors); + + success.Should().BeTrue(); + value.Should().Be(42); + errors.Should().BeNull(); + } + + [Fact] + public void TryGet_WithFailureResult_ShouldReturnFalseAndErrors() + { + var error = Error.Failure("Test.Code", "Test error"); + Result result = error; + + bool success = result.TryGet(out int value, out Error[]? errors); + + success.Should().BeFalse(); + value.Should().Be(0); + errors.Should().NotBeNull(); + errors!.Should().ContainSingle(); + errors[0].Code.Should().Be("Test.Code"); + } + + [Fact] + public void TryGet_NonGeneric_WithSuccessResult_ShouldReturnTrueAndNullErrors() + { + var result = Result.Success(); + + bool success = result.TryGet(out Error[]? errors); + + success.Should().BeTrue(); + errors.Should().BeNull(); + } + + [Fact] + public void TryGet_NonGeneric_WithFailureResult_ShouldReturnFalseAndErrors() + { + var error = Error.Failure("Test.Code", "Test error"); + Result result = error; + + bool success = result.TryGet(out Error[]? errors); + + success.Should().BeFalse(); + errors.Should().NotBeNull(); + errors!.Should().ContainSingle(); + errors[0].Code.Should().Be("Test.Code"); + } +} diff --git a/CSharpEssentials.Tests/Results/ResultTryTests.cs b/CSharpEssentials.Tests/Results/ResultTryTests.cs index fe8f332..b0e7874 100644 --- a/CSharpEssentials.Tests/Results/ResultTryTests.cs +++ b/CSharpEssentials.Tests/Results/ResultTryTests.cs @@ -15,7 +15,7 @@ public void Result_Try_Action_Success_ShouldReturnSuccess() { bool called = false; - Result result = Result.Try(() => { called = true; }, ex => TestError); + var result = Result.Try(() => { called = true; }, ex => TestError); result.IsSuccess.Should().BeTrue(); called.Should().BeTrue(); @@ -24,7 +24,7 @@ public void Result_Try_Action_Success_ShouldReturnSuccess() [Fact] public void Result_Try_Action_Exception_ShouldReturnFailure() { - Result result = Result.Try( + var result = Result.Try( () => throw new InvalidOperationException("boom"), ex => { @@ -43,7 +43,7 @@ public void Result_Try_Action_Exception_ShouldReturnFailure() [Fact] public void Result_TryT_Func_Success_ShouldReturnSuccessWithValue() { - Result result = Result.Try(() => 42, ex => TestError); + var result = Result.Try(() => 42, ex => TestError); result.IsSuccess.Should().BeTrue(); result.Value.Should().Be(42); @@ -52,7 +52,7 @@ public void Result_TryT_Func_Success_ShouldReturnSuccessWithValue() [Fact] public void Result_TryT_Func_Exception_ShouldReturnFailure() { - Result result = Result.Try( + var result = Result.Try( (Func>)(() => throw new InvalidOperationException("boom")), ex => TestError); @@ -63,7 +63,7 @@ public void Result_TryT_Func_Exception_ShouldReturnFailure() [Fact] public void Result_TryT_ResultFunc_Success_ShouldReturnSuccessWithValue() { - Result result = Result.Try(() => Result.Success(42), ex => TestError); + var result = Result.Try(() => Result.Success(42), ex => TestError); result.IsSuccess.Should().BeTrue(); result.Value.Should().Be(42); @@ -72,7 +72,7 @@ public void Result_TryT_ResultFunc_Success_ShouldReturnSuccessWithValue() [Fact] public void Result_TryT_ResultFunc_Exception_ShouldReturnFailure() { - Result result = Result.Try( + var result = Result.Try( () => throw new InvalidOperationException("boom"), ex => TestError); diff --git a/CSharpEssentials.Tests/Results/ResultUnwrapTests.cs b/CSharpEssentials.Tests/Results/ResultUnwrapTests.cs new file mode 100644 index 0000000..c0d28f0 --- /dev/null +++ b/CSharpEssentials.Tests/Results/ResultUnwrapTests.cs @@ -0,0 +1,50 @@ +using CSharpEssentials.Errors; +using CSharpEssentials.ResultPattern; +using FluentAssertions; + +namespace CSharpEssentials.Tests.Results; + +public class ResultUnwrapTests +{ + [Fact] + public void Unwrap_SuccessResult_ShouldReturnValue() + { + Result result = 42; + + int value = result.Unwrap(); + + value.Should().Be(42); + } + + [Fact] + public void Unwrap_FailureResult_ShouldThrowResultUnwrapException() + { + var error = Error.Failure("Test.Code", "Test error"); + Result result = error; + + Action act = () => result.Unwrap(); + + act.Should().Throw() + .Where(ex => ex.Errors.Length == 1 && ex.Errors[0].Code == "Test.Code"); + } + + [Fact] + public void UnwrapOrDefault_SuccessResult_ShouldReturnValue() + { + Result result = 42; + + int value = result.UnwrapOrDefault(0); + + value.Should().Be(42); + } + + [Fact] + public void UnwrapOrDefault_FailureResult_ShouldReturnDefault() + { + Result result = Error.Failure("Test.Code", "Test error"); + + int value = result.UnwrapOrDefault(99); + + value.Should().Be(99); + } +} diff --git a/CSharpEssentials.Tests/Rules/ExtensionTests.cs b/CSharpEssentials.Tests/Rules/ExtensionTests.cs index d2f7f69..04f0540 100644 --- a/CSharpEssentials.Tests/Rules/ExtensionTests.cs +++ b/CSharpEssentials.Tests/Rules/ExtensionTests.cs @@ -1,4 +1,3 @@ -using CSharpEssentials.Errors; using CSharpEssentials.ResultPattern; using CSharpEssentials.Rules; using FluentAssertions; diff --git a/CSharpEssentials.Tests/TestHelpers.cs b/CSharpEssentials.Tests/TestHelpers.cs index 4c8be0c..e6c8f4f 100644 --- a/CSharpEssentials.Tests/TestHelpers.cs +++ b/CSharpEssentials.Tests/TestHelpers.cs @@ -1,5 +1,5 @@ -using FluentAssertions; using System.Diagnostics; +using FluentAssertions; namespace CSharpEssentials.Tests; diff --git a/CSharpEssentials.Tests/Time/DateTimeProviderTests.cs b/CSharpEssentials.Tests/Time/DateTimeProviderTests.cs index 8708881..5a63eba 100644 --- a/CSharpEssentials.Tests/Time/DateTimeProviderTests.cs +++ b/CSharpEssentials.Tests/Time/DateTimeProviderTests.cs @@ -131,7 +131,7 @@ public void UtcNowTime_ShouldChangeWhenUtcNowAdvances() public void IDateTimeProvider_Interface_ShouldBeImplementedCorrectly() { FakeTimeProvider fakeTimeProvider = new(); - DateTimeProvider provider = new DateTimeProvider(fakeTimeProvider); + var provider = new DateTimeProvider(fakeTimeProvider); provider.TimeZone.Should().Be(TimeZoneInfo.Local); provider.TimeZoneUtc.Should().Be(TimeZoneInfo.Utc); diff --git a/CSharpEssentials.Tests/Time/ExtensionsTests.cs b/CSharpEssentials.Tests/Time/ExtensionsTests.cs index d21aff8..66c07a9 100644 --- a/CSharpEssentials.Tests/Time/ExtensionsTests.cs +++ b/CSharpEssentials.Tests/Time/ExtensionsTests.cs @@ -10,7 +10,7 @@ public void ToTimeOnly_ShouldExtractTimeComponent() { DateTime dateTime = new(2024, 6, 15, 14, 30, 45, 123, DateTimeKind.Utc); - TimeOnly result = dateTime.ToTimeOnly(); + var result = dateTime.ToTimeOnly(); result.Should().Be(new TimeOnly(14, 30, 45, 123)); } @@ -20,7 +20,7 @@ public void ToTimeOnly_Midnight_ShouldReturnMidnight() { DateTime dateTime = new(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); - TimeOnly result = dateTime.ToTimeOnly(); + var result = dateTime.ToTimeOnly(); result.Should().Be(TimeOnly.MinValue); } @@ -30,7 +30,7 @@ public void ToDateOnly_ShouldExtractDateComponent() { DateTime dateTime = new(2024, 6, 15, 14, 30, 45, DateTimeKind.Utc); - DateOnly result = dateTime.ToDateOnly(); + var result = dateTime.ToDateOnly(); result.Should().Be(new DateOnly(2024, 6, 15)); } @@ -40,7 +40,7 @@ public void ToDateOnly_MinValue_ShouldReturnMinDateOnly() { DateTime dateTime = DateTime.MinValue; - DateOnly result = dateTime.ToDateOnly(); + var result = dateTime.ToDateOnly(); result.Should().Be(DateOnly.MinValue); } @@ -50,7 +50,7 @@ public void ToDateOnly_MaxValue_ShouldReturnMaxDateOnly() { DateTime dateTime = DateTime.MaxValue; - DateOnly result = dateTime.ToDateOnly(); + var result = dateTime.ToDateOnly(); result.Should().Be(DateOnly.MaxValue); } @@ -60,7 +60,7 @@ public void ToTimeOnly_MaxValue_ShouldReturnMaxTimeOnly() { DateTime dateTime = DateTime.MaxValue; - TimeOnly result = dateTime.ToTimeOnly(); + var result = dateTime.ToTimeOnly(); result.Should().Be(TimeOnly.MaxValue); } @@ -70,8 +70,8 @@ public void ToDateOnly_And_ToTimeOnly_ShouldBeConsistentWithDateTime() { DateTime dateTime = new(2024, 12, 25, 8, 15, 30, DateTimeKind.Utc); - DateOnly date = dateTime.ToDateOnly(); - TimeOnly time = dateTime.ToTimeOnly(); + var date = dateTime.ToDateOnly(); + var time = dateTime.ToTimeOnly(); date.ToDateTime(time).Should().Be(dateTime); } diff --git a/CSharpEssentials.Time/CSharpEssentials.Time.csproj b/CSharpEssentials.Time/CSharpEssentials.Time.csproj index 52ee19b..64f254f 100644 --- a/CSharpEssentials.Time/CSharpEssentials.Time.csproj +++ b/CSharpEssentials.Time/CSharpEssentials.Time.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 CSharpEssentials.Time diff --git a/CSharpEssentials.Time/DateTimeProvider.cs b/CSharpEssentials.Time/DateTimeProvider.cs index 29e8d96..528fd44 100644 --- a/CSharpEssentials.Time/DateTimeProvider.cs +++ b/CSharpEssentials.Time/DateTimeProvider.cs @@ -4,10 +4,7 @@ public sealed class DateTimeProvider : IDateTimeProvider { private readonly TimeProvider _timeProvider; - public DateTimeProvider(TimeProvider timeProvider) - { - _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); - } + public DateTimeProvider(TimeProvider timeProvider) => _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); public DateTimeOffset UtcNow => _timeProvider.GetUtcNow(); public DateTime UtcNowDateTime => UtcNow.DateTime; diff --git a/CSharpEssentials.slnx b/CSharpEssentials.slnx index b2da239..8e4e608 100644 --- a/CSharpEssentials.slnx +++ b/CSharpEssentials.slnx @@ -8,7 +8,9 @@ + + diff --git a/CSharpEssentials/CSharpEssentials.csproj b/CSharpEssentials/CSharpEssentials.csproj index e06b13a..4a54d56 100644 --- a/CSharpEssentials/CSharpEssentials.csproj +++ b/CSharpEssentials/CSharpEssentials.csproj @@ -2,7 +2,7 @@ - net9.0;netstandard2.1 + net11.0;net10.0;net9.0;netstandard2.1 enable enable true @@ -57,6 +57,8 @@ Condition="'$(UseProjectReferences)' != 'true'" /> + + @@ -80,6 +83,8 @@ Condition="'$(UseProjectReferences)' == 'true'" /> + + diff --git a/CSharpEssentials/Extensions/TimeExtensions.cs b/CSharpEssentials/Extensions/TimeExtensions.cs index 13b96e4..b42db5e 100644 --- a/CSharpEssentials/Extensions/TimeExtensions.cs +++ b/CSharpEssentials/Extensions/TimeExtensions.cs @@ -1,4 +1,4 @@ -using System; + using CSharpEssentials.Core; using CSharpEssentials.Maybe; diff --git a/Directory.Build.props b/Directory.Build.props index e9b56b5..57e3b93 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,10 @@ - latest + preview + + + $(DefineConstants);CSHARP_ESSENTIALS_ENABLE_UNIONS true @@ -40,9 +43,9 @@ main - 3.0.1 + 3.0.2 3.0.0.0 - 3.0.1.0 + 3.0.2.0 true diff --git a/Directory.Packages.props b/Directory.Packages.props index 0fcdd1d..bd659b0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,8 +23,10 @@ - + + + @@ -39,17 +41,18 @@ - - - - - - - - - - - + + + + + + + + + + + + @@ -79,6 +82,8 @@ + + diff --git a/examples/Examples.Enums/Examples.Enums.csproj b/examples/Examples.Enums/Examples.Enums.csproj index de4588e..e2512f5 100644 --- a/examples/Examples.Enums/Examples.Enums.csproj +++ b/examples/Examples.Enums/Examples.Enums.csproj @@ -15,7 +15,10 @@ - + diff --git a/examples/Examples.Enums/Program.cs b/examples/Examples.Enums/Program.cs index 92dac63..18b38f4 100644 --- a/examples/Examples.Enums/Program.cs +++ b/examples/Examples.Enums/Program.cs @@ -1,46 +1,110 @@ using CSharpEssentials.Enums; -Console.WriteLine("========================================"); -Console.WriteLine("CSharpEssentials.Enums Example"); -Console.WriteLine("========================================\n"); +namespace Examples.Enums; -// ============================================================================ -// STRING ENUM ATTRIBUTE -// ============================================================================ -Console.WriteLine("--- StringEnumAttribute ---"); +public class Program +{ + public static void Main(string[] args) + { + Console.WriteLine("========================================"); + Console.WriteLine("CSharpEssentials.Enums Example"); + Console.WriteLine("========================================\n"); -// The [StringEnum] attribute marks enums that should be serialized -// as strings (instead of integers) in JSON or other contexts. -Status status = Status.Active; -Console.WriteLine($"Status.Active value: {(int)status}"); -Console.WriteLine($"Status.Active name: {status}"); + // ============================================================================ + // STRING ENUM ATTRIBUTE + // ============================================================================ + Console.WriteLine("--- StringEnumAttribute ---"); -Priority priority = Priority.High; -Console.WriteLine($"Priority.High value: {(int)priority}"); -Console.WriteLine($"Priority.High name: {priority}"); + // The [StringEnum] attribute marks enums that should be serialized + // as strings (instead of integers) in JSON or other contexts. + Status status = Status.Active; + Console.WriteLine($"Status.Active value: {(int)status}"); + Console.WriteLine($"Status.Active name: {status}"); -// The library's own ErrorType enum is also marked with [StringEnum] -Console.WriteLine("ErrorType in library is also [StringEnum]"); -Console.WriteLine(); + Priority priority = Priority.High; + Console.WriteLine($"Priority.High value: {(int)priority}"); + Console.WriteLine($"Priority.High name: {priority}"); -// ============================================================================ -// ENUM PARSING WITH STANDARD C# -// ============================================================================ -Console.WriteLine("--- Enum Parsing ---"); + // The library's own ErrorType enum is also marked with [StringEnum] + Console.WriteLine("ErrorType in library is also [StringEnum]"); + Console.WriteLine(); + + // ============================================================================ + // ENUM PARSING WITH STANDARD C# + // ============================================================================ + Console.WriteLine("--- Enum Parsing ---"); + + bool parsed = Enum.TryParse("Active", out Status activeValue); + Console.WriteLine($"Parse 'Active': {parsed}, Value={activeValue}"); + + bool unknown = Enum.TryParse("Unknown", out Status unknownValue); + Console.WriteLine($"Parse 'Unknown': {unknown}"); + Console.WriteLine(); + + // ============================================================================ + // SOURCE GENERATED FAST ToString & FORMATTING + // ============================================================================ + Console.WriteLine("--- Source Generated String Formatting ---"); + + Status s = Status.Active; + + string text = s.ToOptimizedString(); + Console.WriteLine($"Status.Active.ToOptimizedString() = {text}"); -bool parsed = Enum.TryParse("Active", out Status activeValue); -Console.WriteLine($"Parse 'Active': {parsed}, Value={activeValue}"); + string snake = s.ToSnakeCase(); + Console.WriteLine($"Status.Active.ToSnakeCase() = {snake}"); -bool unknown = Enum.TryParse("Unknown", out Status unknownValue); -Console.WriteLine($"Parse 'Unknown': {unknown}"); -Console.WriteLine(); + string kebab = s.ToKebabCase(); + Console.WriteLine($"Status.Active.ToKebabCase() = {kebab}"); -Console.WriteLine("========================================"); -Console.WriteLine("Demo complete."); -Console.WriteLine("Note: The StringEnumAttribute is used by"); -Console.WriteLine(" JSON converters to serialize enums"); -Console.WriteLine(" as strings instead of integers."); -Console.WriteLine("========================================"); + string lower = s.ToLowerCase(); + Console.WriteLine($"Status.Active.ToLowerCase() = {lower}"); + + string upper = s.ToUpperCase(); + Console.WriteLine($"Status.Active.ToUpperCase() = {upper}"); + Console.WriteLine(); + + // ============================================================================ + // SOURCE GENERATED CONSTANTS + // ============================================================================ + Console.WriteLine("--- Source Generated Constants ---"); + Console.WriteLine($"StatusExtensions.ActiveSnakeCase = {StatusExtensions.ActiveSnakeCase}"); + Console.WriteLine($"StatusExtensions.PendingKebabCase = {StatusExtensions.PendingKebabCase}"); + Console.WriteLine(); + + // ============================================================================ + // SOURCE GENERATED LOOKUP & PARSE + // ============================================================================ + Console.WriteLine("--- Source Generated Lookup & Parse ---"); + + bool known = StatusExtensions.IsDefined("Pending"); + Console.WriteLine($"StatusExtensions.IsDefined(\"Pending\") = {known}"); + + bool notKnown = StatusExtensions.IsDefined("Deleted"); + Console.WriteLine($"StatusExtensions.IsDefined(\"Deleted\") = {notKnown}"); + + if (StatusExtensions.TryParse("Inactive", out Status parsedStatus)) + { + Console.WriteLine($"StatusExtensions.TryParse(\"Inactive\") = {parsedStatus}"); + } + + string[] names = StatusExtensions.GetNames(); + Console.WriteLine($"StatusExtensions.GetNames() = [{string.Join(", ", names)}]"); + + Status[] values = StatusExtensions.GetValues(); + Console.WriteLine($"StatusExtensions.GetValues() = [{string.Join(", ", values)}]"); + Console.WriteLine(); + + Console.WriteLine("========================================"); + Console.WriteLine("Demo complete."); + Console.WriteLine("Note: The StringEnumAttribute is used by"); + Console.WriteLine(" JSON converters to serialize enums"); + Console.WriteLine(" as strings instead of integers."); + Console.WriteLine(" Source generators produce fast"); + Console.WriteLine(" ToString, parsing, and formatting."); + Console.WriteLine("========================================"); + } +} // ============================================================================ // ENUMS diff --git a/examples/Examples.Http/Examples.Http.csproj b/examples/Examples.Http/Examples.Http.csproj new file mode 100644 index 0000000..f7d2c75 --- /dev/null +++ b/examples/Examples.Http/Examples.Http.csproj @@ -0,0 +1,23 @@ + + + + Exe + net9.0 + enable + enable + false + None + false + false + false + false + false + + + + + + + + + diff --git a/examples/Examples.Http/Program.cs b/examples/Examples.Http/Program.cs new file mode 100644 index 0000000..dac9107 --- /dev/null +++ b/examples/Examples.Http/Program.cs @@ -0,0 +1,152 @@ +using System.Net; +using CSharpEssentials.Errors; +using CSharpEssentials.Http; +using CSharpEssentials.ResultPattern; + +Console.WriteLine("=== CSharpEssentials.Http Examples ==="); +Console.WriteLine(); + +// ============================================================================ +// STATUS CODE MAPPING +// ============================================================================ +Console.WriteLine("--- Status Code Mapping ---"); + +ErrorType badRequestType = HttpStatusCodeMapper.ToErrorType(HttpStatusCode.BadRequest); +Console.WriteLine($"400 -> {badRequestType}"); + +ErrorType notFoundType = HttpStatusCodeMapper.ToErrorType(HttpStatusCode.NotFound); +Console.WriteLine($"404 -> {notFoundType}"); + +ErrorType tooManyRequestsType = HttpStatusCodeMapper.ToErrorType(HttpStatusCode.TooManyRequests); +Console.WriteLine($"429 -> {tooManyRequestsType}"); + +ErrorType serverErrorType = HttpStatusCodeMapper.ToErrorType(HttpStatusCode.InternalServerError); +Console.WriteLine($"500 -> {serverErrorType}"); + +Error error = HttpStatusCodeMapper.ToError(HttpStatusCode.Forbidden); +Console.WriteLine($"403 Error: {error.Code} - {error.Description}"); +Console.WriteLine(); + +// ============================================================================ +// MOCK HTTP CLIENT WITH RESULT +// ============================================================================ +Console.WriteLine("--- HttpClient Result Extensions ---"); + +var mockHandler = new MockHttpHandler(); +HttpClient client = new(mockHandler) { BaseAddress = new Uri("https://api.example.com") }; + +Result userResult = await client.GetFromJsonAsResultAsync(new Uri("/users/1", UriKind.Relative)); +userResult.Switch( + onSuccess: u => Console.WriteLine($"GET User: {u.Name} ({u.Email})"), + onError: errs => Console.WriteLine($"GET Failed: {errs[0].Description}") +); + +Result postResult = await client.PostAsJsonAsResultAsync( + new Uri("/users", UriKind.Relative), + new { Name = "Alice", Email = "alice@example.com" }); +postResult.Switch( + onSuccess: u => Console.WriteLine($"POST User: {u.Name}"), + onError: errs => Console.WriteLine($"POST Failed: {errs[0].Description}") +); + +Result putResult = await client.PutAsJsonAsResultAsync( + new Uri("/users/1", UriKind.Relative), + new { Name = "Alice Updated", Email = "alice@example.com" }); +putResult.Switch( + onSuccess: u => Console.WriteLine($"PUT User: {u.Name}"), + onError: errs => Console.WriteLine($"PUT Failed: {errs[0].Description}") +); + +Result deleteResult = await client.DeleteAsResultAsync(new Uri("/users/1", UriKind.Relative)); +deleteResult.Switch( + onSuccess: () => Console.WriteLine("DELETE succeeded"), + onFailure: errs => Console.WriteLine($"DELETE Failed: {errs[0].Description}") +); + +Console.WriteLine(); + +// ============================================================================ +// QUERY STRING BUILDER +// ============================================================================ +Console.WriteLine("--- Query String Builder ---"); + +Uri baseUri = new("https://api.example.com/search"); +var queryUriResult = baseUri.WithQueryString("q", "csharp").WithQueryString("page", "1"); +queryUriResult.Switch( + onSuccess: uri => Console.WriteLine($"Query URI: {uri}"), + onFailure: errs => Console.WriteLine($"Query Failed: {errs[0].Description}") +); + +var dict = new Dictionary { { "sort", "desc" }, { "limit", "10" } }; +var dictUriResult = baseUri.WithQueryString(dict); +dictUriResult.Switch( + onSuccess: uri => Console.WriteLine($"Dict URI: {uri}"), + onFailure: errs => Console.WriteLine($"Dict Failed: {errs[0].Description}") +); + +Console.WriteLine(); + +// ============================================================================ +// HTTP REQUEST BUILDER +// ============================================================================ +Console.WriteLine("--- HttpRequestBuilder ---"); + +Result builderResult = await HttpRequestBuilder + .Get("/users/1") + .WithHeader("Accept", "application/json") + .AsResultAsync(client); + +builderResult.Switch( + onSuccess: u => Console.WriteLine($"Builder GET: {u.Name}"), + onError: errs => Console.WriteLine($"Builder GET Failed: {errs[0].Description}") +); + +Console.WriteLine(); + +// ============================================================================ +// RESILIENCE PIPELINE +// ============================================================================ +Console.WriteLine("--- Resilience Pipeline ---"); + +var pipeline = HttpClientResilienceExtensions.CreateResiliencePipeline( + maxRetryAttempts: 2, + timeout: TimeSpan.FromSeconds(5), + retryDelay: TimeSpan.FromMilliseconds(100)); + +Result resilientResult = await pipeline.ExecuteAsResultAsync(async token => + await client.GetFromJsonAsResultAsync(new Uri("/users/1", UriKind.Relative), cancellationToken: token)); + +resilientResult.Switch( + onSuccess: u => Console.WriteLine($"Resilient GET: {u.Name}"), + onError: errs => Console.WriteLine($"Resilient GET Failed: {errs[0].Description}") +); + +Console.WriteLine(); +Console.WriteLine("=== Done ==="); + +public sealed record User(string Name, string Email); + +public sealed class MockHttpHandler : HttpMessageHandler +{ + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpResponseMessage response = request.Method.Method switch + { + "GET" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("""{"name":"Bob","email":"bob@example.com"}""") + }, + "POST" => new HttpResponseMessage(HttpStatusCode.Created) + { + Content = new StringContent("""{"name":"Alice","email":"alice@example.com"}""") + }, + "PUT" => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("""{"name":"Alice Updated","email":"alice@example.com"}""") + }, + "DELETE" => new HttpResponseMessage(HttpStatusCode.NoContent), + _ => new HttpResponseMessage(HttpStatusCode.NotFound) + }; + return Task.FromResult(response); + } +} diff --git a/global.json b/global.json index cdbb589..3348979 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100", + "version": "11.0.100-preview.3", "rollForward": "latestFeature" } }