Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for ASP.NET Core Minimal APIs (Resolves #8) #10

Merged
merged 4 commits into from
Dec 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/cd-aspnetcore-mvc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions .github/workflows/cd-aspnetcore.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions .github/workflows/cd-net-http.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions Ndjson.AsyncStreams.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
1 change: 1 addition & 0 deletions docs/Ndjson.AsyncStreams.DocFx/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
],
Expand Down
6 changes: 5 additions & 1 deletion docs/Ndjson.AsyncStreams.DocFx/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// An <see cref="IResult"/> that on execution will write the given <see cref="IAsyncEnumerable{T}"/> as NDJSON to the response.
/// </summary>
/// <typeparam name="T">The type of the values in async stream to be serialized.</typeparam>
public partial class NdjsonAsyncEnumerableHttpResult<T> : IResult, IStatusCodeHttpResult, IValueHttpResult, IValueHttpResult<IAsyncEnumerable<T>>, 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");

/// <summary>
/// Gets the object result.
/// </summary>
public IAsyncEnumerable<T>? Value { get; }

object? IValueHttpResult.Value => Value;

/// <summary>
/// Gets the HTTP status code.
/// </summary>
public int? StatusCode { get; }

/// <summary>
/// Gets the value for the <c>Content-Type</c> header.
/// </summary>
public string ContentType { get; } = CONTENT_TYPE;

/// <summary>
/// Gets or sets the serializer settings.
/// </summary>
public JsonSerializerOptions? JsonSerializerOptions { get; internal init; }

/// <summary>
/// Initializes a new instance of the <see cref="NdjsonAsyncEnumerableHttpResult{T}"/> class with the values.
/// </summary>
/// <param name="value">The async stream of values to be serialized to the response.</param>
/// <param name="jsonSerializerOptions">The serializer settings.</param>
internal NdjsonAsyncEnumerableHttpResult(IAsyncEnumerable<T>? value, JsonSerializerOptions? jsonSerializerOptions)
: this(value, statusCode: null, jsonSerializerOptions: jsonSerializerOptions)
{ }

/// <summary>
/// Initializes a new instance of the <see cref="NdjsonAsyncEnumerableHttpResult{T}"/> class with the values.
/// </summary>
/// <param name="value">The async stream of values to be serialized to the response.</param>
/// <param name="jsonSerializerOptions">The serializer settings.</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
internal NdjsonAsyncEnumerableHttpResult(IAsyncEnumerable<T>? value, JsonSerializerOptions? jsonSerializerOptions, int? statusCode)
{
Value = value;
StatusCode = statusCode;
JsonSerializerOptions = jsonSerializerOptions;
}

/// <inheritdoc/>
public async Task ExecuteAsync(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);

var loggerFactory = httpContext.RequestServices?.GetService<ILoggerFactory>();
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<IHttpResponseBodyFeature>();
if (responseBodyFeature is not null)
{
Log.DisablingResponseBuffering(logger);
responseBodyFeature.DisableBuffering();
}
}

private static JsonOptions ResolveJsonOptions(HttpContext httpContext)
{
return httpContext.RequestServices.GetService<IOptions<JsonOptions>>()?.Value ?? new JsonOptions();
}

private static async Task WriteAsyncEnumerableValue(T value, JsonSerializerOptions jsonSerializerOptions, Stream writeStream, CancellationToken cancellationToken)
{
await JsonSerializer.SerializeAsync<T>(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);
}
}
26 changes: 26 additions & 0 deletions src/Ndjson.AsyncStreams.AspNetCore/Http/Results/Results.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Text.Json;
using System.Collections.Generic;
using Ndjson.AsyncStreams.AspNetCore.Http.HttpResults;

namespace Microsoft.AspNetCore.Http;

/// <summary>
/// An extension for <see cref="Results"/> to provide NDJSON related IResult instances.
/// </summary>
public static partial class NdjsonResultExtensions
{
/// <summary>
/// Creates a <see cref="IResult"/> that on execution will write the given <see cref="IAsyncEnumerable{T}"/> as NDJSON to the response.
/// </summary>
/// <param name="resultExtensions">The interface for registering external method that provides <see cref="NdjsonAsyncEnumerableHttpResult{T}"/> instance.</param>
/// <param name="stream">The async stream of values to write as NDJSON.</param>
/// <param name="options">The serializer options to use when serializing the values.</param>
/// <param name="statusCode">The status code to set on the response.</param>
/// <typeparam name="T">The type of the values in async stream to be serialized.</typeparam>
/// <returns>The created <see cref="NdjsonAsyncEnumerableHttpResult{T}"/> that on execution will write the given <paramref name="stream"/> as NDJSON to the response.</returns>
/// <remarks>Callers should cache an instance of serializer settings to avoid recreating cached data with each call.</remarks>
public static IResult Ndjson<T>(this IResultExtensions resultExtensions, IAsyncEnumerable<T>? stream, JsonSerializerOptions? options = null, int? statusCode = null)
{
return new NdjsonAsyncEnumerableHttpResult<T>(stream, options, statusCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Ndjson.AsyncStreams.AspNetCore is a solution for working with asynchronous streaming data sources in ASP.NET Core (Minimal APIs) using NDJSON (Newline Delimited JSON).</Description>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
<TargetFramework>net7.0</TargetFramework>
<AssemblyTitle>Ndjson.AsyncStreams.AspNetCore</AssemblyTitle>
<AssemblyName>Ndjson.AsyncStreams.AspNetCore</AssemblyName>
<PackageId>Ndjson.AsyncStreams.AspNetCore</PackageId>
<PackageTags>ndjson;ndjsonstream;asyncstreams,aspnetcore,minimalapis</PackageTags>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Ndjson.AsyncStreams.AspNetCore.Tests")]
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.1">
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Loading
Loading