diff --git a/src/Ardalis.Result.AspNetCore/MinimalApiResultExtensions.cs b/src/Ardalis.Result.AspNetCore/MinimalApiResultExtensions.cs index a080c78..e18eed5 100644 --- a/src/Ardalis.Result.AspNetCore/MinimalApiResultExtensions.cs +++ b/src/Ardalis.Result.AspNetCore/MinimalApiResultExtensions.cs @@ -42,6 +42,7 @@ internal static Microsoft.AspNetCore.Http.IResult ToMinimalApiResult(this IResul ResultStatus.Invalid => Results.BadRequest(result.ValidationErrors), ResultStatus.Error => UnprocessableEntity(result), ResultStatus.Conflict => ConflictEntity(result), + ResultStatus.Unavailable => UnavailableEntity(result), ResultStatus.CriticalError => CriticalEntity(result), _ => throw new NotSupportedException($"Result {result.Status} conversion is not supported."), }; @@ -119,5 +120,26 @@ private static Microsoft.AspNetCore.Http.IResult CriticalEntity(IResult result) return Results.StatusCode(StatusCodes.Status500InternalServerError); } } + + private static Microsoft.AspNetCore.Http.IResult UnavailableEntity(IResult result) + { + var details = new StringBuilder("Next error(s) occured:"); + + if (result.Errors.Any()) + { + foreach (var error in result.Errors) details.Append("* ").Append(error).AppendLine(); + + return Results.Problem(new ProblemDetails + { + Title = "Service unavailable.", + Detail = details.ToString(), + Status = StatusCodes.Status503ServiceUnavailable + }); + } + else + { + return Results.StatusCode(StatusCodes.Status503ServiceUnavailable); + } + } } #endif diff --git a/src/Ardalis.Result.AspNetCore/ResultStatusMap.cs b/src/Ardalis.Result.AspNetCore/ResultStatusMap.cs index d86e1ae..cb37f64 100644 --- a/src/Ardalis.Result.AspNetCore/ResultStatusMap.cs +++ b/src/Ardalis.Result.AspNetCore/ResultStatusMap.cs @@ -38,7 +38,10 @@ public ResultStatusMap AddDefaultMap() .With(ConflictEntity)) .For(ResultStatus.CriticalError, HttpStatusCode.InternalServerError, resultStatusOptions => resultStatusOptions - .With(CriticalEntity)); + .With(CriticalEntity)) + .For(ResultStatus.Unavailable, HttpStatusCode.ServiceUnavailable, resultStatusOptions => + resultStatusOptions + .With(UnavailableEntity)); } /// @@ -155,6 +158,19 @@ private static ProblemDetails CriticalEntity(ControllerBase controller, IResult Detail = result.Errors.Any() ? details.ToString() : null }; } + + private static ProblemDetails UnavailableEntity(ControllerBase controller, IResult result) + { + var details = new StringBuilder("Next error(s) occured:"); + + foreach (var error in result.Errors) details.Append("* ").Append(error).AppendLine(); + + return new ProblemDetails + { + Title = "Service is unavailable.", + Detail = result.Errors.Any() ? details.ToString() : null + }; + } } public class ResultStatusOptions diff --git a/src/Ardalis.Result/Result.Void.cs b/src/Ardalis.Result/Result.Void.cs index a0c2817..7e1d453 100644 --- a/src/Ardalis.Result/Result.Void.cs +++ b/src/Ardalis.Result/Result.Void.cs @@ -162,6 +162,17 @@ public static Result ErrorWithCorrelationId(string correlationId, params string[ } /// + /// Represents a situation where a service is unavailable, such as when the underlying data store is unavailable. + /// Errors may be transient, so the caller may wish to retry the operation. + /// See also HTTP 503 Service Unavailable: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors + /// + /// A list of string error messages + /// + public new static Result Unavailable(params string[] errorMessages) + { + return new Result(ResultStatus.Unavailable) { Errors = errorMessages }; + } + /// Represents a critical error that occurred during the execution of the service. /// Everything provided by the user was valid, but the service was unable to complete due to an exception. /// See also HTTP 500 Internal Server Error: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors diff --git a/src/Ardalis.Result/Result.cs b/src/Ardalis.Result/Result.cs index 7bca7ee..c880113 100644 --- a/src/Ardalis.Result/Result.cs +++ b/src/Ardalis.Result/Result.cs @@ -206,5 +206,17 @@ public static Result CriticalError(params string[] errorMessages) { return new Result(ResultStatus.CriticalError) { Errors = errorMessages }; } + + /// + /// Represents a situation where a service is unavailable, such as when the underlying data store is unavailable. + /// Errors may be transient, so the caller may wish to retry the operation. + /// See also HTTP 503 Service Unavailable: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors + /// + /// A list of string error messages + /// + public static Result Unavailable(params string[] errorMessages) + { + return new Result(ResultStatus.Unavailable) { Errors = errorMessages}; + } } } diff --git a/src/Ardalis.Result/ResultExtensions.cs b/src/Ardalis.Result/ResultExtensions.cs index 4643ce1..6a27793 100644 --- a/src/Ardalis.Result/ResultExtensions.cs +++ b/src/Ardalis.Result/ResultExtensions.cs @@ -30,6 +30,7 @@ public static Result Map(this Result.Conflict(result.Errors.ToArray()) : Result.Conflict(); case ResultStatus.CriticalError: return Result.CriticalError(result.Errors.ToArray()); + case ResultStatus.Unavailable: return Result.Unavailable(result.Errors.ToArray()); default: throw new NotSupportedException($"Result {result.Status} conversion is not supported."); } diff --git a/src/Ardalis.Result/ResultStatus.cs b/src/Ardalis.Result/ResultStatus.cs index 57e2750..750a06e 100644 --- a/src/Ardalis.Result/ResultStatus.cs +++ b/src/Ardalis.Result/ResultStatus.cs @@ -9,6 +9,7 @@ public enum ResultStatus Invalid, NotFound, Conflict, - CriticalError + CriticalError, + Unavailable } } diff --git a/tests/Ardalis.Result.AspNetCore.UnitTests/ResultConventionDefaultResultStatusMap.cs b/tests/Ardalis.Result.AspNetCore.UnitTests/ResultConventionDefaultResultStatusMap.cs index d06c2c4..dfc847d 100644 --- a/tests/Ardalis.Result.AspNetCore.UnitTests/ResultConventionDefaultResultStatusMap.cs +++ b/tests/Ardalis.Result.AspNetCore.UnitTests/ResultConventionDefaultResultStatusMap.cs @@ -18,7 +18,7 @@ public void TranslateAttributeOnAction() convention.Apply(actionModel); - Assert.Equal(8, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); + Assert.Equal(9, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 204, typeof(void))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 404, typeof(ProblemDetails))); @@ -28,6 +28,7 @@ public void TranslateAttributeOnAction() Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 409, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 422, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 500, typeof(ProblemDetails))); + Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 503, typeof(ProblemDetails))); } [Fact] @@ -42,7 +43,7 @@ public void TranslateAttributeOnController() convention.Apply(actionModel); - Assert.Equal(8, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); + Assert.Equal(9, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 204, typeof(void))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 404, typeof(ProblemDetails))); @@ -52,6 +53,7 @@ public void TranslateAttributeOnController() Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 409, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 422, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 500, typeof(ProblemDetails))); + Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 503, typeof(ProblemDetails))); } [Fact] @@ -81,7 +83,7 @@ public void ExistingProducesResponseTypeAttributePreserved() convention.Apply(actionModel); - Assert.Equal(8, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); + Assert.Equal(9, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 204, typeof(void))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 404, typeof(ProblemDetails))); @@ -91,6 +93,7 @@ public void ExistingProducesResponseTypeAttributePreserved() Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 409, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 422, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 500, typeof(ProblemDetails))); + Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 503, typeof(ProblemDetails))); } [Theory] @@ -107,7 +110,7 @@ public void ResultWithValue(string actionName, Type expectedType) convention.Apply(actionModel); - Assert.Equal(8, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); + Assert.Equal(9, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 200, expectedType)); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 404, typeof(ProblemDetails))); @@ -117,5 +120,6 @@ public void ResultWithValue(string actionName, Type expectedType) Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 409, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 422, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 500, typeof(ProblemDetails))); + Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 503, typeof(ProblemDetails))); } } diff --git a/tests/Ardalis.Result.AspNetCore.UnitTests/ResultConventionDefaultResultStatusMapModified.cs b/tests/Ardalis.Result.AspNetCore.UnitTests/ResultConventionDefaultResultStatusMapModified.cs index 8516d22..c81376e 100644 --- a/tests/Ardalis.Result.AspNetCore.UnitTests/ResultConventionDefaultResultStatusMapModified.cs +++ b/tests/Ardalis.Result.AspNetCore.UnitTests/ResultConventionDefaultResultStatusMapModified.cs @@ -21,7 +21,7 @@ public void RemoveResultStatus() convention.Apply(actionModel); - Assert.Equal(6, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); + Assert.Equal(7, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 204, typeof(void))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 404, typeof(ProblemDetails))); @@ -29,6 +29,7 @@ public void RemoveResultStatus() Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 409, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 422, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 500, typeof(ProblemDetails))); + Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 503, typeof(ProblemDetails))); } [Fact] @@ -45,7 +46,7 @@ public void ChangeResultStatus() convention.Apply(actionModel); - Assert.Equal(7, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); + Assert.Equal(8, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 204, typeof(void))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 404, typeof(ProblemDetails))); @@ -54,6 +55,7 @@ public void ChangeResultStatus() Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 403, typeof(void))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 409, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 500, typeof(void))); + Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 503, typeof(ProblemDetails))); } [Theory] @@ -82,7 +84,7 @@ public void ChangeResultStatus_ForSpecificMethods(string actionName, Type expect convention.Apply(actionModel); - Assert.Equal(8, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); + Assert.Equal(9, actionModel.Filters.Where(f => f is ProducesResponseTypeAttribute).Count()); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, expectedStatusCode, expectedType)); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 404, typeof(ProblemDetails))); @@ -92,5 +94,6 @@ public void ChangeResultStatus_ForSpecificMethods(string actionName, Type expect Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 409, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 422, typeof(ProblemDetails))); Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 500, typeof(ProblemDetails))); + Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 503, typeof(ProblemDetails))); } } diff --git a/tests/Ardalis.Result.UnitTests/PagedResultConstructor.cs b/tests/Ardalis.Result.UnitTests/PagedResultConstructor.cs index 6075fcd..c3ffa2e 100644 --- a/tests/Ardalis.Result.UnitTests/PagedResultConstructor.cs +++ b/tests/Ardalis.Result.UnitTests/PagedResultConstructor.cs @@ -151,4 +151,15 @@ public void InitializesStatusToForbiddenGivenForbiddenFactoryCall() Assert.Equal(ResultStatus.Forbidden, result.Status); Assert.Equal(_pagedInfo, result.PagedInfo); } + + [Fact] + public void InitializesStatusToUnavailableGivenUnavailableFactoryCall() + { + var result = Result + .Unavailable() + .ToPagedResult(_pagedInfo); + + Assert.Equal(ResultStatus.Unavailable, result.Status); + Assert.Equal(_pagedInfo, result.PagedInfo); + } } diff --git a/tests/Ardalis.Result.UnitTests/ResultConstructor.cs b/tests/Ardalis.Result.UnitTests/ResultConstructor.cs index deaed88..f695c56 100644 --- a/tests/Ardalis.Result.UnitTests/ResultConstructor.cs +++ b/tests/Ardalis.Result.UnitTests/ResultConstructor.cs @@ -183,6 +183,17 @@ public void InitializesStatusToForbiddenGivenForbiddenFactoryCall() Assert.Equal(ResultStatus.Forbidden, result.Status); } + [Fact] + public void InitializesStatusToUnavailableGivenUnavailableFactoryCallWithString() + { + var errorMessage = "Service Unavailable"; + var result = Result.Unavailable(errorMessage); + + Assert.Equal(ResultStatus.Unavailable, result.Status); + Assert.Equal(errorMessage, result.Errors.First()); + } + + [Fact] public void InitializedIsSuccessTrueForSuccessFactoryCall() { diff --git a/tests/Ardalis.Result.UnitTests/ResultMap.cs b/tests/Ardalis.Result.UnitTests/ResultMap.cs index 7552f8c..696c2ff 100644 --- a/tests/Ardalis.Result.UnitTests/ResultMap.cs +++ b/tests/Ardalis.Result.UnitTests/ResultMap.cs @@ -164,6 +164,18 @@ public void ShouldProduceConflictWithError() actual.Errors.Single().Should().Be(expectedMessage); } + [Fact] + public void ShouldProduceUnavailableWithError() + { + string expectedMessage = "Something unavailable"; + var result = Result.Unavailable(expectedMessage); + + var actual = result.Map(val => val.ToString()); + + actual.Status.Should().Be(ResultStatus.Unavailable); + actual.Errors.Single().Should().Be(expectedMessage); + } + [Fact] public void ShouldProduceCriticalErrorWithError() { diff --git a/tests/Ardalis.Result.UnitTests/ResultVoidConstructor.cs b/tests/Ardalis.Result.UnitTests/ResultVoidConstructor.cs index 7ca58d5..8a72a68 100644 --- a/tests/Ardalis.Result.UnitTests/ResultVoidConstructor.cs +++ b/tests/Ardalis.Result.UnitTests/ResultVoidConstructor.cs @@ -172,6 +172,17 @@ public void InitializesConflictResultWithFactoryMethodWithErrors() result.Errors.Single().Should().Be(errorMessage); } + [Fact] + public void InitializeUnavailableResultWithFactoryMethodWithErrors() + { + var errorMessage = "Something unavailable"; + var result = Result.Unavailable(errorMessage); + + result.Value.Should().BeNull(); + result.Status.Should().Be(ResultStatus.Unavailable); + result.Errors.Single().Should().Be(errorMessage); + } + [Fact] public void InitializesCriticalErrorResultWithFactoryMethodWithErrors() { diff --git a/tests/Ardalis.Result.UnitTests/ResultVoidMap.cs b/tests/Ardalis.Result.UnitTests/ResultVoidMap.cs index 7464400..16febdc 100644 --- a/tests/Ardalis.Result.UnitTests/ResultVoidMap.cs +++ b/tests/Ardalis.Result.UnitTests/ResultVoidMap.cs @@ -93,6 +93,16 @@ public void ShouldProduceConflict() actual.Value.Should().BeNull(); } + [Fact] + public void ShouldProduceUnavailable() + { + var result = Result.Unavailable(); + + var actual = result.Map(_ => "This should be ignored"); + + actual.Status.Should().Be(ResultStatus.Unavailable); + } + [Fact] public void ShouldProduceCriticalError() { diff --git a/tests/Ardalis.Result.UnitTests/ResultVoidToResultOfT.cs b/tests/Ardalis.Result.UnitTests/ResultVoidToResultOfT.cs index 520428a..a6a155a 100644 --- a/tests/Ardalis.Result.UnitTests/ResultVoidToResultOfT.cs +++ b/tests/Ardalis.Result.UnitTests/ResultVoidToResultOfT.cs @@ -103,6 +103,15 @@ public void ConvertFromConflictResultOfUnit() result.Value.Should().BeNull(); } + [Fact] + public void ConvertFromUnavailableResultOfUnit() + { + var result = DoBusinessOperationExample(Result.Unavailable()); + + result.Status.Should().Be(ResultStatus.Unavailable); + result.Value.Should().BeNull(); + } + [Fact] public void ConvertFromCriticalErrorResultOfUnit() {