Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd609fe
vscode の推奨拡張として usernamehw.errorlens と ms-dotnettools.csdevkit を追加
juner Mar 27, 2026
660390d
nuget 上の Juner.Sequence を使ったベンチマークの作成
juner Mar 27, 2026
4509172
不要なリテラルの削除
juner Mar 27, 2026
e6a5ed4
IsAotCompatible をパッケージプロジェクトにだけ適用する対応および net7.0 には適用しない対応
juner Mar 27, 2026
c7cd827
リフレクションが使われているのは DefaultJsonSerializerOptions の話なので Extensions.Json に…
juner Mar 27, 2026
276acd5
RequiresDynamicCodeAttribute / RequiresUnreferencedCodeAttribute の追加
juner Mar 27, 2026
cba322d
appsettings.json に urls を追加して https を強制する
juner Mar 30, 2026
59ca7dc
AspNetCore 層には <VerifyReferenceAotCompatibility>true</VerifyReference…
juner Mar 30, 2026
2275efe
copilot に README.md を監修してもらう
juner Mar 30, 2026
2bde13e
gemini に README.md を推敲してもらう
juner Mar 30, 2026
1dee017
SequenceSerializerOptions.Empty を Default として internal 化し、 ISequenceS…
juner Mar 30, 2026
55a39b9
copilot に推敲してもらう
juner Mar 30, 2026
1a3ad5b
copilot さんと推敲
juner Mar 30, 2026
74b990e
不要な using の削除
juner Mar 30, 2026
3c90647
copilot さんと推敲
juner Mar 30, 2026
cbd4196
dotnet foramt の実施
juner Mar 30, 2026
3468a0f
chatgptさん と推敲
juner Mar 30, 2026
39d3923
copilot さんと推敲
juner Mar 30, 2026
3870e41
pragmaの閉じ忘れ対応
juner Mar 30, 2026
476abe4
不要なusing の削除
juner Mar 30, 2026
60447fb
copilot と推敲した README.md
juner Mar 30, 2026
e688d80
using 漏れ( NET7.0のみ
juner Mar 30, 2026
dd992b1
copilot / chatgpt との推敲
juner Mar 30, 2026
f25566d
copilotと推敲
juner Mar 30, 2026
0cdbd78
CHANGELOG と csproj の修正と
juner Mar 30, 2026
cb1043b
1.0.0-preview-2 (1.0.0.2) -> 1.0.0 (1.0.0.3)
juner Mar 30, 2026
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
38 changes: 38 additions & 0 deletions AspNetCore.Sequence/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Changelog — Juner.AspNetCore.Sequence

## [1.0.0] — 2026-03-31

### Added

- Initial release of ASP.NET Core integration for streaming JSON.
- Streaming **output** via:
- `JsonSequenceResult<T>`
- `JsonLineResult<T>`
- `NdJsonResult<T>`
- `SequenceResult<T>` (content negotiation)
- Streaming **input** via:
- `Sequence<T>` (Minimal API model binding)
- MVC `SequenceInputFormatter`
- Streaming **output** for MVC via:
- `JsonSequenceOutputFormatter`
- `JsonLineOutputFormatter`
- `NdJsonOutputFormatter`
- Content negotiation for streaming formats (`Accept` header).
- OpenAPI (.NET 10+) support:
- `x-streaming: true`
- `x-itemSchema`
- Per‑format content types
- Minimal API integration:
- `TypedResults.JsonSequence`
- `TypedResults.JsonLine`
- `TypedResults.NdJson`
- `TypedResults.Sequence`
- MVC integration:
- `[Consumes]` / `[Produces]` metadata for streaming formats
- Controller binding for `Sequence<T>`

### Notes

- Minimal API streaming is **AOT‑safe**.
- MVC streaming uses formatters and is **not AOT‑safe** due to ASP.NET Core limitations.
- JSON Array (`application/json`) is accepted/returned only as a **non‑streaming convenience**.
2 changes: 1 addition & 1 deletion AspNetCore.Sequence/Http/Content.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ namespace Juner.AspNetCore.Sequence.Http;
/// </summary>
/// <param name="ContentType"></param>
/// <param name="IsStreaming"></param>
public record Content(string ContentType, bool IsStreaming) : IContent;
public record Content(string ContentType, bool IsStreaming, Type? NoStreamingType = null) : IContent;
5 changes: 5 additions & 0 deletions AspNetCore.Sequence/Http/HttpResults/JsonLineResultOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Reflection;
using System.Threading.Channels;

Expand Down Expand Up @@ -58,4 +61,6 @@ public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
typeof(T),
[new Content(CONTENT_TYPE, true)]));
}

