using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using SqrtSpace.SpaceTime.Core; namespace SqrtSpace.SpaceTime.AspNetCore; /// /// Extensions for streaming large responses with √n memory usage /// public static class SpaceTimeStreamingExtensions { /// /// Writes a large enumerable as JSON stream with √n buffering /// public static async Task WriteAsJsonStreamAsync( this HttpResponse response, IAsyncEnumerable items, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { response.ContentType = "application/json"; response.Headers.Add("X-SpaceTime-Streaming", "sqrtn"); await using var writer = new Utf8JsonWriter(response.Body, new JsonWriterOptions { Indented = options?.WriteIndented ?? false }); writer.WriteStartArray(); var count = 0; var bufferSize = SpaceTimeCalculator.CalculateSqrtInterval(100_000); // Estimate var buffer = new List(bufferSize); await foreach (var item in items.WithCancellation(cancellationToken)) { buffer.Add(item); count++; if (buffer.Count >= bufferSize) { await FlushBufferAsync(writer, buffer, options, cancellationToken); buffer.Clear(); await response.Body.FlushAsync(cancellationToken); } } // Flush remaining items if (buffer.Count > 0) { await FlushBufferAsync(writer, buffer, options, cancellationToken); } writer.WriteEndArray(); await writer.FlushAsync(cancellationToken); } /// /// Creates an async enumerable result with √n chunking /// public static IActionResult StreamWithSqrtNChunking( this ControllerBase controller, IAsyncEnumerable items, int? estimatedCount = null) { return new SpaceTimeStreamResult(items, estimatedCount); } private static async Task FlushBufferAsync( Utf8JsonWriter writer, List buffer, JsonSerializerOptions? options, CancellationToken cancellationToken) { foreach (var item in buffer) { JsonSerializer.Serialize(writer, item, options); cancellationToken.ThrowIfCancellationRequested(); } } } /// /// Action result for streaming with SpaceTime optimizations /// public class SpaceTimeStreamResult : IActionResult { private readonly IAsyncEnumerable _items; private readonly int? _estimatedCount; public SpaceTimeStreamResult(IAsyncEnumerable items, int? estimatedCount = null) { _items = items; _estimatedCount = estimatedCount; } public async Task ExecuteResultAsync(ActionContext context) { var response = context.HttpContext.Response; response.ContentType = "application/json"; response.Headers.Add("X-SpaceTime-Streaming", "chunked"); if (_estimatedCount.HasValue) { response.Headers.Add("X-Total-Count", _estimatedCount.Value.ToString()); } await response.WriteAsJsonStreamAsync(_items, cancellationToken: context.HttpContext.RequestAborted); } } /// /// Attribute to configure streaming behavior /// [AttributeUsage(AttributeTargets.Method)] public class SpaceTimeStreamingAttribute : Attribute { /// /// Chunk size strategy /// public ChunkStrategy ChunkStrategy { get; set; } = ChunkStrategy.SqrtN; /// /// Custom chunk size (if not using automatic strategies) /// public int? ChunkSize { get; set; } /// /// Whether to include progress headers /// public bool IncludeProgress { get; set; } = true; } /// /// Strategies for determining chunk size /// public enum ChunkStrategy { /// Use √n of estimated total SqrtN, /// Fixed size chunks Fixed, /// Adaptive based on response time Adaptive } /// /// Extensions for streaming file downloads /// public static class FileStreamingExtensions { /// /// Streams a file with √n buffer size /// public static async Task StreamFileWithSqrtNBufferAsync( this HttpResponse response, string filePath, string? contentType = null, CancellationToken cancellationToken = default) { var fileInfo = new FileInfo(filePath); if (!fileInfo.Exists) { response.StatusCode = 404; return; } var bufferSize = (int)SpaceTimeCalculator.CalculateOptimalBufferSize( fileInfo.Length, 4 * 1024 * 1024); // Max 4MB buffer response.ContentType = contentType ?? "application/octet-stream"; response.ContentLength = fileInfo.Length; response.Headers.Add("X-SpaceTime-Buffer-Size", bufferSize.ToString()); await using var fileStream = new FileStream( filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync: true); await fileStream.CopyToAsync(response.Body, bufferSize, cancellationToken); } } /// /// Middleware for automatic response streaming optimization /// public class ResponseStreamingMiddleware { private readonly RequestDelegate _next; private readonly ResponseStreamingOptions _options; public ResponseStreamingMiddleware(RequestDelegate next, ResponseStreamingOptions options) { _next = next; _options = options; } public async Task InvokeAsync(HttpContext context) { // Check if response should be streamed if (_options.EnableAutoStreaming && IsLargeResponse(context)) { // Replace response body with buffered stream var originalBody = context.Response.Body; using var bufferStream = new SqrtNBufferedStream(originalBody, _options.MaxBufferSize); context.Response.Body = bufferStream; try { await _next(context); } finally { context.Response.Body = originalBody; } } else { await _next(context); } } private bool IsLargeResponse(HttpContext context) { // Check endpoint metadata var endpoint = context.GetEndpoint(); var streamingAttr = endpoint?.Metadata.GetMetadata(); return streamingAttr != null; } } /// /// Options for response streaming middleware /// public class ResponseStreamingOptions { /// /// Enable automatic streaming optimization /// public bool EnableAutoStreaming { get; set; } = true; /// /// Maximum buffer size in bytes /// public int MaxBufferSize { get; set; } = 4 * 1024 * 1024; // 4MB } /// /// Stream that buffers using √n strategy /// internal class SqrtNBufferedStream : Stream { private readonly Stream _innerStream; private readonly int _bufferSize; private readonly byte[] _buffer; private int _bufferPosition; public SqrtNBufferedStream(Stream innerStream, int maxBufferSize) { _innerStream = innerStream; _bufferSize = Math.Min(maxBufferSize, SpaceTimeCalculator.CalculateSqrtInterval(1_000_000) * 1024); _buffer = new byte[_bufferSize]; } public override bool CanRead => _innerStream.CanRead; public override bool CanSeek => false; public override bool CanWrite => _innerStream.CanWrite; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override void Flush() { if (_bufferPosition > 0) { _innerStream.Write(_buffer, 0, _bufferPosition); _bufferPosition = 0; } _innerStream.Flush(); } public override async Task FlushAsync(CancellationToken cancellationToken) { if (_bufferPosition > 0) { await _innerStream.WriteAsync(_buffer.AsMemory(0, _bufferPosition), cancellationToken); _bufferPosition = 0; } await _innerStream.FlushAsync(cancellationToken); } public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) { while (count > 0) { var bytesToCopy = Math.Min(count, _bufferSize - _bufferPosition); Buffer.BlockCopy(buffer, offset, _buffer, _bufferPosition, bytesToCopy); _bufferPosition += bytesToCopy; offset += bytesToCopy; count -= bytesToCopy; if (_bufferPosition >= _bufferSize) { Flush(); } } } public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { var remaining = buffer; while (remaining.Length > 0) { var bytesToCopy = Math.Min(remaining.Length, _bufferSize - _bufferPosition); remaining.Slice(0, bytesToCopy).CopyTo(_buffer.AsMemory(_bufferPosition)); _bufferPosition += bytesToCopy; remaining = remaining.Slice(bytesToCopy); if (_bufferPosition >= _bufferSize) { await FlushAsync(cancellationToken); } } } protected override void Dispose(bool disposing) { if (disposing) { Flush(); } base.Dispose(disposing); } }