From 8a753419f1a4a3811efad64e1834ecc5d0ee6ce7 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 11:28:46 -0500 Subject: [PATCH 01/17] chore: Add new resources --- .../Resources/ResponseMessages.Designer.cs | 18 ++++++++++++++++++ src/Core/Resources/ResponseMessages.es.resx | 6 ++++++ src/Core/Resources/ResponseMessages.resx | 6 ++++++ 3 files changed, 30 insertions(+) diff --git a/src/Core/Resources/ResponseMessages.Designer.cs b/src/Core/Resources/ResponseMessages.Designer.cs index b8d79e4..c970c45 100644 --- a/src/Core/Resources/ResponseMessages.Designer.cs +++ b/src/Core/Resources/ResponseMessages.Designer.cs @@ -114,6 +114,15 @@ internal static string Error { } } + /// + /// Looks up a localized string similar to Failed conversion. Unable to translate result object into HTTP status code. + /// + internal static string FailedConversion { + get { + return ResourceManager.GetString("FailedConversion", resourceCulture); + } + } + /// /// Looks up a localized string similar to An error occurred during the execution of a service. /// @@ -123,6 +132,15 @@ internal static string Failure { } } + /// + /// Looks up a localized string similar to File content has been returned successfully. + /// + internal static string FileContent { + get { + return ResourceManager.GetString("FileContent", resourceCulture); + } + } + /// /// Looks up a localized string similar to You do not have permissions to perform this action. /// diff --git a/src/Core/Resources/ResponseMessages.es.resx b/src/Core/Resources/ResponseMessages.es.resx index 1b1cf83..459a962 100644 --- a/src/Core/Resources/ResponseMessages.es.resx +++ b/src/Core/Resources/ResponseMessages.es.resx @@ -135,9 +135,15 @@ Error + + Conversión fallida. No se ha podido convertir el objeto resultante en código de estado HTTP + Se ha producido un error durante la ejecución de un servicio + + El contenido del archivo se ha devuelto correctamente + No tienes permisos para realizar esta acción diff --git a/src/Core/Resources/ResponseMessages.resx b/src/Core/Resources/ResponseMessages.resx index 1eeb07a..af968f1 100644 --- a/src/Core/Resources/ResponseMessages.resx +++ b/src/Core/Resources/ResponseMessages.resx @@ -135,9 +135,15 @@ Error + + Failed conversion. Unable to translate result object into HTTP status code + An error occurred during the execution of a service + + File content has been returned successfully + You do not have permissions to perform this action From 732f11b69bf4c64d8d9a862bc9388ad28a4b6a4a Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 11:29:54 -0500 Subject: [PATCH 02/17] feat: Add two result operations to represent the file contents --- src/Core/Result.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Core/Result.cs b/src/Core/Result.cs index 8cfa790..26deff9 100644 --- a/src/Core/Result.cs +++ b/src/Core/Result.cs @@ -145,4 +145,28 @@ public static Result ObtainedResource(T data) /// A set of data. public static ListedResult ObtainedResources(IEnumerable data) => Success(data, ResponseMessages.ObtainedResources); + + /// + /// Represents a situation in which the service returns the contents of a file as an array of bytes. + /// + /// The contents of a file. + public static Result File(ByteArrayContent fileContent) => new() + { + Data = fileContent, + IsSuccess = true, + Message = ResponseMessages.FileContent, + Status = ResultStatus.ByteArrayFile + }; + + /// + /// Represents a situation in which the service returns the contents of a file as a stream. + /// + /// The contents of a file. + public static Result File(StreamFileContent fileContent) => new() + { + Data = fileContent, + IsSuccess = true, + Message = ResponseMessages.FileContent, + Status = ResultStatus.StreamFile + }; } From a71fa691a2b122566817421bcc62436412396047 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 11:32:11 -0500 Subject: [PATCH 03/17] feat: Add two result states to represent the file contents --- src/Core/ResultStatus.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Core/ResultStatus.cs b/src/Core/ResultStatus.cs index 13fe177..79ba81e 100644 --- a/src/Core/ResultStatus.cs +++ b/src/Core/ResultStatus.cs @@ -41,5 +41,13 @@ public enum ResultStatus /// /// Represents a status where the user does not have permission to perform some action. /// - Forbidden + Forbidden, + /// + /// Represents a status where the service returns the contents of a file as an array of bytes. + /// + ByteArrayFile, + /// + /// Represents a status where the service returns the contents of a file as a stream. + /// + StreamFile } From b50e74686dd73b1ea4a6b9dd1a321a75937fa22d Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 11:32:37 -0500 Subject: [PATCH 04/17] feat: Add types to represent the contents of a file --- src/Core/FileContent.cs | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/Core/FileContent.cs diff --git a/src/Core/FileContent.cs b/src/Core/FileContent.cs new file mode 100644 index 0000000..7e1487c --- /dev/null +++ b/src/Core/FileContent.cs @@ -0,0 +1,59 @@ +using System.IO; + +namespace SimpleResults; + +/// +/// Represents the content of a file using a stream. +/// +public class StreamFileContent +{ + /// + /// Initializes a new instance of the class + /// with the provided . + /// + /// The stream that represent the file contents. + public StreamFileContent(Stream content) => Content = content; + + /// + /// Gets the stream that represent the file contents. + /// + public Stream Content { get; } + + /// + /// Gets or sets the content type. Its default value is an empty string. + /// + public string ContentType { get; init; } = string.Empty; + + /// + /// Gets or sets the file name. Its default value is an empty string. + /// + public string FileName { get; init; } = string.Empty; +} + +/// +/// Represents the content of a file using an array of bytes. +/// +public class ByteArrayFileContent +{ + /// + /// Initializes a new instance of the class + /// with the provided . + /// + /// The bytes that represent the file contents. + public ByteArrayFileContent(byte[] content) => Content = content; + + /// + /// Gets the bytes that represent the file contents. + /// + public byte[] Content { get; } + + /// + /// Gets or sets the content type. Its default value is an empty string. + /// + public string ContentType { get; init; } = string.Empty; + + /// + /// Gets or sets the file name. Its default value is an empty string. + /// + public string FileName { get; init; } = string.Empty; +} From fcfde629c6632f95070b4563cec6d5eaa7391cbc Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 11:36:07 -0500 Subject: [PATCH 05/17] feat: Translate the result object to a FileResult --- src/AspNetCore/FileResultFactory.cs | 61 +++++++++++++++++++++++++++++ src/AspNetCore/ResultExtensions.cs | 4 ++ 2 files changed, 65 insertions(+) create mode 100644 src/AspNetCore/FileResultFactory.cs diff --git a/src/AspNetCore/FileResultFactory.cs b/src/AspNetCore/FileResultFactory.cs new file mode 100644 index 0000000..c2536cb --- /dev/null +++ b/src/AspNetCore/FileResultFactory.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Mvc; +using SimpleResults.Resources; + +namespace SimpleResults; + +internal class FileResultFactory +{ + public static FileContentResult CreateFileContentResult(ResultBase resultBase) + { + var result = resultBase as Result ?? + throw new InvalidOperationException(ResponseMessages.FailedConversion); + + var byteArrayFile = result.Data; + var fileContent = new FileContentResult(byteArrayFile.Content, byteArrayFile.ContentType) + { + FileDownloadName = byteArrayFile.FileName + }; + return fileContent; + } + + public static FileStreamResult CreateFileStreamResult(ResultBase resultBase) + { + var result = resultBase as Result ?? + throw new InvalidOperationException(ResponseMessages.FailedConversion); + + var streamFile = result.Data; + var fileContent = new FileStreamResult(streamFile.Content, streamFile.ContentType) + { + FileDownloadName = streamFile.FileName + }; + return fileContent; + } + + public static IResult CreateFileContentHttpResult(ResultBase resultBase) + { + var result = resultBase as Result ?? + throw new InvalidOperationException(ResponseMessages.FailedConversion); + + var byteArrayFile = result.Data; + var fileContent = Results.File( + byteArrayFile.Content, + byteArrayFile.ContentType, + byteArrayFile.FileName); + + return fileContent; + } + + public static IResult CreateFileStreamHttpResult(ResultBase resultBase) + { + var result = resultBase as Result ?? + throw new InvalidOperationException(ResponseMessages.FailedConversion); + + var streamFile = result.Data; + var fileContent = Results.File( + streamFile.Content, + streamFile.ContentType, + streamFile.FileName); + + return fileContent; + } +} diff --git a/src/AspNetCore/ResultExtensions.cs b/src/AspNetCore/ResultExtensions.cs index f2611cb..b32bce0 100644 --- a/src/AspNetCore/ResultExtensions.cs +++ b/src/AspNetCore/ResultExtensions.cs @@ -90,6 +90,8 @@ public static ActionResult ToActionResult(this Result result) ResultStatus.Failure => new UnprocessableContentResult(result), ResultStatus.CriticalError => new InternalServerErrorResult(result), ResultStatus.Forbidden => new ForbiddenResult(result), + ResultStatus.ByteArrayFile => FileResultFactory.CreateFileContentResult(result), + ResultStatus.StreamFile => FileResultFactory.CreateFileStreamResult(result), _ => throw new NotSupportedException(string.Format(ResponseMessages.UnsupportedStatus, result.Status)) }; @@ -151,6 +153,8 @@ public static IResult ToHttpResult(this Result result) ResultStatus.Failure => Results.UnprocessableEntity(result), ResultStatus.CriticalError => new InternalServerErrorHttpResult(result), ResultStatus.Forbidden => new ForbiddenHttpResult(result), + ResultStatus.ByteArrayFile => FileResultFactory.CreateFileContentHttpResult(result), + ResultStatus.StreamFile => FileResultFactory.CreateFileStreamHttpResult(result), _ => throw new NotSupportedException(string.Format(ResponseMessages.UnsupportedStatus, result.Status)) }; } From e07654721950442aed2084caa43b635776c6c39a Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 14:49:28 -0500 Subject: [PATCH 06/17] refactor: Change type to ByteArrayFileContent --- src/Core/Result.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Result.cs b/src/Core/Result.cs index 26deff9..fcf52af 100644 --- a/src/Core/Result.cs +++ b/src/Core/Result.cs @@ -150,7 +150,7 @@ public static ListedResult ObtainedResources(IEnumerable data) /// Represents a situation in which the service returns the contents of a file as an array of bytes. /// /// The contents of a file. - public static Result File(ByteArrayContent fileContent) => new() + public static Result File(ByteArrayFileContent fileContent) => new() { Data = fileContent, IsSuccess = true, From dad05694a5b0ced073733b0f860f5c46ed2160e9 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 16:51:41 -0500 Subject: [PATCH 07/17] chore: Update resource --- src/Core/Resources/ResponseMessages.Designer.cs | 2 +- src/Core/Resources/ResponseMessages.es.resx | 2 +- src/Core/Resources/ResponseMessages.resx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core/Resources/ResponseMessages.Designer.cs b/src/Core/Resources/ResponseMessages.Designer.cs index c970c45..b6abbf0 100644 --- a/src/Core/Resources/ResponseMessages.Designer.cs +++ b/src/Core/Resources/ResponseMessages.Designer.cs @@ -115,7 +115,7 @@ internal static string Error { } /// - /// Looks up a localized string similar to Failed conversion. Unable to translate result object into HTTP status code. + /// Looks up a localized string similar to Conversion failed. Failed to convert the result object to an object of type '{0}'. /// internal static string FailedConversion { get { diff --git a/src/Core/Resources/ResponseMessages.es.resx b/src/Core/Resources/ResponseMessages.es.resx index 459a962..67f2220 100644 --- a/src/Core/Resources/ResponseMessages.es.resx +++ b/src/Core/Resources/ResponseMessages.es.resx @@ -136,7 +136,7 @@ Error - Conversión fallida. No se ha podido convertir el objeto resultante en código de estado HTTP + Conversión fallida. No se ha podido convertir el objeto de resultado a un objeto de tipo '{0}' Se ha producido un error durante la ejecución de un servicio diff --git a/src/Core/Resources/ResponseMessages.resx b/src/Core/Resources/ResponseMessages.resx index af968f1..c81aecb 100644 --- a/src/Core/Resources/ResponseMessages.resx +++ b/src/Core/Resources/ResponseMessages.resx @@ -136,7 +136,7 @@ Error - Failed conversion. Unable to translate result object into HTTP status code + Conversion failed. Failed to convert the result object to an object of type '{0}' An error occurred during the execution of a service From 54fa5c7a7f995825031e25ea417b79ae0c1b5321 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 16:53:13 -0500 Subject: [PATCH 08/17] refactor: Add FailedConversionError type --- src/AspNetCore/FileResultFactory.cs | 33 ++++++++++++++----- .../Reasons/FailedConversionError.cs | 10 ++++++ 2 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 src/AspNetCore/Reasons/FailedConversionError.cs diff --git a/src/AspNetCore/FileResultFactory.cs b/src/AspNetCore/FileResultFactory.cs index c2536cb..340bb53 100644 --- a/src/AspNetCore/FileResultFactory.cs +++ b/src/AspNetCore/FileResultFactory.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc; -using SimpleResults.Resources; namespace SimpleResults; @@ -7,8 +6,12 @@ internal class FileResultFactory { public static FileContentResult CreateFileContentResult(ResultBase resultBase) { - var result = resultBase as Result ?? - throw new InvalidOperationException(ResponseMessages.FailedConversion); + var result = resultBase as Result; + if (result is null) + { + var typeName = typeof(FileContentResult).FullName; + throw new InvalidOperationException(new FailedConversionError(typeName).Message); + } var byteArrayFile = result.Data; var fileContent = new FileContentResult(byteArrayFile.Content, byteArrayFile.ContentType) @@ -20,8 +23,12 @@ public static FileContentResult CreateFileContentResult(ResultBase resultBase) public static FileStreamResult CreateFileStreamResult(ResultBase resultBase) { - var result = resultBase as Result ?? - throw new InvalidOperationException(ResponseMessages.FailedConversion); + var result = resultBase as Result; + if (result is null) + { + var typeName = typeof(FileStreamResult).FullName; + throw new InvalidOperationException(new FailedConversionError(typeName).Message); + } var streamFile = result.Data; var fileContent = new FileStreamResult(streamFile.Content, streamFile.ContentType) @@ -33,8 +40,12 @@ public static FileStreamResult CreateFileStreamResult(ResultBase resultBase) public static IResult CreateFileContentHttpResult(ResultBase resultBase) { - var result = resultBase as Result ?? - throw new InvalidOperationException(ResponseMessages.FailedConversion); + var result = resultBase as Result; + if (result is null) + { + var typeName = typeof(IResult).FullName; + throw new InvalidOperationException(new FailedConversionError(typeName).Message); + } var byteArrayFile = result.Data; var fileContent = Results.File( @@ -47,8 +58,12 @@ public static IResult CreateFileContentHttpResult(ResultBase resultBase) public static IResult CreateFileStreamHttpResult(ResultBase resultBase) { - var result = resultBase as Result ?? - throw new InvalidOperationException(ResponseMessages.FailedConversion); + var result = resultBase as Result; + if (result is null) + { + var typeName = typeof(IResult).FullName; + throw new InvalidOperationException(new FailedConversionError(typeName).Message); + } var streamFile = result.Data; var fileContent = Results.File( diff --git a/src/AspNetCore/Reasons/FailedConversionError.cs b/src/AspNetCore/Reasons/FailedConversionError.cs new file mode 100644 index 0000000..ad1abcc --- /dev/null +++ b/src/AspNetCore/Reasons/FailedConversionError.cs @@ -0,0 +1,10 @@ +using SimpleResults.Resources; + +namespace SimpleResults; + +internal readonly ref struct FailedConversionError +{ + public string Message { get; } + public FailedConversionError(string typeName) + => Message = string.Format(ResponseMessages.FailedConversion, typeName ?? string.Empty); +} From 75db9c6a69c140e9a82babfe262ab2ed73999b19 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 16:57:02 -0500 Subject: [PATCH 09/17] cleanup: Remove file --- src/AspNetCore/FileResultFactory.cs | 76 ----------------------------- 1 file changed, 76 deletions(-) delete mode 100644 src/AspNetCore/FileResultFactory.cs diff --git a/src/AspNetCore/FileResultFactory.cs b/src/AspNetCore/FileResultFactory.cs deleted file mode 100644 index 340bb53..0000000 --- a/src/AspNetCore/FileResultFactory.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace SimpleResults; - -internal class FileResultFactory -{ - public static FileContentResult CreateFileContentResult(ResultBase resultBase) - { - var result = resultBase as Result; - if (result is null) - { - var typeName = typeof(FileContentResult).FullName; - throw new InvalidOperationException(new FailedConversionError(typeName).Message); - } - - var byteArrayFile = result.Data; - var fileContent = new FileContentResult(byteArrayFile.Content, byteArrayFile.ContentType) - { - FileDownloadName = byteArrayFile.FileName - }; - return fileContent; - } - - public static FileStreamResult CreateFileStreamResult(ResultBase resultBase) - { - var result = resultBase as Result; - if (result is null) - { - var typeName = typeof(FileStreamResult).FullName; - throw new InvalidOperationException(new FailedConversionError(typeName).Message); - } - - var streamFile = result.Data; - var fileContent = new FileStreamResult(streamFile.Content, streamFile.ContentType) - { - FileDownloadName = streamFile.FileName - }; - return fileContent; - } - - public static IResult CreateFileContentHttpResult(ResultBase resultBase) - { - var result = resultBase as Result; - if (result is null) - { - var typeName = typeof(IResult).FullName; - throw new InvalidOperationException(new FailedConversionError(typeName).Message); - } - - var byteArrayFile = result.Data; - var fileContent = Results.File( - byteArrayFile.Content, - byteArrayFile.ContentType, - byteArrayFile.FileName); - - return fileContent; - } - - public static IResult CreateFileStreamHttpResult(ResultBase resultBase) - { - var result = resultBase as Result; - if (result is null) - { - var typeName = typeof(IResult).FullName; - throw new InvalidOperationException(new FailedConversionError(typeName).Message); - } - - var streamFile = result.Data; - var fileContent = Results.File( - streamFile.Content, - streamFile.ContentType, - streamFile.FileName); - - return fileContent; - } -} From e4f8e00f32071b1df2984fbca5cfc40006c0c2c8 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 16:57:56 -0500 Subject: [PATCH 10/17] feat: Add file result converter --- src/AspNetCore/FileResultConverter.cs | 76 +++++++++++++++++++++++++++ src/AspNetCore/ResultExtensions.cs | 8 +-- 2 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 src/AspNetCore/FileResultConverter.cs diff --git a/src/AspNetCore/FileResultConverter.cs b/src/AspNetCore/FileResultConverter.cs new file mode 100644 index 0000000..4cd7057 --- /dev/null +++ b/src/AspNetCore/FileResultConverter.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Mvc; + +namespace SimpleResults; + +internal class FileResultConverter +{ + public static FileContentResult ConvertToFileContentResult(ResultBase resultBase) + { + var result = resultBase as Result; + if (result is null) + { + var typeName = typeof(FileContentResult).FullName; + throw new InvalidOperationException(new FailedConversionError(typeName).Message); + } + + var byteArrayFile = result.Data; + var fileContent = new FileContentResult(byteArrayFile.Content, byteArrayFile.ContentType) + { + FileDownloadName = byteArrayFile.FileName + }; + return fileContent; + } + + public static FileStreamResult ConvertToFileStreamResult(ResultBase resultBase) + { + var result = resultBase as Result; + if (result is null) + { + var typeName = typeof(FileStreamResult).FullName; + throw new InvalidOperationException(new FailedConversionError(typeName).Message); + } + + var streamFile = result.Data; + var fileContent = new FileStreamResult(streamFile.Content, streamFile.ContentType) + { + FileDownloadName = streamFile.FileName + }; + return fileContent; + } + + public static IResult ConvertToFileContentHttpResult(ResultBase resultBase) + { + var result = resultBase as Result; + if (result is null) + { + var typeName = typeof(IResult).FullName; + throw new InvalidOperationException(new FailedConversionError(typeName).Message); + } + + var byteArrayFile = result.Data; + var fileContent = Results.File( + byteArrayFile.Content, + byteArrayFile.ContentType, + byteArrayFile.FileName); + + return fileContent; + } + + public static IResult ConvertToFileStreamHttpResult(ResultBase resultBase) + { + var result = resultBase as Result; + if (result is null) + { + var typeName = typeof(IResult).FullName; + throw new InvalidOperationException(new FailedConversionError(typeName).Message); + } + + var streamFile = result.Data; + var fileContent = Results.File( + streamFile.Content, + streamFile.ContentType, + streamFile.FileName); + + return fileContent; + } +} diff --git a/src/AspNetCore/ResultExtensions.cs b/src/AspNetCore/ResultExtensions.cs index b32bce0..9918efe 100644 --- a/src/AspNetCore/ResultExtensions.cs +++ b/src/AspNetCore/ResultExtensions.cs @@ -90,8 +90,8 @@ public static ActionResult ToActionResult(this Result result) ResultStatus.Failure => new UnprocessableContentResult(result), ResultStatus.CriticalError => new InternalServerErrorResult(result), ResultStatus.Forbidden => new ForbiddenResult(result), - ResultStatus.ByteArrayFile => FileResultFactory.CreateFileContentResult(result), - ResultStatus.StreamFile => FileResultFactory.CreateFileStreamResult(result), + ResultStatus.ByteArrayFile => FileResultConverter.ConvertToFileContentResult(result), + ResultStatus.StreamFile => FileResultConverter.ConvertToFileStreamResult(result), _ => throw new NotSupportedException(string.Format(ResponseMessages.UnsupportedStatus, result.Status)) }; @@ -153,8 +153,8 @@ public static IResult ToHttpResult(this Result result) ResultStatus.Failure => Results.UnprocessableEntity(result), ResultStatus.CriticalError => new InternalServerErrorHttpResult(result), ResultStatus.Forbidden => new ForbiddenHttpResult(result), - ResultStatus.ByteArrayFile => FileResultFactory.CreateFileContentHttpResult(result), - ResultStatus.StreamFile => FileResultFactory.CreateFileStreamHttpResult(result), + ResultStatus.ByteArrayFile => FileResultConverter.ConvertToFileContentHttpResult(result), + ResultStatus.StreamFile => FileResultConverter.ConvertToFileStreamHttpResult(result), _ => throw new NotSupportedException(string.Format(ResponseMessages.UnsupportedStatus, result.Status)) }; } From c471b141fdfd7d94e8d4c88909935cce82af9f26 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Thu, 29 Feb 2024 16:59:48 -0500 Subject: [PATCH 11/17] test: Add unit tests related to file contents --- .../ToActionResultTests.FileResult.cs | 98 +++++++++++++++++++ .../ToHttpResultTests.FileHttpResult.cs | 79 +++++++++++++++ tests/Core/ResultTests.cs | 49 ++++++++++ 3 files changed, 226 insertions(+) create mode 100644 tests/AspNetCore/ToActionResultTests.FileResult.cs create mode 100644 tests/AspNetCore/ToHttpResultTests.FileHttpResult.cs diff --git a/tests/AspNetCore/ToActionResultTests.FileResult.cs b/tests/AspNetCore/ToActionResultTests.FileResult.cs new file mode 100644 index 0000000..280362e --- /dev/null +++ b/tests/AspNetCore/ToActionResultTests.FileResult.cs @@ -0,0 +1,98 @@ +namespace SimpleResults.Tests.AspNetCore; + +public partial class ToActionResultTests +{ + [Test] + public void ToActionResult_WhenOperationResultIsByteArrayFileContent_ShouldReturnsFileContentResult() + { + // Arrange + byte[] content = [1, 0, 1, 1]; + var expectedData = new ByteArrayFileContent(content) + { + ContentType = "application/pdf", + FileName = "Report.pdf" + }; + Result result = Result.File(expectedData); + + // Act + ActionResult> actionResult = result.ToActionResult(); + var contentResult = actionResult.Result as FileContentResult; + + // Asserts + contentResult.FileContents.Should().BeEquivalentTo(expectedData.Content); + contentResult.ContentType.Should().Be(expectedData.ContentType); + contentResult.FileDownloadName.Should().Be(expectedData.FileName); + } + + [Test] + public void ToActionResult_WhenOperationResultIsStreamFileContent_ShouldReturnsFileStreamResult() + { + // Arrange + byte[] expectedBuffer = [1, 1, 0, 1]; + Stream content = new MemoryStream(expectedBuffer); + var expectedData = new StreamFileContent(content) + { + ContentType = "application/pdf", + FileName = "Report.pdf" + }; + Result result = Result.File(expectedData); + + // Act + ActionResult> actionResult = result.ToActionResult(); + var contentResult = actionResult.Result as FileStreamResult; + + // Asserts + contentResult + .FileStream + .Should() + .BeOfType() + .Subject + .ToArray() + .Should() + .BeEquivalentTo(expectedBuffer); + contentResult.ContentType.Should().Be(expectedData.ContentType); + contentResult.FileDownloadName.Should().Be(expectedData.FileName); + } + + [Test] + public void ToActionResult_WhenConversionToFileContentResultFails_ShouldThrowInvalidOperationException() + { + // Arrange + var person = new Person { Name = "Test" }; + var result = new Result + { + Status = ResultStatus.ByteArrayFile + }; + var typeName = typeof(FileContentResult).FullName; + var expectedMessage = new FailedConversionError(typeName).Message; + + // Act + Action act = () => result.ToActionResult(); + + // Assert + act.Should() + .Throw() + .WithMessage(expectedMessage); + } + + [Test] + public void ToActionResult_WhenConversionToFileStreamResultFails_ShouldThrowInvalidOperationException() + { + // Arrange + var person = new Person { Name = "Test" }; + var result = new Result + { + Status = ResultStatus.StreamFile + }; + var typeName = typeof(FileStreamResult).FullName; + var expectedMessage = new FailedConversionError(typeName).Message; + + // Act + Action act = () => result.ToActionResult(); + + // Assert + act.Should() + .Throw() + .WithMessage(expectedMessage); + } +} diff --git a/tests/AspNetCore/ToHttpResultTests.FileHttpResult.cs b/tests/AspNetCore/ToHttpResultTests.FileHttpResult.cs new file mode 100644 index 0000000..81d4f9e --- /dev/null +++ b/tests/AspNetCore/ToHttpResultTests.FileHttpResult.cs @@ -0,0 +1,79 @@ +namespace SimpleResults.Tests.AspNetCore; + +public partial class ToHttpResultTests +{ + [Test] + public void ToHttpResult_WhenOperationResultIsByteArrayFileContent_ShouldReturnsFileContentHttpResult() + { + // Arrange + byte[] content = [1, 0, 0, 1]; + var expectedData = new ByteArrayFileContent(content) + { + ContentType = "application/pdf", + FileName = "Report.pdf" + }; + Result result = Result.File(expectedData); + + // Act + IResult httpResult = result.ToHttpResult(); + var contentResult = httpResult as FileContentHttpResult; + + // Asserts + byte[] byteArray = contentResult.FileContents.ToArray(); + byteArray.Should().BeEquivalentTo(expectedData.Content); + contentResult.ContentType.Should().Be(expectedData.ContentType); + contentResult.FileDownloadName.Should().Be(expectedData.FileName); + } + + [Test] + public void ToHttpResult_WhenOperationResultIsStreamFileContent_ShouldReturnsFileStreamHttpResult() + { + // Arrange + byte[] expectedBuffer = [1, 1, 0, 1]; + Stream content = new MemoryStream(expectedBuffer); + var expectedData = new StreamFileContent(content) + { + ContentType = "application/pdf", + FileName = "Report.pdf" + }; + Result result = Result.File(expectedData); + + // Act + IResult httpResult = result.ToHttpResult(); + var contentResult = httpResult as FileStreamHttpResult; + + // Asserts + contentResult + .FileStream + .Should() + .BeOfType() + .Subject + .ToArray() + .Should() + .BeEquivalentTo(expectedBuffer); + contentResult.ContentType.Should().Be(expectedData.ContentType); + contentResult.FileDownloadName.Should().Be(expectedData.FileName); + } + + [TestCase(ResultStatus.ByteArrayFile)] + [TestCase(ResultStatus.StreamFile)] + public void ToHttpResult_WhenConversionToFileHttpResultFails_ShouldThrowInvalidOperationException(ResultStatus status) + { + // Arrange + var person = new Person { Name = "Test" }; + var result = new Result + { + Status = status + }; + var typeName = typeof(IResult).FullName; + var expectedMessage = new FailedConversionError(typeName).Message; + + // Act + Action act = () => result.ToHttpResult(); + + // Assert + act.Should() + .Throw() + .WithMessage(expectedMessage); + } +} diff --git a/tests/Core/ResultTests.cs b/tests/Core/ResultTests.cs index 869fe45..9846698 100644 --- a/tests/Core/ResultTests.cs +++ b/tests/Core/ResultTests.cs @@ -284,4 +284,53 @@ public void ObtainedResources_WhenResultIsObtainedResources_ShouldReturnsListedR actual.Data.Should().BeEquivalentTo(expectedData); actual.Status.Should().Be(ResultStatus.Ok); } + + [Test] + public void File_WhenResultIsByteArrayFileContent_ShouldReturnsResultOfByteArrayFileContent() + { + // Arrange + byte[] content = [1, 1, 0, 0]; + var expectedData = new ByteArrayFileContent(content) + { + ContentType = "application/pdf", + FileName = "Report.pdf" + }; + var expectedMessage = ResponseMessages.FileContent; + + // Act + Result actual = Result.File(expectedData); + + // Asserts + actual.IsSuccess.Should().BeTrue(); + actual.IsFailed.Should().BeFalse(); + actual.Message.Should().Be(expectedMessage); + actual.Errors.Should().BeEmpty(); + actual.Data.Should().BeEquivalentTo(expectedData); + actual.Status.Should().Be(ResultStatus.ByteArrayFile); + } + + [Test] + public void File_WhenResultIsStreamFileContent_ShouldReturnsResultOfStreamFileContent() + { + // Arrange + byte[] buffer = [1, 0, 1]; + Stream content = new MemoryStream(buffer); + var expectedData = new StreamFileContent(content) + { + ContentType = "application/pdf", + FileName = "Report.pdf" + }; + var expectedMessage = ResponseMessages.FileContent; + + // Act + Result actual = Result.File(expectedData); + + // Asserts + actual.IsSuccess.Should().BeTrue(); + actual.IsFailed.Should().BeFalse(); + actual.Message.Should().Be(expectedMessage); + actual.Errors.Should().BeEmpty(); + actual.Data.Should().BeEquivalentTo(expectedData); + actual.Status.Should().Be(ResultStatus.StreamFile); + } } From bae0b86905084f8b6289bf4be7979e09ca6bbe81 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Fri, 1 Mar 2024 09:33:58 -0500 Subject: [PATCH 12/17] chore: Update global usings --- samples/SimpleResults.Example.AspNetCore/GlobalUsings.cs | 4 +++- samples/SimpleResults.Example/GlobalUsings.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/samples/SimpleResults.Example.AspNetCore/GlobalUsings.cs b/samples/SimpleResults.Example.AspNetCore/GlobalUsings.cs index 1dc35d3..858dd20 100644 --- a/samples/SimpleResults.Example.AspNetCore/GlobalUsings.cs +++ b/samples/SimpleResults.Example.AspNetCore/GlobalUsings.cs @@ -1,5 +1,7 @@ -global using Bogus; +global using System.Net.Mime; +global using Bogus; global using Microsoft.AspNetCore.Mvc; +global using Swashbuckle.AspNetCore.Annotations; global using SimpleResults; global using SimpleResults.Example; global using SimpleResults.Example.AspNetCore; diff --git a/samples/SimpleResults.Example/GlobalUsings.cs b/samples/SimpleResults.Example/GlobalUsings.cs index 01b400d..c30b17b 100644 --- a/samples/SimpleResults.Example/GlobalUsings.cs +++ b/samples/SimpleResults.Example/GlobalUsings.cs @@ -1,3 +1,4 @@ global using FluentValidation; global using FluentValidation.Results; -global using System.Text.Json.Serialization; \ No newline at end of file +global using System.Text.Json.Serialization; +global using System.Net.Mime; \ No newline at end of file From c662ddee1ab0fe068e18ecce94f01179faef5f8e Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Fri, 1 Mar 2024 09:34:44 -0500 Subject: [PATCH 13/17] chore: Add new nuget package --- Directory.Packages.props | 3 ++- .../SimpleResults.Example.AspNetCore.csproj | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 21d3330..4066a40 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,6 +9,7 @@ + @@ -17,4 +18,4 @@ - + \ No newline at end of file diff --git a/samples/SimpleResults.Example.AspNetCore/SimpleResults.Example.AspNetCore.csproj b/samples/SimpleResults.Example.AspNetCore/SimpleResults.Example.AspNetCore.csproj index 6681968..ab8dd25 100644 --- a/samples/SimpleResults.Example.AspNetCore/SimpleResults.Example.AspNetCore.csproj +++ b/samples/SimpleResults.Example.AspNetCore/SimpleResults.Example.AspNetCore.csproj @@ -7,6 +7,7 @@ + From 1e80439804cfb4408f05c330abfb9bf96c643e8f Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Fri, 1 Mar 2024 09:40:10 -0500 Subject: [PATCH 14/17] docs: Add an example about the file result --- .../Controllers/FileResultController.cs | 29 +++++++++++++++ .../MinimalApi/FileResultEndpoint.cs | 26 +++++++++++++ .../Models/FileResultRequest.cs | 6 +++ .../Program.cs | 10 ++++- .../FileResultService.cs | 37 +++++++++++++++++++ 5 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 samples/SimpleResults.Example.AspNetCore/Controllers/FileResultController.cs create mode 100644 samples/SimpleResults.Example.AspNetCore/MinimalApi/FileResultEndpoint.cs create mode 100644 samples/SimpleResults.Example.AspNetCore/Models/FileResultRequest.cs create mode 100644 samples/SimpleResults.Example/FileResultService.cs diff --git a/samples/SimpleResults.Example.AspNetCore/Controllers/FileResultController.cs b/samples/SimpleResults.Example.AspNetCore/Controllers/FileResultController.cs new file mode 100644 index 0000000..ee87605 --- /dev/null +++ b/samples/SimpleResults.Example.AspNetCore/Controllers/FileResultController.cs @@ -0,0 +1,29 @@ +namespace SimpleResults.Example.AspNetCore.Controllers; + +[Tags("FileResult WebApi")] +[Route("FileResult-WebApi")] +public class FileResultController +{ + private readonly FileResultService _fileResultService; + + public FileResultController(FileResultService fileResultService) + { + _fileResultService = fileResultService; + } + + [SwaggerResponse(type: typeof(byte[]), statusCode: StatusCodes.Status200OK, contentTypes: MediaTypeNames.Application.Pdf)] + [SwaggerResponse(type: typeof(Result), statusCode: StatusCodes.Status400BadRequest, contentTypes: MediaTypeNames.Application.Json)] + [HttpGet("byte-array")] + public Result GetByteArray([FromQuery]FileResultRequest request) + { + return _fileResultService.GetByteArray(request.FileName); + } + + [SwaggerResponse(type: typeof(byte[]), statusCode: StatusCodes.Status200OK, contentTypes: MediaTypeNames.Application.Pdf)] + [SwaggerResponse(type: typeof(Result), statusCode: StatusCodes.Status400BadRequest, contentTypes: MediaTypeNames.Application.Json)] + [HttpGet("stream")] + public Result GetStream([FromQuery]FileResultRequest request) + { + return _fileResultService.GetStream(request.FileName); + } +} diff --git a/samples/SimpleResults.Example.AspNetCore/MinimalApi/FileResultEndpoint.cs b/samples/SimpleResults.Example.AspNetCore/MinimalApi/FileResultEndpoint.cs new file mode 100644 index 0000000..19256d8 --- /dev/null +++ b/samples/SimpleResults.Example.AspNetCore/MinimalApi/FileResultEndpoint.cs @@ -0,0 +1,26 @@ +namespace SimpleResults.Example.AspNetCore.MinimalApi; + +public static class FileResultEndpoint +{ + public static void AddFileResultRoutes(this WebApplication app) + { + var fileGroup = app + .MapGroup("/FileResult-MinimalApi") + .WithTags("FileResult MinimalApi") + .AddEndpointFilter(); + + fileGroup.MapGet("/byte-array", ([AsParameters]FileResultRequest request, FileResultService service) => + { + return service.GetByteArray(request.FileName); + }) + .Produces(StatusCodes.Status200OK, MediaTypeNames.Application.Pdf) + .Produces(StatusCodes.Status400BadRequest); + + fileGroup.MapGet("/stream", ([AsParameters]FileResultRequest request, FileResultService service) => + { + return service.GetStream(request.FileName); + }) + .Produces(StatusCodes.Status200OK, MediaTypeNames.Application.Pdf) + .Produces(StatusCodes.Status400BadRequest); + } +} diff --git a/samples/SimpleResults.Example.AspNetCore/Models/FileResultRequest.cs b/samples/SimpleResults.Example.AspNetCore/Models/FileResultRequest.cs new file mode 100644 index 0000000..6c43ad5 --- /dev/null +++ b/samples/SimpleResults.Example.AspNetCore/Models/FileResultRequest.cs @@ -0,0 +1,6 @@ +namespace SimpleResults.Example.AspNetCore.Models; + +public class FileResultRequest +{ + public string FileName { get; init; } +} diff --git a/samples/SimpleResults.Example.AspNetCore/Program.cs b/samples/SimpleResults.Example.AspNetCore/Program.cs index e2fd23e..cf4d0cf 100644 --- a/samples/SimpleResults.Example.AspNetCore/Program.cs +++ b/samples/SimpleResults.Example.AspNetCore/Program.cs @@ -7,7 +7,8 @@ .AddSingleton>() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); builder.Services.AddControllers(options => { @@ -20,7 +21,10 @@ }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => +{ + options.EnableAnnotations(); +}); var app = builder.Build(); @@ -44,6 +48,8 @@ app.AddMessageRoutes(); +app.AddFileResultRoutes(); + app.Run(); // This class used in the integration test project. diff --git a/samples/SimpleResults.Example/FileResultService.cs b/samples/SimpleResults.Example/FileResultService.cs new file mode 100644 index 0000000..95aa9e7 --- /dev/null +++ b/samples/SimpleResults.Example/FileResultService.cs @@ -0,0 +1,37 @@ +namespace SimpleResults.Example; + +public class FileResultService +{ + public Result GetByteArray(string fileName) + { + if(string.IsNullOrWhiteSpace(fileName)) + { + return Result.Invalid("FileName is required"); + } + + byte[] content = [1, 1, 0, 0]; + var byteArrayFileContent = new ByteArrayFileContent(content) + { + ContentType = MediaTypeNames.Application.Pdf, + FileName = fileName + }; + return Result.File(byteArrayFileContent); + } + + public Result GetStream(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + return Result.Invalid("FileName is required"); + } + + byte[] buffer = [1, 1, 0, 0]; + Stream content = new MemoryStream(buffer); + var streamFileContent = new StreamFileContent(content) + { + ContentType = MediaTypeNames.Application.Pdf, + FileName = fileName + }; + return Result.File(streamFileContent); + } +} From 2cd7b08bc68173a40190957c68269aede68a1601 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Fri, 1 Mar 2024 09:42:20 -0500 Subject: [PATCH 15/17] test: Add integration tests --- .../Features/GetFileResultTests.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 samples/SimpleResults.Example.Web.Tests/Features/GetFileResultTests.cs diff --git a/samples/SimpleResults.Example.Web.Tests/Features/GetFileResultTests.cs b/samples/SimpleResults.Example.Web.Tests/Features/GetFileResultTests.cs new file mode 100644 index 0000000..d0722ad --- /dev/null +++ b/samples/SimpleResults.Example.Web.Tests/Features/GetFileResultTests.cs @@ -0,0 +1,51 @@ +namespace SimpleResults.Example.Web.Tests.Features; + +public class GetFileResultTests +{ + [TestCase(Routes.File.ByteArrayController)] + [TestCase(Routes.File.StreamController)] + [TestCase(Routes.File.ByteArrayMinimalApi)] + [TestCase(Routes.File.StreamMinimalApi)] + public async Task Get_WhenBytesAreObtained_ShouldReturnsHttpStatusCodeOk(string route) + { + // Arrange + using var factory = new WebApplicationFactory(); + var client = factory.CreateClient(); + byte[] expected = [1, 1, 0, 0]; + var fileName = "Report.pdf"; + var requestUri = $"{route}?fileName={fileName}"; + + // Act + var httpResponse = await client.GetAsync(requestUri); + byte[] result = await httpResponse + .Content + .ReadAsByteArrayAsync(); + + // Asserts + httpResponse.StatusCode.Should().Be(HttpStatusCode.OK); + result.Should().BeEquivalentTo(expected); + } + + [TestCase(Routes.File.ByteArrayController)] + [TestCase(Routes.File.StreamController)] + [TestCase(Routes.File.ByteArrayMinimalApi)] + [TestCase(Routes.File.StreamMinimalApi)] + public async Task Get_WhenFileNameIsEmpty_ShouldReturnsHttpStatusCodeBadRequest(string requestUri) + { + // Arrange + using var factory = new WebApplicationFactory(); + var client = factory.CreateClient(); + + // Act + var httpResponse = await client.GetAsync(requestUri); + var result = await httpResponse + .Content + .ReadFromJsonAsync(); + + // Asserts + httpResponse.StatusCode.Should().Be(HttpStatusCode.BadRequest); + result.IsSuccess.Should().BeFalse(); + result.Message.Should().NotBeNullOrEmpty(); + result.Errors.Should().BeEmpty(); + } +} From 4e689633e358c793e0bc4b4df43aea7caf136465 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Fri, 1 Mar 2024 09:42:38 -0500 Subject: [PATCH 16/17] test: Add routes for testing --- samples/SimpleResults.Example.Web.Tests/Routes.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/samples/SimpleResults.Example.Web.Tests/Routes.cs b/samples/SimpleResults.Example.Web.Tests/Routes.cs index b09c4f8..86b8f04 100644 --- a/samples/SimpleResults.Example.Web.Tests/Routes.cs +++ b/samples/SimpleResults.Example.Web.Tests/Routes.cs @@ -20,6 +20,14 @@ public class Message public const string MinimalApi = "/Message-MinimalApi"; } + public class File + { + public const string ByteArrayController = "/FileResult-WebApi/byte-array"; + public const string StreamController = "/FileResult-WebApi/stream"; + public const string ByteArrayMinimalApi = "/FileResult-MinimalApi/byte-array"; + public const string StreamMinimalApi = "/FileResult-MinimalApi/stream"; + } + public class Order { public const string ManualValidation = "/Order-ManualValidation"; From f7e44830d300672d64d1e642ee507e39f60c62d8 Mon Sep 17 00:00:00 2001 From: MrDave1999 Date: Fri, 1 Mar 2024 10:48:55 -0500 Subject: [PATCH 17/17] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fac520b..829b4ad 100644 --- a/README.md +++ b/README.md @@ -599,6 +599,8 @@ The following table is used as a reference to know which type of result correspo | Result.UpdatedResource | 200 - Ok | | Result.DeletedResource | 200 - Ok | | Result.ObtainedResource | 200 - Ok | +| Result.ObtainedResources| 200 - Ok | +| Result.File | 200 - Ok | | Result.Invalid | 400 - Bad Request | | Result.NotFound | 404 - Not Found | | Result.Unauthorized | 401 - Unauthorized |