diff --git a/.github/actions/cache-nuget/action.yml b/.github/actions/cache-nuget/action.yml
new file mode 100644
index 0000000..01b2aea
--- /dev/null
+++ b/.github/actions/cache-nuget/action.yml
@@ -0,0 +1,14 @@
+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
+ 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..3c34e02
--- /dev/null
+++ b/.github/workflows/release-on-nuget.yml
@@ -0,0 +1,33 @@
+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
+ # 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
+ 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/.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
diff --git a/Directory.Build.props b/Directory.Build.props
index eddcafb..f8bcf3f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,7 +3,13 @@
net8.0
enable
disable
+ true
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/Light.TemporaryStreams.Public.snk b/Light.TemporaryStreams.Public.snk
new file mode 100644
index 0000000..b7264d7
Binary files /dev/null and b/Light.TemporaryStreams.Public.snk differ
diff --git a/Light.TemporaryStreams.sln b/Light.TemporaryStreams.sln
index bf0074f..a49c9b0 100644
--- a/Light.TemporaryStreams.sln
+++ b/Light.TemporaryStreams.sln
@@ -15,6 +15,8 @@ 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
+ readme.md = readme.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{677E4EE1-7062-46AB-81FF-8D20E9316ED6}"
@@ -22,6 +24,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 +41,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 +116,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
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><profile version="1.0">
<option name="myName" value="Kenny's Kleanup" />
@@ -74,84 +86,136 @@
</profile></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
diff --git a/images/light-logo.png b/images/light-logo.png
new file mode 100644
index 0000000..1b016d0
Binary files /dev/null and b/images/light-logo.png differ
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..6557fcc
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,214 @@
+# Light.TemporaryStreams 🌊
+
+[](https://github.com/feO2x/Light.TemporaryStreams/blob/main/LICENSE)
+[](https://www.nuget.org/packages/Light.TemporaryStreams/1.0.0/)
+[](https://github.com/feO2x/Light.GuardClauses/releases)
+
+## Overview 🔍
+
+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 (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
+
+## 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 🚀
+
+First, register the `ITemporaryStreamService` and other dependencies of Light.TemporaryStreams with 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;
+
+public class SomeService
+{
+ private readonly ITemporaryStreamService _temporaryStreamService;
+ private readonly IS3UploadClient _s3UploadClient;
+
+ public SomeService(ITemporaryStreamService temporaryStreamService, IS3UploadClient s3UploadClient)
+ {
+ _temporaryStreamService = temporaryStreamService;
+ _s3UploadClient = s3UploadClient;
+ }
+
+ 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 TemporaryStream temporaryStream =
+ await _temporaryStreamService.CopyToTemporaryStreamAsync(nonSeekableStream, cancellationToken);
+
+ // 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();
+ 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 🛠️
+
+### 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 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.
+
+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
+
+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
+
+You can adjust this behavior using the `TemporaryStreamServiceOptions.DisposeBehavior` property.
+
+### Temporary File Management
+
+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.
+
+By default, Light.TemporaryStreams uses `FileMode.Create`, thus files are either created or overwritten. You can adjust
+this behavior using the `TemporaryStreamServiceOptions.FileStreamOptions` property.
+
+### Temporary Stream Service Options
+
+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`.
+
+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. 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. 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.
+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
+
+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
+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]);
+
+await using var temporaryStream = await _temporaryStreamService
+ .CopyToTemporaryStreamAsync(stream, [hashingPlugin], cancellationToken: cancellationToken);
+
+string sha1HexadecimalHash = hashingPlugin.GetHash(nameof(SHA1));
+byte[] md5HashArray = hashingPlugin.GetHashArray(nameof(MD5));
+```
+
+## When To Use Light.TemporaryStreams 🤔
+
+- 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 🧰
+
+### Light.TemporaryStreams.Core
+
+This package contains the core implementation including:
+
+- `ITemporaryStreamService` interface
+- `TemporaryStreamService` implementation
+- `TemporaryStream` class
+- `TemporaryStreamServiceOptions` for configuration
+- 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 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 supporting
+Microsoft.Extensions.DependencyInjection.
+
+## Contributing 🤝
+
+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 💡
+
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 4d31c2b..3984beb 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -8,6 +8,25 @@
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
+ light-logo.png
+ readme.md
+ MIT
+
+
+
+
+
+
+
+
+
diff --git a/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj b/src/Light.TemporaryStreams.Core/Light.TemporaryStreams.Core.csproj
index 17f6863..e7c0671 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 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
+ ---------------------------------
+
+ - 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/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
{
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..6381a73 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
diff --git a/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs b/tests/Light.TemporaryStreams.Core.Tests/CopyToTemporaryStreamTests.cs
index 65da05d..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);
@@ -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);