Skip to content

Commit

Permalink
Add Unavailable result status (#146)
Browse files Browse the repository at this point in the history
* Add Unavailable result status

* Add Unavailable to AspNetCore tests

* Fix Merge issues
  • Loading branch information
Dorian Green authored Sep 22, 2023
1 parent 9574916 commit c9adeff
Show file tree
Hide file tree
Showing 14 changed files with 143 additions and 9 deletions.
22 changes: 22 additions & 0 deletions src/Ardalis.Result.AspNetCore/MinimalApiResultExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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."),
};
Expand Down Expand Up @@ -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
18 changes: 17 additions & 1 deletion src/Ardalis.Result.AspNetCore/ResultStatusMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/// <summary>
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/Ardalis.Result/Result.Void.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ public static Result ErrorWithCorrelationId(string correlationId, params string[
}

/// <summary>
/// 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
/// </summary>
/// <param name="errorMessages">A list of string error messages</param>
/// <returns></returns>
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
Expand Down
12 changes: 12 additions & 0 deletions src/Ardalis.Result/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,5 +206,17 @@ public static Result<T> CriticalError(params string[] errorMessages)
{
return new Result<T>(ResultStatus.CriticalError) { Errors = errorMessages };
}

/// <summary>
/// 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
/// </summary>
/// <param name="errorMessages">A list of string error messages</param>
/// <returns></returns>
public static Result<T> Unavailable(params string[] errorMessages)
{
return new Result<T>(ResultStatus.Unavailable) { Errors = errorMessages};
}
}
}
1 change: 1 addition & 0 deletions src/Ardalis.Result/ResultExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static Result<TDestination> Map<TSource, TDestination>(this Result<TSourc
? Result<TDestination>.Conflict(result.Errors.ToArray())
: Result<TDestination>.Conflict();
case ResultStatus.CriticalError: return Result<TDestination>.CriticalError(result.Errors.ToArray());
case ResultStatus.Unavailable: return Result<TDestination>.Unavailable(result.Errors.ToArray());
default:
throw new NotSupportedException($"Result {result.Status} conversion is not supported.");
}
Expand Down
3 changes: 2 additions & 1 deletion src/Ardalis.Result/ResultStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum ResultStatus
Invalid,
NotFound,
Conflict,
CriticalError
CriticalError,
Unavailable
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand All @@ -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]
Expand All @@ -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)));
Expand All @@ -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]
Expand Down Expand Up @@ -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)));
Expand All @@ -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]
Expand All @@ -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)));
Expand All @@ -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)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ 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)));
Assert.Contains(actionModel.Filters, f => ProducesResponseTypeAttribute(f, 400, typeof(ValidationProblemDetails)));
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]
Expand All @@ -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)));
Expand All @@ -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]
Expand Down Expand Up @@ -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)));
Expand All @@ -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)));
}
}
11 changes: 11 additions & 0 deletions tests/Ardalis.Result.UnitTests/PagedResultConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>
.Unavailable()
.ToPagedResult(_pagedInfo);

Assert.Equal(ResultStatus.Unavailable, result.Status);
Assert.Equal(_pagedInfo, result.PagedInfo);
}
}
11 changes: 11 additions & 0 deletions tests/Ardalis.Result.UnitTests/ResultConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,17 @@ public void InitializesStatusToForbiddenGivenForbiddenFactoryCall()
Assert.Equal(ResultStatus.Forbidden, result.Status);
}

[Fact]
public void InitializesStatusToUnavailableGivenUnavailableFactoryCallWithString()
{
var errorMessage = "Service Unavailable";
var result = Result<object>.Unavailable(errorMessage);

Assert.Equal(ResultStatus.Unavailable, result.Status);
Assert.Equal(errorMessage, result.Errors.First());
}


[Fact]
public void InitializedIsSuccessTrueForSuccessFactoryCall()
{
Expand Down
12 changes: 12 additions & 0 deletions tests/Ardalis.Result.UnitTests/ResultMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ public void ShouldProduceConflictWithError()
actual.Errors.Single().Should().Be(expectedMessage);
}

[Fact]
public void ShouldProduceUnavailableWithError()
{
string expectedMessage = "Something unavailable";
var result = Result<int>.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()
{
Expand Down
11 changes: 11 additions & 0 deletions tests/Ardalis.Result.UnitTests/ResultVoidConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
10 changes: 10 additions & 0 deletions tests/Ardalis.Result.UnitTests/ResultVoidMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
9 changes: 9 additions & 0 deletions tests/Ardalis.Result.UnitTests/ResultVoidToResultOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ public void ConvertFromConflictResultOfUnit()
result.Value.Should().BeNull();
}

[Fact]
public void ConvertFromUnavailableResultOfUnit()
{
var result = DoBusinessOperationExample<object>(Result.Unavailable());

result.Status.Should().Be(ResultStatus.Unavailable);
result.Value.Should().BeNull();
}

[Fact]
public void ConvertFromCriticalErrorResultOfUnit()
{
Expand Down

0 comments on commit c9adeff

Please sign in to comment.