protected override ILogger GetLogger(IServiceProvider provider) => provider.GetService<ILogger<JsonLineResult<T>>>() ?? (ILogger)NullLogger.Instance;
}
4 changes: 4 additions & 0 deletions AspNetCore.Sequence/Http/HttpResults/JsonSequenceResultOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Reflection;
using System.Threading.Channels;

Expand Down Expand Up @@ -63,4 +66,5 @@ public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
typeof(T),
[new Content(CONTENT_TYPE, true)]));
}
protected override ILogger GetLogger(IServiceProvider provider) => provider.GetService<ILogger<JsonSequenceResult<T>>>() ?? (ILogger)NullLogger.Instance;
}
4 changes: 4 additions & 0 deletions AspNetCore.Sequence/Http/HttpResults/NdJsonResultOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Reflection;
using System.Threading.Channels;

Expand Down Expand Up @@ -57,4 +60,5 @@ public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
typeof(T),
[new Content(CONTENT_TYPE, true)]));
}
protected override ILogger GetLogger(IServiceProvider provider) => provider.GetService<ILogger<NdJsonResult<T>>>() ?? (ILogger)NullLogger.Instance;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text.Json;
using System.Diagnostics;
using Juner.Sequence;
Expand All @@ -19,13 +18,7 @@ namespace Juner.AspNetCore.Sequence.Http.HttpResults;
[DebuggerDisplay("{Values,nq}")]
public abstract partial class SequenceResultBase<T> : IResult
{
ILogger GetLogger(IServiceProvider provider)
{
var loggerType = typeof(ILogger<>).MakeGenericType(GetType());
var logger = provider.GetService(loggerType) as ILogger;
if (logger is not null) return logger;
return NullLogger.Instance;
}
protected abstract ILogger GetLogger(IServiceProvider provider);
static JsonSerializerOptions GetOptions(IServiceProvider provider, ILogger logger)
{
var jsonOptions = provider.GetService<IOptions<JsonOptions>>()?.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using System.Net.Mime;
using Juner.Sequence;

#if !NET8_0_OR_GREATER
Expand All @@ -20,13 +18,7 @@ namespace Juner.AspNetCore.Sequence.Http.HttpResults;
[DebuggerDisplay("{Values,nq}")]
public partial class SequenceResult<T> : IResult
{
ILogger GetLogger(IServiceProvider provider)
{
var loggerType = typeof(ILogger<>).MakeGenericType(GetType());
var logger = provider.GetService(loggerType) as ILogger;
if (logger is not null) return logger;
return NullLogger.Instance;
}
ILogger GetLogger(IServiceProvider provider) => provider.GetService<ILogger<SequenceResult<T>>>() ?? (ILogger)NullLogger.Instance;
JsonSerializerOptions GetOptions(IServiceProvider provider, ILogger logger)
{
var jsonOptions = provider.GetService<IOptions<JsonOptions>>()?.Value;
Expand Down Expand Up @@ -60,7 +52,7 @@ public async Task ExecuteAsync(HttpContext httpContext)
#endif
var values = ToAsyncEnumerable(httpContext.RequestAborted);

if (options.IsEmpty)
if (options.IsInvalid)
{
var jsonTypeInfo = serializerOptions.GetTypeInfo<IAsyncEnumerable<T>>();

Expand Down
4 changes: 2 additions & 2 deletions AspNetCore.Sequence/Http/HttpResults/SequenceResultOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
builder.Metadata.Add(new ProducesSequenceResponseTypeMetadata(
STATUS_CODE,
typeof(T),
[.. MakePatternList.Select(v => new Content(v.ContentType, v.IsStreaming))]));
[.. MakePatternList.Select(v => new Content(v.ContentType, v.IsStreaming, v.IsStreaming ? null : typeof(IAsyncEnumerable<T>)))]));
}

record MakePattern(string ContentType, ISequenceSerializerWriteOptions Options, bool IsStreaming);
Expand Down Expand Up @@ -189,7 +189,7 @@ static IEnumerable<MakePattern> MakePatterns()
{
const string contentType =
"application/json";
yield return new(contentType, SequenceSerializerOptions.Empty, false);
yield return new(contentType, SequenceSerializerOptions.Default, false);
}

}
Expand Down
1 change: 1 addition & 0 deletions AspNetCore.Sequence/Http/IContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public interface IContent
{
string ContentType { get; }
bool IsStreaming { get; }
Type? NoStreamingType { get; }
}
2 changes: 1 addition & 1 deletion AspNetCore.Sequence/Http/SequenceOfT.PopulateMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public sealed partial class Sequence<T> : IEndpointParameterMetadataProvider
public static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder)
=> builder.Metadata.Add(new AcceptsSequenceMetadata(
itemType: typeof(T),
contentTypes: [.. MakePatternActionList.Select(v => new Content(v.ContentType, v.IsStreaming))],
contentTypes: [.. MakePatternActionList.Select(v => new Content(v.ContentType, v.IsStreaming, v.IsStreaming ? null : typeof(IAsyncEnumerable<T>)))],
isOptional: false
));
}
1 change: 0 additions & 1 deletion AspNetCore.Sequence/Http/SequenceOfT.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;
using System.Threading.Channels;

