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);
}
}