From 690aaeb22ad39741e026a4ef1063b1743429033b Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Mon, 7 Jul 2025 07:58:03 +0200 Subject: [PATCH 01/20] chore: prepare .csproj for release Signed-off-by: Kenny Pflug --- Directory.Build.props | 5 +++++ Directory.Packages.props | 1 + images/light-logo.png | Bin 0 -> 4340 bytes readme.md | 1 + src/Directory.Build.props | 21 +++++++++++++++++- .../Light.TemporaryStreams.Core.csproj | 15 +++++++++++++ .../packages.lock.json | 20 +++++++++++++++++ .../Light.TemporaryStreams.csproj | 16 +++++++++++++ src/Light.TemporaryStreams/packages.lock.json | 20 +++++++++++++++++ tests/Directory.Build.props | 1 - 10 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 images/light-logo.png create mode 100644 readme.md diff --git a/Directory.Build.props b/Directory.Build.props index eddcafb..2a0d29e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,5 +5,10 @@ disable true true + Kenny Pflug + Kenny Pflug + Copyright (c) 2025 Kenny Pflug + Light.TemporaryStreams + 1.0.0 diff --git a/Directory.Packages.props b/Directory.Packages.props index 2425cd9..ca2af1a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,6 +12,7 @@ + diff --git a/images/light-logo.png b/images/light-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1b016d054036a671569deb3cf3191187a3be763b GIT binary patch literal 4340 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 true true - Light.TemporaryStreams + git + https://github.com/feO2x/Light.TemporaryStreams.git + https://github.com/feO2x/Light.TemporaryStreams + true + true + snupkg + true + streaming;memory-management;form-file + readme.md + MIT + https://github.com/feO2x/Light.TemporaryStreams/blob/main/LICENSE + + + + + + + + + diff --git a/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj b/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj index 17f6863..44d0c7c 100644 --- a/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj +++ b/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj @@ -1,5 +1,20 @@ + + + + The core libary of Light.TemporaryStreams, containing that base implementation without Microsoft.Extensions.Logging and Microsoft.Extensions.DependencyInjection integration. Provides temporary streams, similar to how IFormFile works in ASP.NET Core. + + Light.TemporaryStreams.Core 1.0.0 + --------------------------------- + + - Initial release 🚀 + - use ITemporaryStreamService and the CopyToTemporaryStreamAsync extension method to create temporary seekable streams easily + - use the HashingPlugin to calculate hashes during the copy operation, or write your own plugins via ICopyToTemporaryStreamPlugin + - check out TemporaryStreamServiceOptions to configure the service + + + diff --git a/src/Light.TemporaryStreams.Core/packages.lock.json b/src/Light.TemporaryStreams.Core/packages.lock.json index 2511975..3b8dd2c 100644 --- a/src/Light.TemporaryStreams.Core/packages.lock.json +++ b/src/Light.TemporaryStreams.Core/packages.lock.json @@ -13,6 +13,26 @@ "requested": "[8.0.17, )", "resolved": "8.0.17", "contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw==" + }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" } } } diff --git a/src/Light.TemporaryStreams/Light.TemporaryStreams.csproj b/src/Light.TemporaryStreams/Light.TemporaryStreams.csproj index 9deda95..b3ffb48 100644 --- a/src/Light.TemporaryStreams/Light.TemporaryStreams.csproj +++ b/src/Light.TemporaryStreams/Light.TemporaryStreams.csproj @@ -1,5 +1,21 @@  + + + + Provides temporary streams, similar to how IFormFile works in ASP.NET Core. With full integration with Microsoft.Extensions.Logging and Microsoft.Extensions.DependencyInjection. + + Light.TemporaryStreams 1.0.0 + --------------------------------- + + - Initial release 🚀 + - use the services.AddTemporaryStreamService extension method to integrate ITemporaryStreamService into Microsoft.Extensions.DependencyInjection + - use ITemporaryStreamService and the CopyToTemporaryStreamAsync extension method to create temporary seekable streams easily + - use the HashingPlugin to calculate hashes during the copy operation, or write your own plugins via ICopyToTemporaryStreamPlugin + - check out TemporaryStreamServiceOptions to configure the service + + + diff --git a/src/Light.TemporaryStreams/packages.lock.json b/src/Light.TemporaryStreams/packages.lock.json index 74dee5a..7b86821 100644 --- a/src/Light.TemporaryStreams/packages.lock.json +++ b/src/Light.TemporaryStreams/packages.lock.json @@ -23,6 +23,26 @@ "resolved": "8.0.17", "contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw==" }, + "Microsoft.SourceLink.GitHub": { + "type": "Direct", + "requested": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==", + "dependencies": { + "Microsoft.Build.Tasks.Git": "8.0.0", + "Microsoft.SourceLink.Common": "8.0.0" + } + }, + "Microsoft.Build.Tasks.Git": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" + }, + "Microsoft.SourceLink.Common": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" + }, "light.temporarystreams.core": { "type": "Project", "dependencies": { diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index b6f38ce..7a2be64 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -11,7 +11,6 @@ false true true - Light.TemporaryStreams From 6ee1a16a5814342af71d3cee82efaa8dcbf57820 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Mon, 7 Jul 2025 08:26:50 +0200 Subject: [PATCH 02/20] docs: initial version of readme.md Signed-off-by: Kenny Pflug --- readme.md | 264 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 263 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 4ec3a1d..8bfe50e 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,263 @@ -# Light.TemporaryStreams +# Light.TemporaryStreams 🌊 + +[![NuGet Badge](https://img.shields.io/nuget/v/Light.TemporaryStreams.svg)](https://www.nuget.org/packages/Light.TemporaryStreams/) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/feO2x/Light.TemporaryStreams/blob/main/LICENSE) + +## Overview 🔍 + +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. + +## Key Features ✨ + +- 🚀 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 + +## Installation 📦 + +```bash +dotnet add package Light.TemporaryStreams +``` + +For just the core functionality without DI and logging integration: + +```bash +dotnet add package Light.TemporaryStreams.Core +``` + +## Basic Usage 🚀 + +```csharp +using Light.TemporaryStreams; +using System.IO; +using System.Threading.Tasks; + +// Example: Convert a non-seekable stream to a seekable temporary stream +public async Task 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 +} +``` + +## How It Works 🛠️ + +### TemporaryStream + +A `TemporaryStream` is a wrapper around either: + +- 🧠 A `MemoryStream` (for smaller files, less than 80 KB by default) +- 📄 A `FileStream` to a temporary file (for larger files) + +This approach is similar to how `IFormFile` works in ASP.NET Core. + +### Automatic Cleanup + +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 + +## DI Integration 🔌 + +```csharp +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 Plugins 🧩 + +### Hash Calculation During Stream Copy + +```csharp +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(hashingPlugin) + ); + + // Get the calculated hashes + string md5Hash = hashingPlugin.GetHash("MD5"); + string sha256Hash = hashingPlugin.GetHash("SHA256"); + + return (temporaryStream, md5Hash, sha256Hash); +} +``` + +## When To Use Light.TemporaryStreams 🤔 + +### When Processing Files in ASP.NET Core Without IFormFile + +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](https://andrewlock.net/reading-json-and-binary-data-from-multipart-form-data-sections-in-aspnetcore/) +for an example of when this might be needed. + +### Example: Processing a File Upload with JSON Metadata + +```csharp +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 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(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; } + } +} +``` + +## Configuring the TemporaryStreamService ⚙️ + +You can customize the behavior of `TemporaryStreamService` through `TemporaryStreamServiceOptions`: + +```csharp +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); +``` + +## Light.TemporaryStreams.Core vs. Light.TemporaryStreams 🧰 + +### Light.TemporaryStreams.Core + +This package contains the core implementation including: + +- `ITemporaryStreamService` interface +- `TemporaryStreamService` implementation +- `TemporaryStream` class +- `TemporaryStreamServiceOptions` for configuration +- Extension methods like `CopyToTemporaryStreamAsync` +- Plugin system and existing plugins (like `HashingPlugin`) + +### Light.TemporaryStreams + +This package builds on Core and adds integration with: + +- Microsoft.Extensions.DependencyInjection for registering services +- Microsoft.Extensions.Logging for logging events +- Extension methods for `IServiceCollection` to 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. + +## Contributing 🤝 + +Contributions are welcome! Feel free to submit issues and pull requests. + +## License 📜 + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. From 6e3321906662bb4d2ccde1fede48a75b9b5d79ae Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Mon, 7 Jul 2025 08:29:26 +0200 Subject: [PATCH 03/20] feat: temporary stream position is reset to 0 during CopyToTemporaryStreamAsync Signed-off-by: Kenny Pflug --- src/Light.TemporaryStreams.Core/TemporaryStream.cs | 5 +++++ .../TemporaryStreamServiceExtensions.cs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/Light.TemporaryStreams.Core/TemporaryStream.cs b/src/Light.TemporaryStreams.Core/TemporaryStream.cs index 46ac25e..a0a6dc3 100644 --- a/src/Light.TemporaryStreams.Core/TemporaryStream.cs +++ b/src/Light.TemporaryStreams.Core/TemporaryStream.cs @@ -327,4 +327,9 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo /// public override void WriteByte(byte value) => UnderlyingStream.WriteByte(value); + + /// + /// Resets the position of the stream to the beginning. + /// + public void ResetStreamPosition() => Position = 0; } diff --git a/src/Light.TemporaryStreams.Core/TemporaryStreamServiceExtensions.cs b/src/Light.TemporaryStreams.Core/TemporaryStreamServiceExtensions.cs index e600e79..d6edcb6 100644 --- a/src/Light.TemporaryStreams.Core/TemporaryStreamServiceExtensions.cs +++ b/src/Light.TemporaryStreams.Core/TemporaryStreamServiceExtensions.cs @@ -63,6 +63,8 @@ await source { await source.CopyToAsync(temporaryStream, cancellationToken).ConfigureAwait(false); } + + temporaryStream.ResetStreamPosition(); } catch { @@ -141,6 +143,8 @@ await source { await plugins[i].AfterCopyAsync(cancellationToken).ConfigureAwait(false); } + + temporaryStream.ResetStreamPosition(); } catch { From f5d15a311c91c5f8efc665494deaf1f15333c2a0 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 05:45:05 +0200 Subject: [PATCH 04/20] test: ensure that temporary stream position is 0 after CopyToTemporaryStreamAsync Signed-off-by: Kenny Pflug --- .../CopyToTemporaryStreamTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs b/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs index 65da05d..59d6afd 100644 --- a/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs +++ b/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs @@ -456,11 +456,10 @@ private static async Task AssertTemporaryStreamContentsMatchAsync( CancellationToken cancellationToken ) { - temporaryStream.Should().NotBeNull(); + temporaryStream.Position.Should().Be(0); temporaryStream.IsFileBased.Should().Be(expectFileBased); temporaryStream.Length.Should().Be(expectedData.Length); - temporaryStream.Position = 0; var copiedData = new byte[expectedData.Length]; await temporaryStream.ReadExactlyAsync(copiedData, cancellationToken); copiedData.Should().Equal(expectedData); From 9b3354aef496b1f7d45b9b45182f7d30c96b84a9 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 06:58:34 +0200 Subject: [PATCH 05/20] chore: add nupkg to list of known words Signed-off-by: Kenny Pflug --- Light.TemporaryStreams.sln.DotSettings | 203 +++++++++++++++++-------- 1 file changed, 136 insertions(+), 67 deletions(-) diff --git a/Light.TemporaryStreams.sln.DotSettings b/Light.TemporaryStreams.sln.DotSettings index 0a92497..65d7d9b 100644 --- a/Light.TemporaryStreams.sln.DotSettings +++ b/Light.TemporaryStreams.sln.DotSettings @@ -1,14 +1,26 @@ - - True - HINT - SUGGESTION - HINT - SUGGESTION - SUGGESTION - HINT - DO_NOT_SHOW - DO_NOT_SHOW - DO_NOT_SHOW + + True + HINT + SUGGESTION + HINT + SUGGESTION + SUGGESTION + HINT + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW True <?xml version="1.0" encoding="utf-16"?><Profile name="Kenny's Kleanup"><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CppAddTypenameTemplateKeywords>True</CppAddTypenameTemplateKeywords><CppCStyleToStaticCastDescriptor>True</CppCStyleToStaticCastDescriptor><CppRedundantDereferences>True</CppRedundantDereferences><CppDeleteRedundantAccessSpecifier>True</CppDeleteRedundantAccessSpecifier><CppRemoveCastDescriptor>True</CppRemoveCastDescriptor><CppRemoveElseKeyword>True</CppRemoveElseKeyword><CppShortenQualifiedName>True</CppShortenQualifiedName><CppDeleteRedundantSpecifier>True</CppDeleteRedundantSpecifier><CppRemoveStatement>True</CppRemoveStatement><CppDeleteRedundantTypenameTemplateKeywords>True</CppDeleteRedundantTypenameTemplateKeywords><CppReplaceExpressionWithBooleanConst>True</CppReplaceExpressionWithBooleanConst><CppMakeIfConstexpr>True</CppMakeIfConstexpr><CppMakePostfixOperatorPrefix>True</CppMakePostfixOperatorPrefix><CppMakeVariableConstexpr>True</CppMakeVariableConstexpr><CppChangeSmartPointerToMakeFunction>True</CppChangeSmartPointerToMakeFunction><CppReplaceThrowWithRethrowFix>True</CppReplaceThrowWithRethrowFix><CppTypeTraitAliasDescriptor>True</CppTypeTraitAliasDescriptor><CppRemoveRedundantConditionalExpressionDescriptor>True</CppRemoveRedundantConditionalExpressionDescriptor><CppSimplifyConditionalExpressionDescriptor>True</CppSimplifyConditionalExpressionDescriptor><CppReplaceExpressionWithNullptr>True</CppReplaceExpressionWithNullptr><CppReplaceTieWithStructuredBindingDescriptor>True</CppReplaceTieWithStructuredBindingDescriptor><CppUseAssociativeContainsDescriptor>True</CppUseAssociativeContainsDescriptor><CppUseEraseAlgorithmDescriptor>True</CppUseEraseAlgorithmDescriptor><CppCodeStyleCleanupDescriptor ArrangeBraces="True" ArrangeAuto="True" ArrangeFunctionDeclarations="True" ArrangeNestedNamespaces="True" ArrangeTypeAliases="True" ArrangeCVQualifiers="True" ArrangeSlashesInIncludeDirectives="True" ArrangeOverridingFunctions="True" SortIncludeDirectives="True" SortMemberInitializers="True" /><CppReformatCode>True</CppReformatCode><CSReorderTypeMembers>True</CSReorderTypeMembers><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" ArrangeNullCheckingPattern="True" /><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><FSharpReformatCode>True</FSharpReformatCode><ShaderLabReformatCode>True</ShaderLabReformatCode><RemoveCodeRedundanciesVB>True</RemoveCodeRedundanciesVB><VBMakeFieldReadonly>True</VBMakeFieldReadonly><Xaml.RemoveRedundantNamespaceAlias>True</Xaml.RemoveRedundantNamespaceAlias><Xaml.RedundantFreezeAttribute>True</Xaml.RedundantFreezeAttribute><Xaml.RemoveRedundantModifiersAttribute>True</Xaml.RemoveRedundantModifiersAttribute><Xaml.RemoveRedundantNameAttribute>True</Xaml.RemoveRedundantNameAttribute><Xaml.RemoveRedundantResource>True</Xaml.RemoveRedundantResource><Xaml.RemoveRedundantCollectionProperty>True</Xaml.RemoveRedundantCollectionProperty><Xaml.RemoveRedundantAttachedPropertySetter>True</Xaml.RemoveRedundantAttachedPropertySetter><Xaml.RemoveRedundantStyledValue>True</Xaml.RemoveRedundantStyledValue><Xaml.RemoveForbiddenResourceName>True</Xaml.RemoveForbiddenResourceName><Xaml.RemoveRedundantGridDefinitionsAttribute>True</Xaml.RemoveRedundantGridDefinitionsAttribute><Xaml.RemoveRedundantUpdateSourceTriggerAttribute>True</Xaml.RemoveRedundantUpdateSourceTriggerAttribute><Xaml.RemoveRedundantBindingModeAttribute>True</Xaml.RemoveRedundantBindingModeAttribute><Xaml.RemoveRedundantGridSpanAttribut>True</Xaml.RemoveRedundantGridSpanAttribut><XMLReformatCode>True</XMLReformatCode><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><VBOptimizeImports>True</VBOptimizeImports><VBShortenReferences>True</VBShortenReferences><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><VBReformatCode>True</VBReformatCode><VBFormatDocComments>True</VBFormatDocComments><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSharpReformatComments>True</CSharpReformatComments><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><IDEA_SETTINGS>&lt;profile version="1.0"&gt; &lt;option name="myName" value="Kenny's Kleanup" /&gt; @@ -74,84 +86,136 @@ &lt;/profile&gt;</RIDER_SETTINGS></Profile> Kenny's Kleanup Kenny's Kleanup - False + False Required - Required - Required + Required + Required Required ExpressionBody - ExpressionBody + ExpressionBody ExpressionBody - False + False True - True - True - True - True - True - True - True + True + True + True + True + True + True + True True - True - TOGETHER_SAME_LINE - True - True - True - NO_INDENT - True - True - True - False + True + TOGETHER_SAME_LINE + True + True + True + NO_INDENT + True + True + True + False True - 90 + 90 1 - 10000 + 10000 COMPACT True - True + True True True - NEVER + NEVER NEVER - False - ALWAYS - IF_OWNER_IS_SINGLE_LINE - NEVER + False + ALWAYS + IF_OWNER_IS_SINGLE_LINE + NEVER False - True - True + True + True False - True - True - CHOP_IF_LONG + True + True + CHOP_IF_LONG CHOP_IF_LONG - False - True - True - True - True - True - True - False + False + True + True + True + True + True + True + False CHOP_IF_LONG CHOP_IF_LONG - CHOP_IF_LONG - CHOP_IF_LONG + CHOP_IF_LONG + CHOP_IF_LONG CHOP_ALWAYS - CHOP_IF_LONG - RemoveIndent - RemoveIndent - False - ByFirstAttr + CHOP_IF_LONG + RemoveIndent + RemoveIndent + False + ByFirstAttr 150 False False False False True - False - True + False + True True False False @@ -159,9 +223,14 @@ False True False - True - True - True - True + True + True + True + True True + True From beb5ab13e14c12f80584515cb7d01e8207433ff0 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 06:59:13 +0200 Subject: [PATCH 06/20] ci: add release-on-nuget.yml Signed-off-by: Kenny Pflug --- .github/actions/cache-nuget/action.yml | 15 ++++++++++++ .github/workflows/build-and-test.yml | 7 +----- .github/workflows/release-on-nuget.yml | 32 ++++++++++++++++++++++++++ Light.TemporaryStreams.sln | 10 ++++++++ 4 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 .github/actions/cache-nuget/action.yml create mode 100644 .github/workflows/release-on-nuget.yml diff --git a/.github/actions/cache-nuget/action.yml b/.github/actions/cache-nuget/action.yml new file mode 100644 index 0000000..9268650 --- /dev/null +++ b/.github/actions/cache-nuget/action.yml @@ -0,0 +1,15 @@ +name: 'Cache NuGet Packages' +description: 'Sets up caching for NuGet packages to speed up builds' +author: 'Kenny Pflug' + +runs: + using: 'composite' + steps: + - name: Cache NuGet packages + uses: actions/cache@v4 + shell: bash + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + nuget-${{ runner.os }}- diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 83e3b95..0407b6f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,12 +17,7 @@ jobs: with: global-json-file: ./global.json - name: Cache NuGet packages - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - nuget-${{ runner.os }}- + uses: ./.github/actions/cache-nuget - name: Restore dependencies run: dotnet restore ./Light.TemporaryStreams.sln /p:ContinuousIntegrationBuild=true - name: Build diff --git a/.github/workflows/release-on-nuget.yml b/.github/workflows/release-on-nuget.yml new file mode 100644 index 0000000..fd96790 --- /dev/null +++ b/.github/workflows/release-on-nuget.yml @@ -0,0 +1,32 @@ +name: Release on NuGet + +on: + release: + types: [ published ] + workflow_dispatch: + +jobs: + release-on-nuget: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: ./global.json + - name: Cache NuGet packages + uses: ./.github/actions/cache-nuget + - name: Prepare SNK file + env: + SNK: ${{ secrets.SNK }} + run: echo "$SNK" > base64 --decode > Light.TemporaryStreams.snk + - name: Create NuGet packages + run: dotnet pack ./Light.TemporaryStreams.sln --configuration Release /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=./Light.TemporaryStreams.snk /p:ContinuousIntegrationBuild=true + - name: Delete SNK file + run: rm ./Light.TemporaryStreams.snk + - name: Push NuGet packages + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: dotnet nuget push "./src/**/*.nupkg" --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json diff --git a/Light.TemporaryStreams.sln b/Light.TemporaryStreams.sln index bf0074f..a2674d5 100644 --- a/Light.TemporaryStreams.sln +++ b/Light.TemporaryStreams.sln @@ -22,6 +22,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{05D48EEC-A2AB-4143-9533-A633E7B25EA3}" ProjectSection(SolutionItems) = preProject .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml + .github\workflows\release-on-nuget.yml = .github\workflows\release-on-nuget.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{048D0C61-6CF0-43E6-B7DB-1FDD8F791D57}" @@ -38,6 +39,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Light.TemporaryStreams", "s EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Light.TemporaryStreams.Tests", "tests\Light.TemporaryStreams.Tests\Light.TemporaryStreams.Tests.csproj", "{93CCDAD2-A16A-4BA3-A805-4FC7C4B518C8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "actions", "actions", "{68BCD857-ACA2-4D68-A0D5-1B0E92FD444B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cache-nuget", "cache-nuget", "{1D23A697-FFF5-4FA2-B540-5F8CBBF6863C}" + ProjectSection(SolutionItems) = preProject + .github\actions\cache-nuget\action.yml = .github\actions\cache-nuget\action.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -106,5 +114,7 @@ Global {4D6B4F48-1E4B-4ACA-9F32-829442DB5E56} = {DA93B299-75F5-4A49-B2A6-4A1247047E5E} {61725DD8-D81C-4EC0-A0A1-63D96A87DAC1} = {048D0C61-6CF0-43E6-B7DB-1FDD8F791D57} {93CCDAD2-A16A-4BA3-A805-4FC7C4B518C8} = {DA93B299-75F5-4A49-B2A6-4A1247047E5E} + {68BCD857-ACA2-4D68-A0D5-1B0E92FD444B} = {677E4EE1-7062-46AB-81FF-8D20E9316ED6} + {1D23A697-FFF5-4FA2-B540-5F8CBBF6863C} = {68BCD857-ACA2-4D68-A0D5-1B0E92FD444B} EndGlobalSection EndGlobal From dc832cd6b28bd5886a89c9aceb68a8e1d12a92e5 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 07:01:18 +0200 Subject: [PATCH 07/20] chore: add Light.TemporaryStreams.Public.snk Signed-off-by: Kenny Pflug --- Light.TemporaryStreams.Public.snk | Bin 0 -> 160 bytes Light.TemporaryStreams.sln | 1 + 2 files changed, 1 insertion(+) create mode 100644 Light.TemporaryStreams.Public.snk diff --git a/Light.TemporaryStreams.Public.snk b/Light.TemporaryStreams.Public.snk new file mode 100644 index 0000000000000000000000000000000000000000..b7264d7ab4cad811f8575e151d384a6a3b9d23af GIT binary patch literal 160 zcmV;R0AK$ABme*efB*oL000060ssI2Bme+XQ$aBR1ONa50097@o@Psq%Mn`_L*P^) z7kplU0YMkEJzQu*Ov)ScnX%_Fip0SSaNaANwLW3dUPa#3CbP0aTk^W1ZqQ}0AC$)2-!YcY5-2Fh OO#nj-s0f+0+PUDBhCl59 literal 0 HcmV?d00001 diff --git a/Light.TemporaryStreams.sln b/Light.TemporaryStreams.sln index a2674d5..f3f4a81 100644 --- a/Light.TemporaryStreams.sln +++ b/Light.TemporaryStreams.sln @@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt Light.TemporaryStreams.sln.DotSettings = Light.TemporaryStreams.sln.DotSettings Directory.Packages.props = Directory.Packages.props global.json = global.json + Light.TemporaryStreams.Public.snk = Light.TemporaryStreams.Public.snk EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{677E4EE1-7062-46AB-81FF-8D20E9316ED6}" From 4c76f58780b4699314a53b11f16690dabe11b6e8 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 07:02:02 +0200 Subject: [PATCH 08/20] chore: ignore Light.TemporaryStreams.snk Signed-off-by: Kenny Pflug --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 739ff4d..25b1271 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ bin/ obj/ *.suo -*.user \ No newline at end of file +*.user +Light.TemporaryStreams.snk From dc1d151be9fec5ac68f6487b585e9d5def7e8521 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 07:17:14 +0200 Subject: [PATCH 09/20] ci: enable TreatWarningsAsErrors for Release builds Signed-off-by: Kenny Pflug --- Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Build.props b/Directory.Build.props index 2a0d29e..f8bcf3f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,6 +3,7 @@ net8.0 enable disable + true true true Kenny Pflug From a5b6ae395b143e39be7a36b45206aed32a8cd301 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 07:17:30 +0200 Subject: [PATCH 10/20] ci: enable NuGet Package Icon Signed-off-by: Kenny Pflug --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f9f7091..3984beb 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -16,9 +16,9 @@ snupkg true streaming;memory-management;form-file + light-logo.png readme.md MIT - https://github.com/feO2x/Light.TemporaryStreams/blob/main/LICENSE From 45e177621ccfb07b4b036ea8f151c245f9b8c625 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 07:17:53 +0200 Subject: [PATCH 11/20] ci: fix AssemblyOriginatorKeyFile path in release-on-nuget.yml Signed-off-by: Kenny Pflug --- .github/workflows/release-on-nuget.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-on-nuget.yml b/.github/workflows/release-on-nuget.yml index fd96790..bb0aab9 100644 --- a/.github/workflows/release-on-nuget.yml +++ b/.github/workflows/release-on-nuget.yml @@ -23,7 +23,8 @@ jobs: SNK: ${{ secrets.SNK }} run: echo "$SNK" > base64 --decode > Light.TemporaryStreams.snk - name: Create NuGet packages - run: dotnet pack ./Light.TemporaryStreams.sln --configuration Release /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=./Light.TemporaryStreams.snk /p:ContinuousIntegrationBuild=true + # AssemblyOriginatorKeyFile must be a relative path from the csproj file that is being built, hence the ../../ + run: dotnet pack ./Light.TemporaryStreams.sln --configuration Release /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=../../Light.TemporaryStreams.snk /p:ContinuousIntegrationBuild=true - name: Delete SNK file run: rm ./Light.TemporaryStreams.snk - name: Push NuGet packages From 71e4e99bfe06327273e1bbbbdbe6941a0206faca Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 07:18:49 +0200 Subject: [PATCH 12/20] chore: add readme to solution items Signed-off-by: Kenny Pflug --- Light.TemporaryStreams.sln | 1 + 1 file changed, 1 insertion(+) diff --git a/Light.TemporaryStreams.sln b/Light.TemporaryStreams.sln index f3f4a81..a49c9b0 100644 --- a/Light.TemporaryStreams.sln +++ b/Light.TemporaryStreams.sln @@ -16,6 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt Directory.Packages.props = Directory.Packages.props global.json = global.json Light.TemporaryStreams.Public.snk = Light.TemporaryStreams.Public.snk + readme.md = readme.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{677E4EE1-7062-46AB-81FF-8D20E9316ED6}" From 267a7f7af2feaf0eac35091d25f7d0170639c068 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 08:14:50 +0200 Subject: [PATCH 13/20] test: add additional Position == 0 checks in plugin test cases Signed-off-by: Kenny Pflug --- .../CopyToTemporaryStreamTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs b/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs index 59d6afd..657442d 100644 --- a/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs +++ b/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs @@ -289,7 +289,7 @@ public static async Task CopyToTemporaryStreamAsync_WithHashingPlugin_ShouldUseC ); // Assert - temporaryStream.Should().NotBeNull(); + temporaryStream.Position.Should().Be(0); temporaryStream.Length.Should().Be(sourceData.Length); hashingPlugin.GetHash(nameof(SHA1)).Should().Be(Convert.ToBase64String(SHA1.HashData(sourceData))); } @@ -312,7 +312,7 @@ public static async Task CopyToTemporaryStreamAsync_WithHashingPlugin_ShouldForw ); // Assert - temporaryStream.Should().NotBeNull(); + temporaryStream.Position.Should().Be(0); temporaryStream.IsFileBased.Should().BeTrue(); temporaryStream.Length.Should().Be(sourceData.Length); temporaryStream.GetUnderlyingFilePath().Should().Be(filePath); From e59d0eb430b739377155c129c83a22e9eb15bfd5 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 08:15:21 +0200 Subject: [PATCH 14/20] docs: adjusted first part of readme.md Signed-off-by: Kenny Pflug --- readme.md | 165 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 97 insertions(+), 68 deletions(-) diff --git a/readme.md b/readme.md index 8bfe50e..8807b49 100644 --- a/readme.md +++ b/readme.md @@ -1,19 +1,23 @@ # Light.TemporaryStreams 🌊 -[![NuGet Badge](https://img.shields.io/nuget/v/Light.TemporaryStreams.svg)](https://www.nuget.org/packages/Light.TemporaryStreams/) -[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/feO2x/Light.TemporaryStreams/blob/main/LICENSE) +[![License](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](https://github.com/feO2x/Light.TemporaryStreams/blob/main/LICENSE) +[![NuGet](https://img.shields.io/badge/NuGet-1.0.0-blue.svg?style=for-the-badge)](https://www.nuget.org/packages/Light.TemporaryStreams/1.0.0/) +[![Documentation](https://img.shields.io/badge/Docs-Changelog-yellowgreen.svg?style=for-the-badge)](https://github.com/feO2x/Light.GuardClauses/releases) ## Overview 🔍 -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. +Light.TemporaryStreams is a lightweight .NET library that helps you convert non-seekable streams into seekable temporary +streams. A temporary stream is either backed by a memory stream (for input smaller than 80 KB) or a file stream to a +temporary file. This is particularly useful for backend services that receive streams from HTTP requests (e.g., +`application/octet-stream`, custom-parsed `multipart/form-data`) or download files from storage systems for further +processing. ## Key Features ✨ - 🚀 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 +- 🔄 Smart switching between memory-based and file-based streams based on size (similar behavior to ASP.NET Core's + `IFormFile`) - 🧩 Plugin system for extending functionality (e.g., calculating hashes during stream copying) - 🔌 Integration with Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Logging @@ -31,39 +35,76 @@ dotnet add package Light.TemporaryStreams.Core ## Basic Usage 🚀 +First, register the `ITemporaryStreamService` in your dependency injection container: + +```csharp +services.AddTemporaryStreamService(); +``` + +Then, inject the `ITemporaryStreamService` into any class that needs to convert non-seekable streams to seekable +temporary streams: + ```csharp using Light.TemporaryStreams; using System.IO; using System.Threading.Tasks; -// Example: Convert a non-seekable stream to a seekable temporary stream -public async Task MakeStreamSeekable(Stream nonSeekableStream) +public class SomeService { - // Create the temporary stream service - var temporaryStreamService = new TemporaryStreamService(); - - // Copy the non-seekable stream to a temporary stream - var temporaryStream = await temporaryStreamService.CopyToTemporaryStreamAsync(nonSeekableStream); + private readonly ITemporaryStreamService _temporaryStreamService; + private readonly IS3UploadClient _s3UploadClient; - // The temporary stream is seekable and positioned at the beginning - temporaryStream.Position = 0; + public SomeService(ITemporaryStreamService temporaryStreamService, IS3UploadClient s3UploadClient) + { + _temporaryStreamService = temporaryStreamService; + _s3UploadClient = s3UploadClient; + } - // 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 + public async Task ProcessStreamAsync( + Stream nonSeekableStream, + CancellationToken cancellationToken = default + ) + { + // A temporary stream is either backed by a memory stream or a file stream + // and thus seekable. + await using var temporaryStream = + await _temporaryStreamService.CopyToTemporaryStreamAsync(nonSeekableStream, cancellationToken); + + // Do something here with the temporary stream (analysis, processing, etc.) + using (var pdf = new PdfProcessor(temporaryStream, leaveOpen: true)) + { + var emptyOrIrrelevantPages = pdf.DetermineEmptyOrIrrelevantPages(); + pdf.RemovePages(emptyOrIrrelevantPages); + } + + // Once you are done with processing, you can easily reset the stream to Position 0. + // You can also use resilience patterns here and always reset the stream + // for each upload attempt. + temporaryStream.ResetStreamPosition(); + await _s3UploadClient.UploadAsync(temporaryStream); + + // When the temporary stream is disposed, it will automatically delete the + // underlying file if necessary. No need to worry about manual cleanup. + // This is also great when a temporary stream is returned in an + // MVC Controller action or in Minimal API endpoint. + } } ``` ## How It Works 🛠️ -### TemporaryStream +### Smart Memory Usage A `TemporaryStream` is a wrapper around either: - 🧠 A `MemoryStream` (for smaller files, less than 80 KB by default) - 📄 A `FileStream` to a temporary file (for larger files) -This approach is similar to how `IFormFile` works in ASP.NET Core. +This approach is similar to how `IFormFile` works in ASP.NET Core. You can adjust the threshold for using file streams +using the `TemporaryStreamServiceOptions.FileThresholdInBytes` property. + +Use the `TemporaryStream.IsFileBased` property to check if the stream is backed by a file or a memory stream. Use +`TemporaryStream.TryGetUnderlyingFilePath` or `TemporaryStream.GetUnderlyingFilePath` to get the absolute file path. ### Automatic Cleanup @@ -72,64 +113,51 @@ 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 -## DI Integration 🔌 - -```csharp -using Light.TemporaryStreams; -using Microsoft.Extensions.DependencyInjection; - -public void ConfigureServices(IServiceCollection services) -{ - // Register the temporary stream service with default options - services.AddTemporaryStreamService(); +You can adjust this behavior using the `TemporaryStreamServiceOptions.DisposeBehavior` property. - // 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; - }); -} -``` +### Temporary File Management -## Using Plugins 🧩 +By default, temporary files are created using `Path.GetTempFileName()`. You can pass your own file path by providing a +value to the optional `filePath` argument of `ITemporaryStreamService.CreateTemporaryStream` or the +`CopyToTemporaryStreamAsync` extension methods. -### Hash Calculation During Stream Copy +By default, Light.TemporaryStreams uses `FileMode.Create`, thus files are either created or overwritten. You can adjust +this behavior using the `TemporaryStreamServiceOptions.FileStreamOptions` property. -```csharp -using Light.TemporaryStreams; -using Light.TemporaryStreams.Hashing; -using System.Collections.Immutable; -using System.Security.Cryptography; -using System.Threading.Tasks; +### Temporary Stream Service Options -public async Task<(TemporaryStream Stream, string Md5Hash, string Sha256Hash)> CopyStreamWithHashing(Stream source) -{ - var temporaryStreamService = new TemporaryStreamService(); +When you call `services.AddTemporaryStreamService()`, a singleton instance of `TemporaryStreamServiceOptions` is +registered with the DI container. This default instance is used when you do not explicitly pass a reference to +`ITemporaryStreamService.CreateTemporaryStream` or `CopyToTemporaryStreamAsync`. - // Create hash calculators for MD5 and SHA256 - var md5Calculator = new CopyToHashCalculator(MD5.Create(), "MD5"); - var sha256Calculator = new CopyToHashCalculator(SHA256.Create(), "SHA256"); +However, if you want to deviate from the defaults in certain use cases, simply instantiate your own and pass them to the +`options` argument of aforementioned methods. - // Create the hashing plugin with both calculators - var hashingPlugin = new HashingPlugin( - ImmutableArray.Create(md5Calculator, sha256Calculator) - ); +## Plugins 🧩 - // Copy the stream and calculate hashes in one go - var temporaryStream = await temporaryStreamService.CopyToTemporaryStreamAsync( - source, - ImmutableArray.Create(hashingPlugin) - ); +`CopyToTemporaryStreamAsync` supports a plugin system that allows you to extend the behavior of the stream copying +process. Light.TemporaryStreams comes with a `HashingPlugin` to calculate hashes. And, you can create your own plugins +by implementing the `ICopyToTemporaryStreamPlugin` interface. - // Get the calculated hashes - string md5Hash = hashingPlugin.GetHash("MD5"); - string sha256Hash = hashingPlugin.GetHash("SHA256"); +### Basic Usage of HashingPlugin - return (temporaryStream, md5Hash, sha256Hash); -} +```csharp +// You can simply pass any instance of System.Security.Cryptography.HashAlgorithm +// to the hashing plugin constructor. They will be disposed of when the hashingplugin is disposed of. +await using var hashingPlugin = new HashingPlugin([SHA1.Create(), MD5.Create()]); +await using var temporaryStream = await _temporaryStreamService + .CopyToTemporaryStreamAsync(stream, [hashingPlugin], cancellationToken: cancellationToken); + +// After copying is done, you can call GetHash to obtain the hash as a base64 string +// or GetHashArray to obtain the hash in its raw byte array form. +// Calling these methods before `CopyToTemporaryStreamAsync` has completed will result +// in an InvalidOperationException. +string sha1Base64Hash = hashingPlugin.GetHash(nameof(SHA1)); +byte[] md5HashArray = hashingPlugin.GetHashArray(nameof(MD5)); ``` +### Hexadecimal Hashes via CopyToHashCalculator + ## When To Use Light.TemporaryStreams 🤔 ### When Processing Files in ASP.NET Core Without IFormFile @@ -256,8 +284,9 @@ Microsoft.Extensions.DependencyInjection. ## Contributing 🤝 -Contributions are welcome! Feel free to submit issues and pull requests. +Contributions are welcome! First, create an issue Feel free to submit issues and pull requests. ## License 📜 -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License - see +the [LICENSE](https://github.com/feO2x/Light.TemporaryStreams/blob/main/LICENSE) file for details. From 4037925b7e626d088ec2953ac8dc1b56f80c67f4 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 08:18:17 +0200 Subject: [PATCH 15/20] docs: fixed basic usage example in readme.md Signed-off-by: Kenny Pflug --- readme.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 8807b49..b0ac55f 100644 --- a/readme.md +++ b/readme.md @@ -60,14 +60,11 @@ public class SomeService _s3UploadClient = s3UploadClient; } - public async Task ProcessStreamAsync( - Stream nonSeekableStream, - CancellationToken cancellationToken = default - ) + public async Task ProcessStreamAsync(Stream nonSeekableStream, CancellationToken cancellationToken = default) { // A temporary stream is either backed by a memory stream or a file stream // and thus seekable. - await using var temporaryStream = + await using TemporaryStream temporaryStream = await _temporaryStreamService.CopyToTemporaryStreamAsync(nonSeekableStream, cancellationToken); // Do something here with the temporary stream (analysis, processing, etc.) From 62da429f08f44e286fb236c87e1e5f6d61cfce06 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 22:21:54 +0200 Subject: [PATCH 16/20] docs: finished readme.md Signed-off-by: Kenny Pflug --- readme.md | 131 ++++++++++++------------------------------------------ 1 file changed, 28 insertions(+), 103 deletions(-) diff --git a/readme.md b/readme.md index b0ac55f..6557fcc 100644 --- a/readme.md +++ b/readme.md @@ -35,7 +35,7 @@ dotnet add package Light.TemporaryStreams.Core ## Basic Usage 🚀 -First, register the `ITemporaryStreamService` in your dependency injection container: +First, register the `ITemporaryStreamService` and other dependencies of Light.TemporaryStreams with your dependency injection container: ```csharp services.AddTemporaryStreamService(); @@ -67,7 +67,8 @@ public class SomeService await using TemporaryStream temporaryStream = await _temporaryStreamService.CopyToTemporaryStreamAsync(nonSeekableStream, cancellationToken); - // Do something here with the temporary stream (analysis, processing, etc.) + // Do something here with the temporary stream (analysis, processing, etc.). + // For example, your code base has a PdfProcessor that requires a seekable stream. using (var pdf = new PdfProcessor(temporaryStream, leaveOpen: true)) { var emptyOrIrrelevantPages = pdf.DetermineEmptyOrIrrelevantPages(); @@ -95,7 +96,7 @@ public class SomeService A `TemporaryStream` is a wrapper around either: - 🧠 A `MemoryStream` (for smaller files, less than 80 KB by default) -- 📄 A `FileStream` to a temporary file (for larger files) +- 📄 A `FileStream` to a temporary file (for 80 KB or larger files) This approach is similar to how `IFormFile` works in ASP.NET Core. You can adjust the threshold for using file streams using the `TemporaryStreamServiceOptions.FileThresholdInBytes` property. @@ -128,19 +129,19 @@ registered with the DI container. This default instance is used when you do not `ITemporaryStreamService.CreateTemporaryStream` or `CopyToTemporaryStreamAsync`. However, if you want to deviate from the defaults in certain use cases, simply instantiate your own and pass them to the -`options` argument of aforementioned methods. +`options` argument of aforementioned methods. The `TemporaryStreamServiceOptions` class is an immutable record. ## Plugins 🧩 `CopyToTemporaryStreamAsync` supports a plugin system that allows you to extend the behavior of the stream copying -process. Light.TemporaryStreams comes with a `HashingPlugin` to calculate hashes. And, you can create your own plugins +process. Light.TemporaryStreams comes with a `HashingPlugin` to calculate hashes. You can also create your own plugins by implementing the `ICopyToTemporaryStreamPlugin` interface. ### Basic Usage of HashingPlugin ```csharp // You can simply pass any instance of System.Security.Cryptography.HashAlgorithm -// to the hashing plugin constructor. They will be disposed of when the hashingplugin is disposed of. +// to the hashing plugin constructor. They will be disposed of when the hashingPlugin is disposed of. await using var hashingPlugin = new HashingPlugin([SHA1.Create(), MD5.Create()]); await using var temporaryStream = await _temporaryStreamService .CopyToTemporaryStreamAsync(stream, [hashingPlugin], cancellationToken: cancellationToken); @@ -155,104 +156,25 @@ byte[] md5HashArray = hashingPlugin.GetHashArray(nameof(MD5)); ### Hexadecimal Hashes via CopyToHashCalculator -## When To Use Light.TemporaryStreams 🤔 - -### When Processing Files in ASP.NET Core Without IFormFile - -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](https://andrewlock.net/reading-json-and-binary-data-from-multipart-form-data-sections-in-aspnetcore/) -for an example of when this might be needed. - -### Example: Processing a File Upload with JSON Metadata +The `HashAlgorithm` instances passed to the `HashingPlugin` constructor in the previous example are actually converted into instances of `CopyToHashCalculator` via an implicit conversion operator. You can instantiate this class yourself to have more control over the conversion method that converts a hash byte array into a string as well as the name used to identify the hash calculator. ```csharp -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 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(metadata); - - // Get the file stream - if (!form.Files.TryGetValue("file", out var formFile)) - return BadRequest("File is required"); +var sha1Calculator = new CopyToHashCalculator(SHA1.Create(), HashConversionMethod.UpperHexadecimal, "SHA1"); +var md5Calculator = new CopyToHashCalculator(MD5.Create(), HashConversionMethod.None, "MD5"); +await using var hashingPlugin = new HashingPlugin([sha1Calculator, md5Calculator]); - // 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" }); - } +await using var temporaryStream = await _temporaryStreamService + .CopyToTemporaryStreamAsync(stream, [hashingPlugin], cancellationToken: cancellationToken); - public class FileMetadata - { - public string Name { get; set; } - public string ContentType { get; set; } - } -} +string sha1HexadecimalHash = hashingPlugin.GetHash(nameof(SHA1)); +byte[] md5HashArray = hashingPlugin.GetHashArray(nameof(MD5)); ``` -## Configuring the TemporaryStreamService ⚙️ - -You can customize the behavior of `TemporaryStreamService` through `TemporaryStreamServiceOptions`: - -```csharp -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 -}; +## When To Use Light.TemporaryStreams 🤔 -var service = new TemporaryStreamService(options); -``` +- Your service implements endpoints that receives `application/octet-stream` that you need to process further. +- Your service implements endpoints that receives `multipart/form-data` and you cannot use `IFormFile`, for example because the request has both JSON and binary data. See [this blog post by Andrew Lock](https://andrewlock.net/reading-json-and-binary-data-from-multipart-form-data-sections-in-aspnetcore/) for an example. +- Your service downloads files from storage systems like S3 and processes them further. ## Light.TemporaryStreams.Core vs. Light.TemporaryStreams 🧰 @@ -264,26 +186,29 @@ This package contains the core implementation including: - `TemporaryStreamService` implementation - `TemporaryStream` class - `TemporaryStreamServiceOptions` for configuration -- Extension methods like `CopyToTemporaryStreamAsync` -- Plugin system and existing plugins (like `HashingPlugin`) +- Extension method `CopyToTemporaryStreamAsync` +- Plugin system `ICopyToTemporaryStreamPlugin` and existing plugin `HashingPlugin` ### Light.TemporaryStreams This package builds on Core and adds integration with: - Microsoft.Extensions.DependencyInjection for registering services -- Microsoft.Extensions.Logging for logging events -- Extension methods for `IServiceCollection` to register the service +- Microsoft.Extensions.Logging for logging when a temporary stream cannot be properly deleted 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 +Use Light.TemporaryStreams if you're working in an ASP.NET Core application or any other application supporting Microsoft.Extensions.DependencyInjection. ## Contributing 🤝 -Contributions are welcome! First, create an issue Feel free to submit issues and pull requests. +Contributions are welcome! First, create an issue to discuss your idea. After that, you can submit pull requests. ## License 📜 This project is licensed under the MIT License - see the [LICENSE](https://github.com/feO2x/Light.TemporaryStreams/blob/main/LICENSE) file for details. + +## Let there be... Light 💡 + +![Light Libraries Logo](https://raw.githubusercontent.com/feO2x/Light.GuardClauses/main/Images/light_logo.png) From 9347fe107585415bc47530f8c57c2ed6145f661c Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 22:31:49 +0200 Subject: [PATCH 17/20] ci: fix pipe operator in Prepare SNK file Signed-off-by: Kenny Pflug --- .github/workflows/release-on-nuget.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-on-nuget.yml b/.github/workflows/release-on-nuget.yml index bb0aab9..3c34e02 100644 --- a/.github/workflows/release-on-nuget.yml +++ b/.github/workflows/release-on-nuget.yml @@ -21,7 +21,7 @@ jobs: - name: Prepare SNK file env: SNK: ${{ secrets.SNK }} - run: echo "$SNK" > base64 --decode > Light.TemporaryStreams.snk + run: echo "$SNK" | base64 --decode > Light.TemporaryStreams.snk - name: Create NuGet packages # AssemblyOriginatorKeyFile must be a relative path from the csproj file that is being built, hence the ../../ run: dotnet pack ./Light.TemporaryStreams.sln --configuration Release /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=../../Light.TemporaryStreams.snk /p:ContinuousIntegrationBuild=true From 23aecc2d39a61e4692ab27e824f246d9931493f8 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 22:35:28 +0200 Subject: [PATCH 18/20] ci: remove shell in cache-nuget action Signed-off-by: Kenny Pflug --- .github/actions/cache-nuget/action.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/cache-nuget/action.yml b/.github/actions/cache-nuget/action.yml index 9268650..01b2aea 100644 --- a/.github/actions/cache-nuget/action.yml +++ b/.github/actions/cache-nuget/action.yml @@ -7,7 +7,6 @@ runs: steps: - name: Cache NuGet packages uses: actions/cache@v4 - shell: bash with: path: ~/.nuget/packages key: nuget-${{ runner.os }}-${{ hashFiles('**/packages.lock.json') }} From afeab0fd87cc92bc54763864f48c1a55608ebbae Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 22:37:42 +0200 Subject: [PATCH 19/20] chore: fixed Description typo in Light.TemporaryStreams.csproj Signed-off-by: Kenny Pflug --- src/Light.TemporaryStreams/Light.TemporaryStreams.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Light.TemporaryStreams/Light.TemporaryStreams.csproj b/src/Light.TemporaryStreams/Light.TemporaryStreams.csproj index b3ffb48..6381a73 100644 --- a/src/Light.TemporaryStreams/Light.TemporaryStreams.csproj +++ b/src/Light.TemporaryStreams/Light.TemporaryStreams.csproj @@ -3,7 +3,7 @@ - Provides temporary streams, similar to how IFormFile works in ASP.NET Core. With full integration with Microsoft.Extensions.Logging and Microsoft.Extensions.DependencyInjection. + Provides temporary streams, similar to how IFormFile works in ASP.NET Core. With full integration with Microsoft.Extensions.Logging and Microsoft.Extensions.DependencyInjection. Light.TemporaryStreams 1.0.0 --------------------------------- From 3e789a33f06dd5e1981da0dffe89985c6ccebe78 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Tue, 8 Jul 2025 22:38:58 +0200 Subject: [PATCH 20/20] chore: fix 'library' typo in Light.TemporaryStreams.Core.csproj Signed-off-by: Kenny Pflug --- .../Light.TemporaryStreams.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj b/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj index 44d0c7c..e7c0671 100644 --- a/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj +++ b/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj @@ -3,7 +3,7 @@ - The core libary of Light.TemporaryStreams, containing that base implementation without Microsoft.Extensions.Logging and Microsoft.Extensions.DependencyInjection integration. Provides temporary streams, similar to how IFormFile works in ASP.NET Core. + The core library of Light.TemporaryStreams, containing that base implementation without Microsoft.Extensions.Logging and Microsoft.Extensions.DependencyInjection integration. Provides temporary streams, similar to how IFormFile works in ASP.NET Core. Light.TemporaryStreams.Core 1.0.0 ---------------------------------