namespace Juner.AspNetCore.Sequence.Http;
Expand Down
5 changes: 5 additions & 0 deletions AspNetCore.Sequence/Internals/InternalFormatReader.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Juner.Sequence;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.IO.Pipelines;
using System.Text.Json.Serialization.Metadata;
using System.Threading.Channels;
Expand Down Expand Up @@ -29,6 +30,8 @@ public static object ReadResult<T>(
};
}

[RequiresDynamicCode("Uses dynamic generic delegate generation")]
[RequiresUnreferencedCode("Uses reflection to create generic methods")]
public static object ReadResult(
Type elementType,
EnumerableType enumerableType,
Expand Down Expand Up @@ -59,6 +62,8 @@ CancellationToken cancellationToken

static readonly ConcurrentDictionary<Type, Delegate> cache = new();

[RequiresDynamicCode("Uses dynamic generic delegate generation")]
[RequiresUnreferencedCode("Uses reflection to create generic methods")]
static Delegate CreateDelegate(Type elementType)
{
var method =
Expand Down
14 changes: 10 additions & 4 deletions AspNetCore.Sequence/Internals/InternalFormatWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
using System.Text.Json.Serialization.Metadata;
using System.Threading.Channels;

#if NET9_0_OR_GREATER
using System.IO.Pipelines;
#endif

namespace Juner.AspNetCore.Sequence.Internals;

internal static class InternalFormatWriter
Expand All @@ -27,6 +23,8 @@ internal static class InternalFormatWriter
{typeof(ChannelReader<>), EnumerableType.ChannelReader },
{typeof(Http.Sequence<>), EnumerableType.Sequence },
}.AsReadOnly();

[RequiresUnreferencedCode("Uses reflection to search interfaces")]
public static bool TryGetOutputMode([NotNullWhen(true)] Type? objectType, [NotNullWhen(true)] out EnumerableType outputType, [NotNullWhen(true)] out Type type)
{
outputType = default;
Expand Down Expand Up @@ -54,6 +52,9 @@ public static bool TryGetOutputMode([NotNullWhen(true)] Type? objectType, [NotNu
}

static JsonTypeInfo GetJsonTypeInfo(JsonSerializerOptions serializerOptions, Type type) => serializerOptions.GetTypeInfo(type);

[RequiresDynamicCode("Uses dynamic generic delegate generation")]
[RequiresUnreferencedCode("Uses reflection to create generic methods")]
public static Task WriteResponseBodyAsync(
Type? objectType,
object? @object,
Expand All @@ -75,6 +76,7 @@ public static Task WriteResponseBodyAsync(
cancellationToken);
}

[RequiresUnreferencedCode("Uses reflection to search interfaces")]
public static Task WriteAsync<Enumerable, T>(
Enumerable? @object,
HttpContext httpContext,
Expand Down Expand Up @@ -117,6 +119,8 @@ static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(IEnumerable<T>? values, [E

static readonly ConcurrentDictionary<Type, Delegate> cache = new();

[RequiresDynamicCode("Uses dynamic generic delegate generation")]
[RequiresUnreferencedCode("Uses reflection to create generic methods")]
static Task WriteAsync(
Type objectType,
object? @object,
Expand Down Expand Up @@ -147,6 +151,8 @@ static Task WriteAsync(
cancellationToken);
}

[RequiresDynamicCode("Uses dynamic generic delegate generation")]
[RequiresUnreferencedCode("Uses reflection to create generic methods")]
static Delegate CreateDelegate(Type objectType)
{
if (!TryGetOutputMode(objectType, out _, out var type))
Expand Down
6 changes: 3 additions & 3 deletions AspNetCore.Sequence/Juner.AspNetCore.Sequence.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Streaming JSON sequence support for ASP.NET Core.
Supports NDJSON, JSON Lines, JSON Sequence, and JSON arrays using IAsyncEnumerable.</Description>
<Description>Streaming JSON support for ASP.NET Core (NDJSON, JSON Lines, JSON Sequence).
Also supports JSON arrays as non-streaming input/output for convenience.
Provides Minimal API and MVC integration with content negotiation and OpenAPI support.</Description>
<PackageTags>aspnetcore;streaming;json;ndjson;jsonlines;json-seq;iasyncenumerable</PackageTags>
<PackageProjectUrl>https://github.com/juner/Sequence</PackageProjectUrl>
<IsPackable>True</IsPackable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Text;
using System.Text.Json;
using Juner.Sequence;
using System.Diagnostics.CodeAnalysis;



#if NET8_0_OR_GREATER
Expand All @@ -22,6 +24,8 @@ namespace Juner.AspNetCore.Sequence.Mvc.Formatters;
/// <summary>
/// application/jsonl 対応の フォーマッター
/// </summary>
[RequiresDynamicCode("Uses dynamic generic delegate generation")]
[RequiresUnreferencedCode("Uses reflection to create generic methods")]
public partial class JsonLineOutputFormatter : TextOutputFormatter
{
/// <summary>
Expand All @@ -37,7 +41,7 @@ public JsonLineOutputFormatter()
const string ContentType =
"application/jsonl";

/// <inheritdoc />
/// <inheritdoc />
protected override bool CanWriteType(Type? type)
=> InternalFormatWriter.TryGetOutputMode(type, out _, out _);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Text;
using System.Text.Json;
using Juner.Sequence;
using System.Diagnostics.CodeAnalysis;



#if NET8_0_OR_GREATER
Expand All @@ -22,6 +24,8 @@ namespace Juner.AspNetCore.Sequence.Mvc.Formatters;
/// <summary>
/// application/json-seq 対応の フォーマッター
/// </summary>
[RequiresDynamicCode("Uses dynamic generic delegate generation")]
[RequiresUnreferencedCode("Uses reflection to create generic methods")]
public partial class JsonSequenceOutputFormatter : TextOutputFormatter
{
/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions AspNetCore.Sequence/Mvc/Formatters/NdJsonOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Text;
using System.Text.Json;
using Juner.Sequence;
using System.Diagnostics.CodeAnalysis;



#if NET8_0_OR_GREATER
Expand All @@ -22,6 +24,8 @@ namespace Juner.AspNetCore.Sequence.Mvc.Formatters;
/// <summary>
/// application/x-ndjson 対応の フォーマッター
/// </summary>
[RequiresDynamicCode("Uses dynamic generic delegate generation")]
[RequiresUnreferencedCode("Uses reflection to create generic methods")]
public partial class NdJsonOutputFormatter : TextOutputFormatter
{
/// <summary>
Expand Down
14 changes: 6 additions & 8 deletions AspNetCore.Sequence/Mvc/Formatters/SequenceInputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading.Channels;
#if !NET8_0_OR_GREATER
using System.Text.Json.Serialization.Metadata;
#endif

namespace Juner.AspNetCore.Sequence.Mvc.Formatters;

[RequiresDynamicCode("Uses dynamic generic delegate generation")]
[RequiresUnreferencedCode("Uses reflection to create generic methods")]
public partial class SequenceInputFormatter : TextInputFormatter
{

Expand Down Expand Up @@ -58,13 +62,7 @@ public override bool CanRead(InputFormatterContext context)
return true;
}

ILogger GetLogger(IServiceProvider provider)
{
var loggerType = typeof(ILogger<>).MakeGenericType(GetType());
var logger = provider.GetService(loggerType) as ILogger;
if (logger is not null) return logger;
return NullLogger.Instance;
}
ILogger GetLogger(IServiceProvider provider) => provider.GetService<ILogger<SequenceInputFormatter>>() ?? (ILogger)NullLogger.Instance;
static JsonSerializerOptions GetOptions(IServiceProvider provider, ILogger logger)
{
var jsonOptions = provider.GetService<IOptions<JsonOptions>>()?.Value;
Expand Down
Loading
Loading