Light.TemporaryStreams is a .NET library that helps you convert non-seekable streams into seekable temporary streams. This is particularly useful for backend services that receive streams from multipart/form-data requests or download files from storage systems for further processing.
- π Easy conversion of non-seekable streams to seekable temporary streams
- πΎ Automatic management of temporary files (creation and deletion)
- π Smart switching between memory-based and file-based streams based on size
- π§© Plugin system for extending functionality (e.g., calculating hashes during stream copying)
- π Integration with Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Logging
dotnet add package Light.TemporaryStreamsFor just the core functionality without DI and logging integration:
dotnet add package Light.TemporaryStreams.Coreusing Light.TemporaryStreams;
using System.IO;
using System.Threading.Tasks;
// Example: Convert a non-seekable stream to a seekable temporary stream
public async Task<Stream> MakeStreamSeekable(Stream nonSeekableStream)
{
// Create the temporary stream service
var temporaryStreamService = new TemporaryStreamService();
// Copy the non-seekable stream to a temporary stream
var temporaryStream = await temporaryStreamService.CopyToTemporaryStreamAsync(nonSeekableStream);
// The temporary stream is seekable and positioned at the beginning
temporaryStream.Position = 0;
// When you're done, dispose the temporary stream
// If it's backed by a file, the file will be automatically deleted
return temporaryStream; // Remember to dispose this when done
}A TemporaryStream is a wrapper around either:
- π§ A
MemoryStream(for smaller files, less than 80 KB by default) - π A
FileStreamto a temporary file (for larger files)
This approach is similar to how IFormFile works in ASP.NET Core.
When a TemporaryStream instance is disposed:
- If the underlying stream is a
FileStream, the temporary file is automatically deleted - You don't need to worry about cleaning up temporary files manually
using Light.TemporaryStreams;
using Microsoft.Extensions.DependencyInjection;
public void ConfigureServices(IServiceCollection services)
{
// Register the temporary stream service with default options
services.AddTemporaryStreamService();
// Or with custom options
services.AddTemporaryStreamService(options =>
{
options.MemoryStreamThreshold = 1024 * 512; // Use memory stream for files less than 512 KB
options.FileOptions = FileOptions.Asynchronous | FileOptions.DeleteOnClose;
});
}using Light.TemporaryStreams;
using Light.TemporaryStreams.Hashing;
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Threading.Tasks;
public async Task<(TemporaryStream Stream, string Md5Hash, string Sha256Hash)> CopyStreamWithHashing(Stream source)
{
var temporaryStreamService = new TemporaryStreamService();
// Create hash calculators for MD5 and SHA256
var md5Calculator = new CopyToHashCalculator(MD5.Create(), "MD5");
var sha256Calculator = new CopyToHashCalculator(SHA256.Create(), "SHA256");
// Create the hashing plugin with both calculators
var hashingPlugin = new HashingPlugin(
ImmutableArray.Create(md5Calculator, sha256Calculator)
);
// Copy the stream and calculate hashes in one go
var temporaryStream = await temporaryStreamService.CopyToTemporaryStreamAsync(
source,
ImmutableArray.Create<ICopyToTemporaryStreamPlugin>(hashingPlugin)
);
// Get the calculated hashes
string md5Hash = hashingPlugin.GetHash("MD5");
string sha256Hash = hashingPlugin.GetHash("SHA256");
return (temporaryStream, md5Hash, sha256Hash);
}You might need to use Light.TemporaryStreams when:
- You need to manually parse multipart/form-data requests
- Your endpoint accepts both JSON and binary data
- You're processing files in a custom middleware
- You need to work with raw streams but still need seeking capability
See Andrew Lock's blog post for an example of when this might be needed.
using Light.TemporaryStreams;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
private readonly ITemporaryStreamService _temporaryStreamService;
public UploadController(ITemporaryStreamService temporaryStreamService)
{
_temporaryStreamService = temporaryStreamService;
}
[HttpPost]
public async Task<IActionResult> Upload()
{
if (!Request.HasFormContentType)
return BadRequest("Multipart form data expected");
var form = await Request.ReadFormAsync();
// Get metadata from the form
if (!form.TryGetValue("metadata", out var metadata))
return BadRequest("Metadata is required");
var metadataObj = JsonSerializer.Deserialize<FileMetadata>(metadata);
// Get the file stream
if (!form.Files.TryGetValue("file", out var formFile))
return BadRequest("File is required");
// Create a seekable temporary stream from the file stream
using var fileStream = formFile.OpenReadStream();
using var temporaryStream = await _temporaryStreamService.CopyToTemporaryStreamAsync(fileStream);
// Now you can seek and process the file as needed
temporaryStream.Position = 0;
// Process the file...
return Ok(new { message = "File processed successfully" });
}
public class FileMetadata
{
public string Name { get; set; }
public string ContentType { get; set; }
}
}You can customize the behavior of TemporaryStreamService through TemporaryStreamServiceOptions:
var options = new TemporaryStreamServiceOptions
{
// Use memory stream for files smaller than 1 MB
MemoryStreamThreshold = 1024 * 1024,
// Custom buffer size for file operations
BufferSize = 81920,
// Custom file options
FileOptions = FileOptions.Asynchronous | FileOptions.SequentialScan,
// Custom file mode
FileMode = FileMode.Create,
// Custom file access
FileAccess = FileAccess.ReadWrite
};
var service = new TemporaryStreamService(options);This package contains the core implementation including:
ITemporaryStreamServiceinterfaceTemporaryStreamServiceimplementationTemporaryStreamclassTemporaryStreamServiceOptionsfor configuration- Extension methods like
CopyToTemporaryStreamAsync - Plugin system and existing plugins (like
HashingPlugin)
This package builds on Core and adds integration with:
- Microsoft.Extensions.DependencyInjection for registering services
- Microsoft.Extensions.Logging for logging events
- Extension methods for
IServiceCollectionto register the service
Use Light.TemporaryStreams.Core if you're working in a non-DI environment or have your own DI container. Use Light.TemporaryStreams if you're working in an ASP.NET Core application or any other application using Microsoft.Extensions.DependencyInjection.
Contributions are welcome! Feel free to submit issues and pull requests.
This project is licensed under the MIT License - see the LICENSE file for details.