Initial push
This commit is contained in:
199
src/SqrtSpace.SpaceTime.AspNetCore/CheckpointMiddleware.cs
Normal file
199
src/SqrtSpace.SpaceTime.AspNetCore/CheckpointMiddleware.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SqrtSpace.SpaceTime.Core;
|
||||
|
||||
namespace SqrtSpace.SpaceTime.AspNetCore;
|
||||
|
||||
/// <summary>
|
||||
/// Middleware that enables checkpointing for long-running requests
|
||||
/// </summary>
|
||||
public class CheckpointMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<CheckpointMiddleware> _logger;
|
||||
private readonly CheckpointOptions _options;
|
||||
|
||||
public CheckpointMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<CheckpointMiddleware> logger,
|
||||
CheckpointOptions options)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (!ShouldCheckpoint(context))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var checkpointId = context.Request.Headers["X-Checkpoint-Id"].FirstOrDefault();
|
||||
var checkpointManager = new CheckpointManager(
|
||||
_options.CheckpointDirectory,
|
||||
_options.Strategy,
|
||||
_options.EstimatedOperations);
|
||||
|
||||
// Store in HttpContext for access by controllers
|
||||
context.Features.Set<ICheckpointFeature>(new CheckpointFeature(checkpointManager, checkpointId, _options));
|
||||
|
||||
try
|
||||
{
|
||||
// If resuming from checkpoint, restore state
|
||||
if (!string.IsNullOrEmpty(checkpointId))
|
||||
{
|
||||
_logger.LogInformation("Resuming from checkpoint {CheckpointId}", checkpointId);
|
||||
var state = await checkpointManager.RestoreLatestCheckpointAsync<Dictionary<string, object>>();
|
||||
if (state != null)
|
||||
{
|
||||
context.Items["CheckpointState"] = state;
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
checkpointManager.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldCheckpoint(HttpContext context)
|
||||
{
|
||||
// Check if the path matches checkpoint patterns
|
||||
foreach (var pattern in _options.PathPatterns)
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments(pattern))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if endpoint has checkpoint attribute
|
||||
var endpoint = context.GetEndpoint();
|
||||
if (endpoint != null)
|
||||
{
|
||||
var checkpointAttribute = endpoint.Metadata.GetMetadata<EnableCheckpointAttribute>();
|
||||
return checkpointAttribute != null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for checkpoint middleware
|
||||
/// </summary>
|
||||
public class CheckpointOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Directory to store checkpoints
|
||||
/// </summary>
|
||||
public string? CheckpointDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Checkpointing strategy
|
||||
/// </summary>
|
||||
public CheckpointStrategy Strategy { get; set; } = CheckpointStrategy.SqrtN;
|
||||
|
||||
/// <summary>
|
||||
/// Estimated number of operations for √n calculation
|
||||
/// </summary>
|
||||
public long EstimatedOperations { get; set; } = 100_000;
|
||||
|
||||
/// <summary>
|
||||
/// Path patterns that should enable checkpointing
|
||||
/// </summary>
|
||||
public List<string> PathPatterns { get; set; } = new()
|
||||
{
|
||||
"/api/import",
|
||||
"/api/export",
|
||||
"/api/process"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feature interface for checkpoint access
|
||||
/// </summary>
|
||||
public interface ICheckpointFeature
|
||||
{
|
||||
CheckpointManager CheckpointManager { get; }
|
||||
string? CheckpointId { get; }
|
||||
Task<T?> LoadStateAsync<T>(string key, CancellationToken cancellationToken = default) where T : class;
|
||||
Task SaveStateAsync<T>(string key, T state, CancellationToken cancellationToken = default) where T : class;
|
||||
bool ShouldCheckpoint(long currentOperation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of checkpoint feature
|
||||
/// </summary>
|
||||
internal class CheckpointFeature : ICheckpointFeature
|
||||
{
|
||||
private readonly CheckpointOptions _options;
|
||||
private long _operationCount = 0;
|
||||
|
||||
public CheckpointFeature(CheckpointManager checkpointManager, string? checkpointId, CheckpointOptions options)
|
||||
{
|
||||
CheckpointManager = checkpointManager;
|
||||
CheckpointId = checkpointId;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public CheckpointManager CheckpointManager { get; }
|
||||
public string? CheckpointId { get; }
|
||||
|
||||
public async Task<T?> LoadStateAsync<T>(string key, CancellationToken cancellationToken = default) where T : class
|
||||
{
|
||||
if (string.IsNullOrEmpty(CheckpointId))
|
||||
return null;
|
||||
|
||||
return await CheckpointManager.LoadStateAsync<T>(CheckpointId, key, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SaveStateAsync<T>(string key, T state, CancellationToken cancellationToken = default) where T : class
|
||||
{
|
||||
if (string.IsNullOrEmpty(CheckpointId))
|
||||
return;
|
||||
|
||||
await CheckpointManager.SaveStateAsync(CheckpointId, key, state, cancellationToken);
|
||||
}
|
||||
|
||||
public bool ShouldCheckpoint(long currentOperation)
|
||||
{
|
||||
_operationCount = currentOperation;
|
||||
|
||||
return _options.Strategy switch
|
||||
{
|
||||
CheckpointStrategy.SqrtN => currentOperation > 0 && currentOperation % (int)Math.Sqrt(_options.EstimatedOperations) == 0,
|
||||
CheckpointStrategy.Linear => currentOperation > 0 && currentOperation % 1000 == 0,
|
||||
CheckpointStrategy.Logarithmic => IsPowerOfTwo(currentOperation),
|
||||
CheckpointStrategy.None => false,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsPowerOfTwo(long n)
|
||||
{
|
||||
return n > 0 && (n & (n - 1)) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to enable checkpointing on specific endpoints
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public class EnableCheckpointAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Checkpoint strategy to use
|
||||
/// </summary>
|
||||
public CheckpointStrategy Strategy { get; set; } = CheckpointStrategy.SqrtN;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically restore from checkpoint
|
||||
/// </summary>
|
||||
public bool AutoRestore { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SqrtSpace.SpaceTime.Core;
|
||||
using SqrtSpace.SpaceTime.Diagnostics;
|
||||
|
||||
namespace SqrtSpace.SpaceTime.AspNetCore;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring SpaceTime services
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds SpaceTime services to the service collection
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSpaceTime(
|
||||
this IServiceCollection services,
|
||||
Action<SpaceTimeServiceOptions>? configureOptions = null)
|
||||
{
|
||||
var options = new SpaceTimeServiceOptions();
|
||||
configureOptions?.Invoke(options);
|
||||
|
||||
// Register options
|
||||
services.AddSingleton(options);
|
||||
|
||||
// Add checkpoint services if enabled
|
||||
if (options.EnableCheckpointing)
|
||||
{
|
||||
services.AddSingleton(options.CheckpointOptions);
|
||||
}
|
||||
|
||||
// Add streaming services if enabled
|
||||
if (options.EnableStreaming)
|
||||
{
|
||||
services.AddSingleton(options.StreamingOptions);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds SpaceTime middleware to the pipeline
|
||||
/// </summary>
|
||||
public static IApplicationBuilder UseSpaceTime(this IApplicationBuilder app)
|
||||
{
|
||||
var options = app.ApplicationServices.GetService<SpaceTimeServiceOptions>();
|
||||
if (options == null)
|
||||
{
|
||||
throw new InvalidOperationException("SpaceTime services not registered. Call AddSpaceTime() in ConfigureServices.");
|
||||
}
|
||||
|
||||
if (options.EnableCheckpointing)
|
||||
{
|
||||
var checkpointOptions = app.ApplicationServices.GetRequiredService<CheckpointOptions>();
|
||||
app.UseMiddleware<CheckpointMiddleware>(checkpointOptions);
|
||||
}
|
||||
|
||||
if (options.EnableStreaming)
|
||||
{
|
||||
var streamingOptions = app.ApplicationServices.GetRequiredService<ResponseStreamingOptions>();
|
||||
app.UseMiddleware<ResponseStreamingMiddleware>(streamingOptions);
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps SpaceTime diagnostic and monitoring endpoints
|
||||
/// </summary>
|
||||
public static IApplicationBuilder UseSpaceTimeEndpoints(this IApplicationBuilder app)
|
||||
{
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
// Health check endpoint
|
||||
endpoints.MapGet("/spacetime/health", async context =>
|
||||
{
|
||||
context.Response.StatusCode = 200;
|
||||
await context.Response.WriteAsync("OK");
|
||||
});
|
||||
|
||||
// Metrics endpoint (for Prometheus scraping)
|
||||
endpoints.MapGet("/spacetime/metrics", async context =>
|
||||
{
|
||||
context.Response.ContentType = "text/plain";
|
||||
await context.Response.WriteAsync("# SpaceTime metrics endpoint\n");
|
||||
await context.Response.WriteAsync("# Configure OpenTelemetry with Prometheus exporter for metrics\n");
|
||||
});
|
||||
|
||||
// Diagnostics report endpoint
|
||||
endpoints.MapGet("/spacetime/diagnostics", async context =>
|
||||
{
|
||||
var diagnostics = context.RequestServices.GetService<ISpaceTimeDiagnostics>();
|
||||
if (diagnostics != null)
|
||||
{
|
||||
var report = await diagnostics.GenerateReportAsync(TimeSpan.FromHours(1));
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsJsonAsync(report);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
await context.Response.WriteAsync("Diagnostics not configured");
|
||||
}
|
||||
});
|
||||
|
||||
// Configuration endpoint
|
||||
endpoints.MapGet("/spacetime/config", async context =>
|
||||
{
|
||||
var options = context.RequestServices.GetService<SpaceTimeServiceOptions>();
|
||||
if (options != null)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsJsonAsync(options);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
await context.Response.WriteAsync("Configuration not found");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for SpaceTime services
|
||||
/// </summary>
|
||||
public class SpaceTimeServiceOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable checkpointing middleware
|
||||
/// </summary>
|
||||
public bool EnableCheckpointing { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable streaming optimizations
|
||||
/// </summary>
|
||||
public bool EnableStreaming { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Options for checkpointing
|
||||
/// </summary>
|
||||
public CheckpointOptions CheckpointOptions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for streaming
|
||||
/// </summary>
|
||||
public ResponseStreamingOptions StreamingOptions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Directory for storing checkpoints
|
||||
/// </summary>
|
||||
public string CheckpointDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "spacetime-checkpoints");
|
||||
|
||||
/// <summary>
|
||||
/// Checkpointing strategy to use
|
||||
/// </summary>
|
||||
public CheckpointStrategy CheckpointStrategy { get; set; } = CheckpointStrategy.SqrtN;
|
||||
|
||||
/// <summary>
|
||||
/// Interval for checkpointing operations
|
||||
/// </summary>
|
||||
public TimeSpan CheckpointInterval { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Directory for external storage operations
|
||||
/// </summary>
|
||||
public string ExternalStorageDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "spacetime-storage");
|
||||
|
||||
/// <summary>
|
||||
/// Default strategy for space-time operations
|
||||
/// </summary>
|
||||
public SpaceTimeStrategy DefaultStrategy { get; set; } = SpaceTimeStrategy.SqrtN;
|
||||
|
||||
/// <summary>
|
||||
/// Default chunk size for streaming operations
|
||||
/// </summary>
|
||||
public int DefaultChunkSize { get; set; } = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Buffer size for streaming operations
|
||||
/// </summary>
|
||||
public int StreamingBufferSize { get; set; } = 8192;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strategies for space-time tradeoffs
|
||||
/// </summary>
|
||||
public enum SpaceTimeStrategy
|
||||
{
|
||||
/// <summary>Use √n space strategy</summary>
|
||||
SqrtN,
|
||||
/// <summary>Use O(1) space strategy</summary>
|
||||
Constant,
|
||||
/// <summary>Use O(log n) space strategy</summary>
|
||||
Logarithmic,
|
||||
/// <summary>Use O(n) space strategy</summary>
|
||||
Linear
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for streaming large responses with √n memory usage
|
||||
/// </summary>
|
||||
public static class SpaceTimeStreamingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a large enumerable as JSON stream with √n buffering
|
||||
/// </summary>
|
||||
public static async Task WriteAsJsonStreamAsync<T>(
|
||||
this HttpResponse response,
|
||||
IAsyncEnumerable<T> 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<T>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an async enumerable result with √n chunking
|
||||
/// </summary>
|
||||
public static IActionResult StreamWithSqrtNChunking<T>(
|
||||
this ControllerBase controller,
|
||||
IAsyncEnumerable<T> items,
|
||||
int? estimatedCount = null)
|
||||
{
|
||||
return new SpaceTimeStreamResult<T>(items, estimatedCount);
|
||||
}
|
||||
|
||||
private static async Task FlushBufferAsync<T>(
|
||||
Utf8JsonWriter writer,
|
||||
List<T> buffer,
|
||||
JsonSerializerOptions? options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var item in buffer)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, item, options);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action result for streaming with SpaceTime optimizations
|
||||
/// </summary>
|
||||
public class SpaceTimeStreamResult<T> : IActionResult
|
||||
{
|
||||
private readonly IAsyncEnumerable<T> _items;
|
||||
private readonly int? _estimatedCount;
|
||||
|
||||
public SpaceTimeStreamResult(IAsyncEnumerable<T> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to configure streaming behavior
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class SpaceTimeStreamingAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Chunk size strategy
|
||||
/// </summary>
|
||||
public ChunkStrategy ChunkStrategy { get; set; } = ChunkStrategy.SqrtN;
|
||||
|
||||
/// <summary>
|
||||
/// Custom chunk size (if not using automatic strategies)
|
||||
/// </summary>
|
||||
public int? ChunkSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to include progress headers
|
||||
/// </summary>
|
||||
public bool IncludeProgress { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strategies for determining chunk size
|
||||
/// </summary>
|
||||
public enum ChunkStrategy
|
||||
{
|
||||
/// <summary>Use √n of estimated total</summary>
|
||||
SqrtN,
|
||||
/// <summary>Fixed size chunks</summary>
|
||||
Fixed,
|
||||
/// <summary>Adaptive based on response time</summary>
|
||||
Adaptive
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for streaming file downloads
|
||||
/// </summary>
|
||||
public static class FileStreamingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Streams a file with √n buffer size
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Middleware for automatic response streaming optimization
|
||||
/// </summary>
|
||||
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<SpaceTimeStreamingAttribute>();
|
||||
return streamingAttr != null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for response streaming middleware
|
||||
/// </summary>
|
||||
public class ResponseStreamingOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enable automatic streaming optimization
|
||||
/// </summary>
|
||||
public bool EnableAutoStreaming { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum buffer size in bytes
|
||||
/// </summary>
|
||||
public int MaxBufferSize { get; set; } = 4 * 1024 * 1024; // 4MB
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stream that buffers using √n strategy
|
||||
/// </summary>
|
||||
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<byte> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core middleware and extensions for SpaceTime optimizations</Description>
|
||||
<PackageId>SqrtSpace.SpaceTime.AspNetCore</PackageId>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Authors>David H. Friedel Jr</Authors>
|
||||
<Company>MarketAlly LLC</Company>
|
||||
<Copyright>Copyright © 2025 MarketAlly LLC</Copyright>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageProjectUrl>https://github.com/sqrtspace/sqrtspace-dotnet</PackageProjectUrl>
|
||||
<RepositoryUrl>https://www.sqrtspace.dev</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SqrtSpace.SpaceTime.Core\SqrtSpace.SpaceTime.Core.csproj" />
|
||||
<ProjectReference Include="..\SqrtSpace.SpaceTime.Diagnostics\SqrtSpace.SpaceTime.Diagnostics.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user