From 1f76ef8b84f956fb5e212e31663ba3da3ee77189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20P=C4=99czek?= Date: Thu, 30 Nov 2023 21:56:23 +0100 Subject: [PATCH 1/4] Scaffolding Ndjson.AsyncStreams.AspNetCore --- .github/workflows/cd-aspnetcore-mvc.yml | 4 +++ .github/workflows/cd-net-http.yml | 4 +++ .github/workflows/ci.yml | 4 +++ .github/workflows/gh-pages.yml | 4 +++ Ndjson.AsyncStreams.sln | 14 +++++++++++ .../Ndjson.AsyncStreams.AspNetCore.csproj | 15 +++++++++++ ...n.AsyncStreams.AspNetCore.Mvc.Tests.csproj | 8 +++--- .../Unit/NdjsonWriterTests.cs | 2 +- ...djson.AsyncStreams.AspNetCore.Tests.csproj | 25 +++++++++++++++++++ .../Ndjson.AsyncStreams.Net.Http.Tests.csproj | 10 ++++---- 10 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 src/Ndjson.AsyncStreams.AspNetCore/Ndjson.AsyncStreams.AspNetCore.csproj create mode 100644 test/Ndjson.AsyncStreams.AspNetCore.Tests/Ndjson.AsyncStreams.AspNetCore.Tests.csproj diff --git a/.github/workflows/cd-aspnetcore-mvc.yml b/.github/workflows/cd-aspnetcore-mvc.yml index cc126a0..64e1b16 100644 --- a/.github/workflows/cd-aspnetcore-mvc.yml +++ b/.github/workflows/cd-aspnetcore-mvc.yml @@ -27,6 +27,10 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: '7.0.x' + - name: Setup .NET 8.0 SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' - name: Restore run: dotnet restore - name: Build diff --git a/.github/workflows/cd-net-http.yml b/.github/workflows/cd-net-http.yml index 70f2dd3..940d8b9 100644 --- a/.github/workflows/cd-net-http.yml +++ b/.github/workflows/cd-net-http.yml @@ -27,6 +27,10 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: '7.0.x' + - name: Setup .NET 8.0 SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' - name: Restore run: dotnet restore - name: Build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19daaf9..a3889ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,10 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: '7.0.x' + - name: Setup .NET 8.0 SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' - name: Restore run: dotnet restore - name: Build diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index d3d684a..03d78b9 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -22,6 +22,10 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: '7.0.x' + - name: Setup .NET 8.0 SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' - name: Restore run: dotnet restore - name: Build diff --git a/Ndjson.AsyncStreams.sln b/Ndjson.AsyncStreams.sln index 3006727..4174c45 100644 --- a/Ndjson.AsyncStreams.sln +++ b/Ndjson.AsyncStreams.sln @@ -30,6 +30,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ndjson.AsyncStreams.AspNetCore", "src\Ndjson.AsyncStreams.AspNetCore\Ndjson.AsyncStreams.AspNetCore.csproj", "{EE057DD0-020E-4618-AB7E-57E89ABD4C52}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ndjson.AsyncStreams.AspNetCore.Tests", "test\Ndjson.AsyncStreams.AspNetCore.Tests\Ndjson.AsyncStreams.AspNetCore.Tests.csproj", "{5629047B-8E27-4E23-BC43-A7F9F7EF743B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,6 +64,14 @@ Global {96123E46-1C81-4810-B710-FD4B1F08E582}.Debug|Any CPU.Build.0 = Debug|Any CPU {96123E46-1C81-4810-B710-FD4B1F08E582}.Release|Any CPU.ActiveCfg = Release|Any CPU {96123E46-1C81-4810-B710-FD4B1F08E582}.Release|Any CPU.Build.0 = Release|Any CPU + {EE057DD0-020E-4618-AB7E-57E89ABD4C52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE057DD0-020E-4618-AB7E-57E89ABD4C52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE057DD0-020E-4618-AB7E-57E89ABD4C52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE057DD0-020E-4618-AB7E-57E89ABD4C52}.Release|Any CPU.Build.0 = Release|Any CPU + {5629047B-8E27-4E23-BC43-A7F9F7EF743B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5629047B-8E27-4E23-BC43-A7F9F7EF743B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5629047B-8E27-4E23-BC43-A7F9F7EF743B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5629047B-8E27-4E23-BC43-A7F9F7EF743B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -71,6 +83,8 @@ Global {60ECE376-F1BF-4CCD-877C-BAB401BFB9D4} = {0889D597-88B4-432C-8C30-DE10CAD53834} {0F85F6AE-1C13-476E-945F-AAF4997DD051} = {55EFE6F9-33D5-4153-B1DE-5672827DEB08} {96123E46-1C81-4810-B710-FD4B1F08E582} = {3E0EBFB8-5167-4F4F-8A0F-C013D23D5238} + {EE057DD0-020E-4618-AB7E-57E89ABD4C52} = {55EFE6F9-33D5-4153-B1DE-5672827DEB08} + {5629047B-8E27-4E23-BC43-A7F9F7EF743B} = {0889D597-88B4-432C-8C30-DE10CAD53834} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5C3DC840-115D-4634-9527-340F8269AF24} diff --git a/src/Ndjson.AsyncStreams.AspNetCore/Ndjson.AsyncStreams.AspNetCore.csproj b/src/Ndjson.AsyncStreams.AspNetCore/Ndjson.AsyncStreams.AspNetCore.csproj new file mode 100644 index 0000000..daae847 --- /dev/null +++ b/src/Ndjson.AsyncStreams.AspNetCore/Ndjson.AsyncStreams.AspNetCore.csproj @@ -0,0 +1,15 @@ + + + Ndjson.AsyncStreams.AspNetCore is a solution for working with asynchronous streaming data sources in ASP.NET Core (Minimal APIs) using NDJSON (Newline Delimited JSON). + 1.0.0 + + net6.0 + Ndjson.AsyncStreams.AspNetCore + Ndjson.AsyncStreams.AspNetCore + Ndjson.AsyncStreams.AspNetCore + ndjson;ndjsonstream;asyncstreams,aspnetcore,minimalapis + + + + + diff --git a/test/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests.csproj b/test/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests.csproj index e79c4e2..15d0d98 100644 --- a/test/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests.csproj +++ b/test/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests.csproj @@ -1,18 +1,18 @@  - netcoreapp3.1;net5.0;net6.0 + netcoreapp3.1;net5.0;net6.0;net7.0;net8.0 false latest - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests/Unit/NdjsonWriterTests.cs b/test/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests/Unit/NdjsonWriterTests.cs index 4278113..4666683 100644 --- a/test/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests/Unit/NdjsonWriterTests.cs +++ b/test/Ndjson.AsyncStreams.AspNetCore.Mvc.Tests/Unit/NdjsonWriterTests.cs @@ -41,7 +41,7 @@ private static INdjsonWriter PrepareSystemTextNdjsonWriter(Stream wri }; #endif -#if NET5_0 || NET6_0 +#if NET5_0 || NET6_0 || NET7_0 || NET8_0 JsonSerializerOptions jsonSerializerOptions = new(JsonSerializerDefaults.Web); #endif diff --git a/test/Ndjson.AsyncStreams.AspNetCore.Tests/Ndjson.AsyncStreams.AspNetCore.Tests.csproj b/test/Ndjson.AsyncStreams.AspNetCore.Tests/Ndjson.AsyncStreams.AspNetCore.Tests.csproj new file mode 100644 index 0000000..d740c2f --- /dev/null +++ b/test/Ndjson.AsyncStreams.AspNetCore.Tests/Ndjson.AsyncStreams.AspNetCore.Tests.csproj @@ -0,0 +1,25 @@ + + + net6.0;net7.0;net8.0 + false + latest + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/Ndjson.AsyncStreams.Net.Http.Tests/Ndjson.AsyncStreams.Net.Http.Tests.csproj b/test/Ndjson.AsyncStreams.Net.Http.Tests/Ndjson.AsyncStreams.Net.Http.Tests.csproj index 0c9fc2f..892af66 100644 --- a/test/Ndjson.AsyncStreams.Net.Http.Tests/Ndjson.AsyncStreams.Net.Http.Tests.csproj +++ b/test/Ndjson.AsyncStreams.Net.Http.Tests/Ndjson.AsyncStreams.Net.Http.Tests.csproj @@ -1,17 +1,17 @@  - net7.0 + net7.0;net8.0 false latest - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 983077c4bfd8d6504da06525e2a227c79bba0a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20P=C4=99czek?= Date: Sat, 2 Dec 2023 22:34:35 +0100 Subject: [PATCH 2/4] Adding NdjsonAsyncEnumerableHttpResultOfT --- .../NdjsonAsyncEnumerableHttpResultOfT.cs | 158 ++++++++++++++++++ .../Http/Results/Results.cs | 25 +++ .../Ndjson.AsyncStreams.AspNetCore.csproj | 2 +- .../Properties/AssemblyInfo.cs | 1 + ...djson.AsyncStreams.AspNetCore.Tests.csproj | 3 +- .../NdjsonAsyncEnumerableHttpResultTests.cs | 105 ++++++++++++ 6 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 src/Ndjson.AsyncStreams.AspNetCore/Http/Results/NdjsonAsyncEnumerableHttpResultOfT.cs create mode 100644 src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs create mode 100644 src/Ndjson.AsyncStreams.AspNetCore/Properties/AssemblyInfo.cs create mode 100644 test/Ndjson.AsyncStreams.AspNetCore.Tests/Unit/Http/Results/NdjsonAsyncEnumerableHttpResultTests.cs diff --git a/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/NdjsonAsyncEnumerableHttpResultOfT.cs b/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/NdjsonAsyncEnumerableHttpResultOfT.cs new file mode 100644 index 0000000..5372dec --- /dev/null +++ b/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/NdjsonAsyncEnumerableHttpResultOfT.cs @@ -0,0 +1,158 @@ +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Ndjson.AsyncStreams.AspNetCore.Http.HttpResults; + +/// +/// An that on execution will write the given as NDJSON to the response. +/// +/// The type of the values in async stream to be serialized. +public partial class NdjsonAsyncEnumerableHttpResult : IResult, IStatusCodeHttpResult, IValueHttpResult, IValueHttpResult>, IContentTypeHttpResult +{ + private static readonly string CONTENT_TYPE = new MediaTypeHeaderValue("application/x-ndjson") + { + Encoding = Encoding.UTF8 + }.ToString(); + + private static readonly byte[] _newlineDelimiter = Encoding.UTF8.GetBytes("\n"); + + /// + /// Gets the object result. + /// + public IAsyncEnumerable? Value { get; } + + object? IValueHttpResult.Value => Value; + + /// + /// Gets the HTTP status code. + /// + public int? StatusCode { get; } + + /// + /// Gets the value for the Content-Type header. + /// + public string ContentType { get; } = CONTENT_TYPE; + + /// + /// Gets or sets the serializer settings. + /// + public JsonSerializerOptions? JsonSerializerOptions { get; internal init; } + + /// + /// Initializes a new instance of the class with the values. + /// + /// The async stream of values to be serialized to the response. + /// The serializer settings. + internal NdjsonAsyncEnumerableHttpResult(IAsyncEnumerable? value, JsonSerializerOptions? jsonSerializerOptions) + : this(value, statusCode: null, jsonSerializerOptions: jsonSerializerOptions) + { } + + /// + /// Initializes a new instance of the class with the values. + /// + /// The async stream of values to be serialized to the response. + /// The serializer settings. + /// The HTTP status code of the response. + internal NdjsonAsyncEnumerableHttpResult(IAsyncEnumerable? value, JsonSerializerOptions? jsonSerializerOptions, int? statusCode) + { + Value = value; + StatusCode = statusCode; + JsonSerializerOptions = jsonSerializerOptions; + } + + /// + public async Task ExecuteAsync(HttpContext httpContext) + { + ArgumentNullException.ThrowIfNull(httpContext); + + var loggerFactory = httpContext.RequestServices?.GetService(); + var logger = loggerFactory?.CreateLogger("Ndjson.AsyncStreams.AspNetCore.Http.Result.NdjsonAsyncEnumerableHttpResult") ?? NullLogger.Instance; + + SetStatusCode(httpContext, logger); + SetContentType(httpContext, logger); + DisableResponseBuffering(httpContext, logger); + + if (Value is null) + { + return; + } + + JsonSerializerOptions jsonSerializerOptions = JsonSerializerOptions ?? ResolveJsonOptions(httpContext).SerializerOptions; + + try + { + Log.WritingAsyncEnumerableAsNdjson(logger); + + await foreach (T value in Value.WithCancellation(httpContext.RequestAborted)) + { + await WriteAsyncEnumerableValue(value, jsonSerializerOptions, httpContext.Response.Body, httpContext.RequestAborted); + } + } + catch (OperationCanceledException) when (httpContext.RequestAborted.IsCancellationRequested) { } + } + + private void SetStatusCode(HttpContext httpContext, ILogger logger) + { + if (StatusCode is { } statusCode) + { + Log.SettingStatusCode(logger, statusCode); + httpContext.Response.StatusCode = statusCode; + } + } + + private static void SetContentType(HttpContext httpContext, ILogger logger) + { + Log.SettingContentType(logger, CONTENT_TYPE); + httpContext.Response.ContentType = CONTENT_TYPE; + } + + private static void DisableResponseBuffering(HttpContext httpContext, ILogger logger) + { + IHttpResponseBodyFeature? responseBodyFeature = httpContext.Features.Get(); + if (responseBodyFeature is not null) + { + Log.DisablingResponseBuffering(logger); + responseBodyFeature.DisableBuffering(); + } + } + + private static JsonOptions ResolveJsonOptions(HttpContext httpContext) + { + return httpContext.RequestServices.GetService>()?.Value ?? new JsonOptions(); + } + + private static async Task WriteAsyncEnumerableValue(T value, JsonSerializerOptions jsonSerializerOptions, Stream writeStream, CancellationToken cancellationToken) + { + await JsonSerializer.SerializeAsync(writeStream, value, jsonSerializerOptions, cancellationToken); + await writeStream.WriteAsync(_newlineDelimiter, cancellationToken); + await writeStream.FlushAsync(cancellationToken); + } + + internal static partial class Log + { + [LoggerMessage(1, LogLevel.Information, "Setting HTTP status code {StatusCode}.", EventName = "SettingStatusCode")] + public static partial void SettingStatusCode(ILogger logger, int statusCode); + + [LoggerMessage(2, LogLevel.Information, "Setting Content-Type header to {ContentType}.", EventName = "SettingContentType")] + public static partial void SettingContentType(ILogger logger, string contentType); + + [LoggerMessage(3, LogLevel.Information, "Disabling response buffering.", EventName = "DisablingResponseBuffering")] + public static partial void DisablingResponseBuffering(ILogger logger); + + [LoggerMessage(4, LogLevel.Information, "Writing values as NDJSON.", EventName = "WritingAsyncEnumerableAsNdjson")] + public static partial void WritingAsyncEnumerableAsNdjson(ILogger logger); + } +} diff --git a/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs b/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs new file mode 100644 index 0000000..a7a4331 --- /dev/null +++ b/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using System.Collections.Generic; +using Ndjson.AsyncStreams.AspNetCore.Http.HttpResults; + +namespace Microsoft.AspNetCore.Http; + +/// +/// An extension for to provide NDJSON related IResult instances. +/// +public static partial class NdjsonResultExtensions +{ + /// + /// Creates a that on execution will write the given as NDJSON to the response. + /// + /// The interface for registering external method that provides instance. + /// The async stream of values to write as NDJSON. + /// The serializer options to use when serializing the values. + /// The status code to set on the response. + /// The created that on execution will write the given as NDJSON to the response. + /// Callers should cache an instance of serializer settings to avoid recreating cached data with each call. + public static IResult Ndjson(this IResultExtensions resultExtensions, IAsyncEnumerable? stream, JsonSerializerOptions? options = null, int? statusCode = null) + { + return new NdjsonAsyncEnumerableHttpResult(stream, options, statusCode); + } +} diff --git a/src/Ndjson.AsyncStreams.AspNetCore/Ndjson.AsyncStreams.AspNetCore.csproj b/src/Ndjson.AsyncStreams.AspNetCore/Ndjson.AsyncStreams.AspNetCore.csproj index daae847..f162ea3 100644 --- a/src/Ndjson.AsyncStreams.AspNetCore/Ndjson.AsyncStreams.AspNetCore.csproj +++ b/src/Ndjson.AsyncStreams.AspNetCore/Ndjson.AsyncStreams.AspNetCore.csproj @@ -3,7 +3,7 @@ Ndjson.AsyncStreams.AspNetCore is a solution for working with asynchronous streaming data sources in ASP.NET Core (Minimal APIs) using NDJSON (Newline Delimited JSON). 1.0.0 - net6.0 + net7.0 Ndjson.AsyncStreams.AspNetCore Ndjson.AsyncStreams.AspNetCore Ndjson.AsyncStreams.AspNetCore diff --git a/src/Ndjson.AsyncStreams.AspNetCore/Properties/AssemblyInfo.cs b/src/Ndjson.AsyncStreams.AspNetCore/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..86bb235 --- /dev/null +++ b/src/Ndjson.AsyncStreams.AspNetCore/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Ndjson.AsyncStreams.AspNetCore.Tests")] \ No newline at end of file diff --git a/test/Ndjson.AsyncStreams.AspNetCore.Tests/Ndjson.AsyncStreams.AspNetCore.Tests.csproj b/test/Ndjson.AsyncStreams.AspNetCore.Tests/Ndjson.AsyncStreams.AspNetCore.Tests.csproj index d740c2f..c097ea2 100644 --- a/test/Ndjson.AsyncStreams.AspNetCore.Tests/Ndjson.AsyncStreams.AspNetCore.Tests.csproj +++ b/test/Ndjson.AsyncStreams.AspNetCore.Tests/Ndjson.AsyncStreams.AspNetCore.Tests.csproj @@ -1,11 +1,12 @@  - net6.0;net7.0;net8.0 + net7.0;net8.0 false latest + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Ndjson.AsyncStreams.AspNetCore.Tests/Unit/Http/Results/NdjsonAsyncEnumerableHttpResultTests.cs b/test/Ndjson.AsyncStreams.AspNetCore.Tests/Unit/Http/Results/NdjsonAsyncEnumerableHttpResultTests.cs new file mode 100644 index 0000000..6f2b560 --- /dev/null +++ b/test/Ndjson.AsyncStreams.AspNetCore.Tests/Unit/Http/Results/NdjsonAsyncEnumerableHttpResultTests.cs @@ -0,0 +1,105 @@ +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Microsoft.Net.Http.Headers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Ndjson.AsyncStreams.AspNetCore.Http.HttpResults; +using Xunit; +using Moq; + +namespace Ndjson.AsyncStreams.AspNetCore.Tests.Unit.Http.Results +{ + public class NdjsonAsyncEnumerableHttpResultTests + { + public struct ValueType + { + public int Id { get; set; } + + public string Name { get; set; } + } + + private static readonly int STATUS_CODE = 100; + private static readonly string CONTENT_TYPE = new MediaTypeHeaderValue("application/x-ndjson") + { + Encoding = Encoding.UTF8 + }.ToString(); + + private static readonly ValueType[] VALUES = [new ValueType { Id = 1, Name = "Value 01" }, new ValueType { Id = 2, Name = "Value 02" }]; + private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private const string VALUES_AS_NDJSON = "{\"id\":1,\"name\":\"Value 01\"}\n{\"id\":2,\"name\":\"Value 02\"}\n"; + + private static HttpContext PrepareHttpContext(IHttpResponseBodyFeature httpResponseBodyFeature = null) + { + HttpContext httpContext = new DefaultHttpContext(); + httpContext.Response.Body = new MemoryStream(); + if (httpResponseBodyFeature != null) + { + httpContext.Features.Set(httpResponseBodyFeature); + } + + return httpContext; + } + + [Fact] + public async Task ExecuteAsync_StatusCodeIsProvided_SetsResponseStatusCode() + { + HttpContext httpContext = PrepareHttpContext(); + NdjsonAsyncEnumerableHttpResult ndjsonAsyncEnumerableHttpResult = new NdjsonAsyncEnumerableHttpResult(StreamValuesAsync(), JSON_SERIALIZER_OPTIONS, statusCode: STATUS_CODE); + + await ndjsonAsyncEnumerableHttpResult.ExecuteAsync(httpContext); + + Assert.Equal(STATUS_CODE, httpContext.Response.StatusCode); + } + + [Fact] + public async Task ExecuteAsync_ResponseContentTypeIsSetToNdjsonWithUtf8Encoding() + { + HttpContext httpContext = PrepareHttpContext(); + NdjsonAsyncEnumerableHttpResult ndjsonAsyncEnumerableHttpResult = new NdjsonAsyncEnumerableHttpResult(StreamValuesAsync(), JSON_SERIALIZER_OPTIONS); + + await ndjsonAsyncEnumerableHttpResult.ExecuteAsync(httpContext); + + Assert.Equal(CONTENT_TYPE, httpContext.Response.ContentType); + } + + [Fact] + public async Task ExecuteAsync_DisablesResponseBuffering() + { + Mock httpResponseBodyFeatureMock = new(Stream.Null); + HttpContext httpContext = PrepareHttpContext(httpResponseBodyFeatureMock.Object); + NdjsonAsyncEnumerableHttpResult ndjsonAsyncEnumerableHttpResult = new NdjsonAsyncEnumerableHttpResult(StreamValuesAsync(), JSON_SERIALIZER_OPTIONS); + + await ndjsonAsyncEnumerableHttpResult.ExecuteAsync(httpContext); + + httpResponseBodyFeatureMock.Verify(m => m.DisableBuffering(), Times.Once); + } + + [Fact] + public async Task ExecuteAsync_WritesValuesAsNdjson() + { + HttpContext httpContext = PrepareHttpContext(); + NdjsonAsyncEnumerableHttpResult ndjsonAsyncEnumerableHttpResult = new NdjsonAsyncEnumerableHttpResult(StreamValuesAsync(), JSON_SERIALIZER_OPTIONS); + + await ndjsonAsyncEnumerableHttpResult.ExecuteAsync(httpContext); + + httpContext.Response.Body.Seek(0, SeekOrigin.Begin); + StreamReader responseBodyReader = new(httpContext.Response.Body); + Assert.Equal(VALUES_AS_NDJSON, responseBodyReader.ReadToEnd()); + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + async IAsyncEnumerable StreamValuesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + for (int valueIndex = 0; valueIndex < VALUES.Length; valueIndex++) + { + yield return VALUES[valueIndex]; + }; + } +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + } +} From e92c8dd786fe38786ffe84db32cadcea1317595a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20P=C4=99czek?= Date: Sun, 3 Dec 2023 18:37:34 +0100 Subject: [PATCH 3/4] Updating documentation --- README.md | 1 + docs/Ndjson.AsyncStreams.DocFx/docfx.json | 1 + docs/Ndjson.AsyncStreams.DocFx/index.md | 6 +++++- src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8efc8ce..854f770 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Ndjson.AsyncStreams [![NuGet Version](https://img.shields.io/nuget/v/Ndjson.AsyncStreams.Net.Http?label=Ndjson.AsyncStreams.Net.Http&logo=nuget)](https://www.nuget.org/packages/Ndjson.AsyncStreams.Net.Http/) +[![NuGet Version](https://img.shields.io/nuget/v/Ndjson.AsyncStreams.AspNetCore?label=Ndjson.AsyncStreams.AspNetCore&logo=nuget)](https://www.nuget.org/packages/Ndjson.AsyncStreams.AspNetCore/) [![NuGet Version](https://img.shields.io/nuget/v/Ndjson.AsyncStreams.AspNetCore.Mvc?label=Ndjson.AsyncStreams.AspNetCore.Mvc&logo=nuget)](https://www.nuget.org/packages/Ndjson.AsyncStreams.AspNetCore.Mvc/) Ndjson.AsyncStreams is a solution for working with asynchronous streaming data sources over HTTP using NDJSON (Newline Delimited JSON). diff --git a/docs/Ndjson.AsyncStreams.DocFx/docfx.json b/docs/Ndjson.AsyncStreams.DocFx/docfx.json index 00e711d..3cdac77 100644 --- a/docs/Ndjson.AsyncStreams.DocFx/docfx.json +++ b/docs/Ndjson.AsyncStreams.DocFx/docfx.json @@ -5,6 +5,7 @@ { "files": [ "src/Ndjson.AsyncStreams.Net.Http/Ndjson.AsyncStreams.Net.Http.csproj", + "src/Ndjson.AsyncStreams.AspNetCore/Ndjson.AsyncStreams.AspNetCore.csproj", "src/Ndjson.AsyncStreams.AspNetCore.Mvc/Ndjson.AsyncStreams.AspNetCore.Mvc.csproj", "src/Ndjson.AsyncStreams.AspNetCore.Mvc.NewtonsoftJson/Ndjson.AsyncStreams.AspNetCore.Mvc.NewtonsoftJson.csproj" ], diff --git a/docs/Ndjson.AsyncStreams.DocFx/index.md b/docs/Ndjson.AsyncStreams.DocFx/index.md index 7da9fe9..8484514 100644 --- a/docs/Ndjson.AsyncStreams.DocFx/index.md +++ b/docs/Ndjson.AsyncStreams.DocFx/index.md @@ -4,12 +4,16 @@ Ndjson.AsyncStreams is a solution for working with asynchronous streaming data s ## Installation -You can install [Ndjson.AsyncStreams.Net.Http](https://www.nuget.org/packages/Ndjson.AsyncStreams.Net.Http), and [Ndjson.AsyncStreams.AspNetCore.Mvc](https://www.nuget.org/packages/Ndjson.AsyncStreams.AspNetCore.Mvc) from NuGet. +You can install [Ndjson.AsyncStreams.Net.Http](https://www.nuget.org/packages/Ndjson.AsyncStreams.Net.Http), [Ndjson.AsyncStreams.AspNetCore](https://www.nuget.org/packages/Ndjson.AsyncStreams.AspNetCore), or [Ndjson.AsyncStreams.AspNetCore.Mvc](https://www.nuget.org/packages/Ndjson.AsyncStreams.AspNetCore.Mvc) from NuGet. ``` PM> Install-Package Ndjson.AsyncStreams.Net.Http ``` +``` +PM> Install-Package Ndjson.AsyncStreams.AspNetCore +``` + ``` PM> Install-Package Ndjson.AsyncStreams.AspNetCore.Mvc ``` diff --git a/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs b/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs index a7a4331..12d6946 100644 --- a/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs +++ b/src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs @@ -16,6 +16,7 @@ public static partial class NdjsonResultExtensions /// The async stream of values to write as NDJSON. /// The serializer options to use when serializing the values. /// The status code to set on the response. + /// The type of the values in async stream to be serialized. /// The created that on execution will write the given as NDJSON to the response. /// Callers should cache an instance of serializer settings to avoid recreating cached data with each call. public static IResult Ndjson(this IResultExtensions resultExtensions, IAsyncEnumerable? stream, JsonSerializerOptions? options = null, int? statusCode = null) From 594e789bbff04a8d30c68a2cfcaf34e8a5d1eb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20P=C4=99czek?= Date: Sun, 3 Dec 2023 18:50:13 +0100 Subject: [PATCH 4/4] Adding continuous delivery pipeline --- .github/workflows/cd-aspnetcore.yml | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/cd-aspnetcore.yml diff --git a/.github/workflows/cd-aspnetcore.yml b/.github/workflows/cd-aspnetcore.yml new file mode 100644 index 0000000..8ae7b94 --- /dev/null +++ b/.github/workflows/cd-aspnetcore.yml @@ -0,0 +1,50 @@ +name: Ndjson.AsyncStreams.AspNetCore - CD +on: + push: + tags: + - "aspnetcore-v[0-9]+.[0-9]+.[0-9]+" +jobs: + deployment: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Extract VERSION + run: echo "VERSION=${GITHUB_REF/refs\/tags\/aspnetcore-v/}" >> $GITHUB_ENV + - name: Setup .NET Core 3.1 SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '3.1.x' + - name: Setup .NET 5.0 SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '5.0.x' + - name: Setup .NET 6.0 SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '6.0.x' + - name: Setup .NET 7.0 SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '7.0.x' + - name: Setup .NET 8.0 SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + - name: Restore + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore + - name: Test + run: dotnet test --configuration Release --no-build + - name: Pack + run: dotnet pack --configuration Release --no-build + - name: NuGet Push Ndjson.AsyncStreams.AspNetCore + run: dotnet nuget push src/Ndjson.AsyncStreams.AspNetCore/bin/Release/Ndjson.AsyncStreams.AspNetCore.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_API_KEY} + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + - name: Publish Documentation + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: docs/Ndjson.AsyncStreams.DocFx/wwwroot \ No newline at end of file