├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── Tests.yaml ├── .gitignore ├── LICENSE.txt ├── README.md ├── Sharpify.sln ├── build.sh ├── demos └── Calc │ ├── Calc.sln │ ├── Calc │ ├── Calc.csproj │ ├── Commands │ │ ├── AddCommand.cs │ │ ├── DivideCommand.cs │ │ ├── MultiplyCommand.cs │ │ └── SubtractCommand.cs │ ├── GlobalUsings.cs │ └── Program.cs │ ├── README.md │ └── yt.video.thumbnail.png ├── docs └── 10.-Sharpify.Data.md ├── src ├── Sharpify.CommandLineInterface │ ├── ArgumentsAccess.cs │ ├── ArgumentsAccessMultiple.cs │ ├── ArgumentsCore.cs │ ├── CHANGELOG.md │ ├── CHANGELOGLATEST.md │ ├── CliBuilder.cs │ ├── CliMetadata.cs │ ├── CliRunner.cs │ ├── CliRunnerConfiguration.cs │ ├── Command.cs │ ├── ConfigurationEnums.cs │ ├── Extensions.cs │ ├── OutputHelper.cs │ ├── Parser.cs │ ├── README.md │ ├── Sharpify.CommandLineInterface.csproj │ └── SynchronousCommand.cs ├── Sharpify.Data │ ├── Build.txt │ ├── CHANGELOG.md │ ├── CHANGELOGLATEST.md │ ├── DataChangeType.cs │ ├── DataChangedEventArgs.cs │ ├── DatabaseBase.cs │ ├── DatabaseConfiguration.cs │ ├── DatabaseReads.cs │ ├── DatabaseRemovals.cs │ ├── DatabaseSerialization.cs │ ├── DatabaseUpserts.cs │ ├── FlexibleDatabaseFilter{T}.cs │ ├── GlobalSuppressions.cs │ ├── Helper.cs │ ├── IDatabaseFilter{T}.cs │ ├── IFilterable{T}.cs │ ├── MemoryPackDatabaseFilter{T}.cs │ ├── README.md │ ├── Serializers │ │ ├── AbstractSerializer.cs │ │ ├── DisabledSerializers.cs │ │ ├── EncryptedSerializer.cs │ │ ├── IgnoreCaseEncryptedSerializer.cs │ │ ├── IgnoreCaseSerializer.cs │ │ └── Serializer.cs │ └── Sharpify.Data.csproj └── Sharpify │ ├── AesProvider.cs │ ├── Build.txt │ ├── CHANGELOG.md │ ├── CHANGELOGLATEST.md │ ├── CollectionExtensions.cs │ ├── Collections │ ├── BufferWrapper{T}.cs │ ├── LazyLocalPersistentDictionary.cs │ ├── LocalPersistentDictionary.cs │ ├── PersistentDictionary.cs │ ├── RentedBufferWriter{T}.cs │ ├── SortedList.cs │ └── StringBuffer.cs │ ├── Either.cs │ ├── ExtensionsHeader.cs │ ├── IAsyncAction.cs │ ├── InternalHelper.cs │ ├── MonitoredSerializableObject{T}.cs │ ├── ParallelExtensions.cs │ ├── README.md │ ├── Result.cs │ ├── Routines │ ├── AsyncRoutine.cs │ ├── Routine.cs │ └── RoutineOptions.cs │ ├── SerializableObjectEventArgs.cs │ ├── SerializableObject{T}.cs │ ├── Sharpify.csproj │ ├── Sharpify.csproj.DotSettings │ ├── StringExtensions.cs │ ├── ThreadSafe.cs │ ├── UnmanangedExtensions.cs │ ├── UnsafeSpanAccessor.cs │ ├── UtilsDateAndTime.cs │ ├── UtilsEnv.cs │ ├── UtilsHeader.cs │ ├── UtilsMathematics.cs │ ├── UtilsStrings.cs │ └── UtilsUnsafe.cs └── tests ├── Sharpify.CommandLineInterface.Tests ├── AddCommand.cs ├── ArgumentsIsolatedTests.cs ├── AssemblyInfo.cs ├── CliBuilderTests.cs ├── EchoCommand.cs ├── GlobalUsings.cs ├── Helper.cs ├── ParserArgumentsTests.cs ├── Sharpify.CommandLineInterface.Tests.csproj └── SingleCommand.cs ├── Sharpify.Data.Tests ├── AssemblyInfo.cs ├── Color.cs ├── DatabaseEncryptedIgnoreCaseTests.cs ├── DatabaseEncryptedTests.cs ├── DatabaseIgnoreCaseTests.cs ├── DatabaseTests.cs ├── Dog.cs ├── FactoryResult.cs ├── GlobalUsings.cs ├── HelperTests.cs ├── JsonContext.cs ├── Person.cs ├── Sharpify.Data.Tests.csproj └── User.cs └── Sharpify.Tests ├── AesProviderTests.cs ├── AssemblyInfo.cs ├── CollectionExtensionsTests.cs ├── Collections ├── BufferWrapperTests.cs ├── LazyLocalPersistentDictionaryTests.cs ├── LocalPersistentDictionaryTests.cs ├── RentedBufferWriterTests.cs ├── SortedListTests.cs ├── StringBuffersTests.cs └── TestLocalPersistentDictionary.cs ├── EitherTests.cs ├── ParallelExtensionsTests.cs ├── ResultTests.cs ├── Routines ├── AsyncRoutineTests.cs └── RoutineTests.cs ├── SerializableObjectTests.cs ├── Sharpify.Tests.csproj ├── StringExtensionsTests.cs ├── TempFile.cs ├── ThreadSafeTests.cs ├── UnmanagedExtensionsTests.cs ├── UnsafeSpanIteratorTests.cs ├── Usings.cs ├── UtilsDateAndTimeTests.cs ├── UtilsMathematicsTests.cs ├── UtilsStringsTests.cs └── UtilsUnsafeTests.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/Tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test-sharpify: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest, windows-latest, macos-latest] 15 | configuration: [Debug, Release] 16 | 17 | env: 18 | # Define the path to project and test project 19 | PROJECT: src/Sharpify/Sharpify.csproj 20 | TEST_PROJECT: tests/Sharpify.Tests/Sharpify.Tests.csproj 21 | 22 | steps: 23 | # 1. Checkout the repository code 24 | - name: Checkout Repository 25 | uses: actions/checkout@v4 26 | 27 | # 2. Cache NuGet packages 28 | - name: Cache NuGet Packages 29 | uses: actions/cache@v4 30 | with: 31 | path: ~/.nuget/packages 32 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} 33 | restore-keys: | 34 | ${{ runner.os }}-nuget- 35 | 36 | # 3. Setup .NET 37 | - name: Setup .NET 38 | uses: actions/setup-dotnet@v4 39 | with: 40 | dotnet-version: 9.0.x 41 | 42 | # 4. Clean 43 | - name: Clean 44 | run: | 45 | dotnet clean ${{ env.PROJECT }} -c ${{ matrix.configuration }} 46 | dotnet clean ${{ env.TEST_PROJECT }} -c ${{ matrix.configuration }} 47 | 48 | # 5. Run Unit Tests 49 | - name: Run Unit Tests 50 | run: dotnet test ${{ env.TEST_PROJECT }} -c ${{ matrix.configuration }} 51 | 52 | test-sharpify-data: 53 | runs-on: ${{ matrix.os }} 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | os: [ubuntu-latest, windows-latest, macos-latest] 58 | configuration: [Debug, Release] 59 | 60 | env: 61 | # Define the path to project and test project 62 | PROJECT: src/Sharpify.Data/Sharpify.Data.csproj 63 | TEST_PROJECT: tests/Sharpify.Data.Tests/Sharpify.Data.Tests.csproj 64 | 65 | steps: 66 | # 1. Checkout the repository code 67 | - name: Checkout Repository 68 | uses: actions/checkout@v4 69 | 70 | # 2. Cache NuGet packages 71 | - name: Cache NuGet Packages 72 | uses: actions/cache@v4 73 | with: 74 | path: ~/.nuget/packages 75 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} 76 | restore-keys: | 77 | ${{ runner.os }}-nuget- 78 | 79 | # 3. Setup .NET 80 | - name: Setup .NET 81 | uses: actions/setup-dotnet@v4 82 | with: 83 | dotnet-version: 9.0.x 84 | 85 | # 4. Clean 86 | - name: Clean 87 | run: | 88 | dotnet clean ${{ env.PROJECT }} -c ${{ matrix.configuration }} 89 | dotnet clean ${{ env.TEST_PROJECT }} -c ${{ matrix.configuration }} 90 | 91 | # 5. Run Unit Tests 92 | - name: Run Unit Tests 93 | run: dotnet test ${{ env.TEST_PROJECT }} -c ${{ matrix.configuration }} 94 | 95 | test-sharpify-cli: 96 | runs-on: ${{ matrix.os }} 97 | strategy: 98 | fail-fast: false 99 | matrix: 100 | os: [ubuntu-latest, windows-latest, macos-latest] 101 | configuration: [Debug, Release] 102 | 103 | env: 104 | # Define the path to project and test project 105 | PROJECT: src/Sharpify.CommandLineInterface/Sharpify.CommandLineInterface.csproj 106 | TEST_PROJECT: tests/Sharpify.CommandLineInterface.Tests/Sharpify.CommandLineInterface.Tests.csproj 107 | 108 | steps: 109 | # 1. Checkout the repository code 110 | - name: Checkout Repository 111 | uses: actions/checkout@v4 112 | 113 | # 2. Cache NuGet packages 114 | - name: Cache NuGet Packages 115 | uses: actions/cache@v4 116 | with: 117 | path: ~/.nuget/packages 118 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} 119 | restore-keys: | 120 | ${{ runner.os }}-nuget- 121 | 122 | # 3. Setup .NET 123 | - name: Setup .NET 124 | uses: actions/setup-dotnet@v4 125 | with: 126 | dotnet-version: 9.0.x 127 | 128 | # 4. Clean 129 | - name: Clean 130 | run: | 131 | dotnet clean ${{ env.PROJECT }} -c ${{ matrix.configuration }} 132 | dotnet clean ${{ env.TEST_PROJECT }} -c ${{ matrix.configuration }} 133 | 134 | # 5. Run Unit Tests 135 | - name: Run Unit Tests 136 | run: dotnet test ${{ env.TEST_PROJECT }} -c ${{ matrix.configuration }} -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [David Shnayder] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sharpify.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33516.290 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sharpify", "src\Sharpify\Sharpify.csproj", "{7E6960CB-A0F2-4AE4-B383-98763AB03E75}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sharpify.Data", "src\Sharpify.Data\Sharpify.Data.csproj", "{13E844B7-D575-489D-B1E4-97F30B948227}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sharpify.CommandLineInterface", "src\Sharpify.CommandLineInterface\Sharpify.CommandLineInterface.csproj", "{85B4CC1F-D9F3-4EE7-BB8D-3A2FBA3CB52A}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sharpify.Tests", "tests\Sharpify.Tests\Sharpify.Tests.csproj", "{39884502-0357-4A6D-A03E-02D5BE7B0BFF}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sharpify.Data.Tests", "tests\Sharpify.Data.Tests\Sharpify.Data.Tests.csproj", "{D7749972-F24B-426D-B46C-D5949C4DCFD5}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sharpify.CommandLineInterface.Tests", "tests\Sharpify.CommandLineInterface.Tests\Sharpify.CommandLineInterface.Tests.csproj", "{C653A5E1-39AA-4C0C-A5A7-CB735C1DCDB4}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {37D56CBF-AD80-4361-8F43-568A93FB1D42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {37D56CBF-AD80-4361-8F43-568A93FB1D42}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {37D56CBF-AD80-4361-8F43-568A93FB1D42}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {37D56CBF-AD80-4361-8F43-568A93FB1D42}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {7E6960CB-A0F2-4AE4-B383-98763AB03E75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {7E6960CB-A0F2-4AE4-B383-98763AB03E75}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {7E6960CB-A0F2-4AE4-B383-98763AB03E75}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {7E6960CB-A0F2-4AE4-B383-98763AB03E75}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {13E844B7-D575-489D-B1E4-97F30B948227}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {13E844B7-D575-489D-B1E4-97F30B948227}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {13E844B7-D575-489D-B1E4-97F30B948227}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {13E844B7-D575-489D-B1E4-97F30B948227}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {85B4CC1F-D9F3-4EE7-BB8D-3A2FBA3CB52A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {85B4CC1F-D9F3-4EE7-BB8D-3A2FBA3CB52A}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {85B4CC1F-D9F3-4EE7-BB8D-3A2FBA3CB52A}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {85B4CC1F-D9F3-4EE7-BB8D-3A2FBA3CB52A}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {39884502-0357-4A6D-A03E-02D5BE7B0BFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {39884502-0357-4A6D-A03E-02D5BE7B0BFF}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {39884502-0357-4A6D-A03E-02D5BE7B0BFF}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {39884502-0357-4A6D-A03E-02D5BE7B0BFF}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {D7749972-F24B-426D-B46C-D5949C4DCFD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {D7749972-F24B-426D-B46C-D5949C4DCFD5}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {D7749972-F24B-426D-B46C-D5949C4DCFD5}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {D7749972-F24B-426D-B46C-D5949C4DCFD5}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {C653A5E1-39AA-4C0C-A5A7-CB735C1DCDB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {C653A5E1-39AA-4C0C-A5A7-CB735C1DCDB4}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {C653A5E1-39AA-4C0C-A5A7-CB735C1DCDB4}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {C653A5E1-39AA-4C0C-A5A7-CB735C1DCDB4}.Release|Any CPU.Build.0 = Release|Any CPU 52 | EndGlobalSection 53 | GlobalSection(SolutionProperties) = preSolution 54 | HideSolutionNode = FALSE 55 | EndGlobalSection 56 | GlobalSection(ExtensibilityGlobals) = postSolution 57 | SolutionGuid = {E9922EF9-146A-40B4-B612-F9C6BB5D9F64} 58 | EndGlobalSection 59 | EndGlobal 60 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# != 2 ]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | project_name=$1 9 | snk_path=$2 10 | 11 | currentDir=$(pwd) 12 | 13 | navigateAndBuild() { 14 | cd $1 # Navigate to the project directory 15 | dotnet clean -c Release 16 | dotnet build -c Release -p:SignAssembly="$snk_path" 17 | cd $currentDir # Navigate back to the root directory 18 | } 19 | 20 | if [ $project_name == "main" ]; then 21 | navigateAndBuild src/Sharpify/ 22 | elif [ $project_name == "data" ]; then 23 | navigateAndBuild src/Sharpify.Data/ 24 | elif [ $project_name == "cli" ]; then 25 | navigateAndBuild src/Sharpify.CommandLineInterface/ 26 | fi -------------------------------------------------------------------------------- /demos/Calc/Calc.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calc", "Calc\Calc.csproj", "{FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {FEBFD7FC-632D-4EC7-87E0-6802B6DB30FD}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /demos/Calc/Calc/Calc.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net8.0 5 | enable 6 | enable 7 | true 8 | true 9 | 10 | 11 | 12 | Calc 13 | Calc 14 | com.calc.demo.macos 15 | 1.0.0 16 | Major 17 | APPL 18 | ???? 19 | Calc 20 | NSApplication 21 | true 22 | 23 | 24 | 25 | 26 | 28 | Calc URL 29 | Calc;Calc:// 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /demos/Calc/Calc/Commands/AddCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Calc.Commands; 2 | 3 | public class AddCommand : SynchronousCommand { 4 | public override string Name => "Add"; 5 | 6 | public override string Description => "Add two numbers"; 7 | 8 | public override string Usage => "Add "; 9 | 10 | public override int Execute(Arguments args) { 11 | if (!args.TryGetValue(0, default(int), out var a)) { 12 | Console.WriteLine("Invalid number 1"); 13 | return 1; 14 | } 15 | 16 | if (!args.TryGetValue(1, 0, out var b)) { 17 | Console.WriteLine("Number 2 defaulted to 0"); 18 | // return 1; 19 | } 20 | 21 | Console.WriteLine($"{a} + {b} = {a + b}"); 22 | 23 | return 0; 24 | } 25 | } -------------------------------------------------------------------------------- /demos/Calc/Calc/Commands/DivideCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Calc.Commands; 2 | 3 | public class DivideCommand : SynchronousCommand { 4 | public override string Name => "Divide"; 5 | 6 | public override string Description => "Divide one number by another"; 7 | 8 | public override string Usage => "Divide "; 9 | 10 | public override int Execute(Arguments args) { 11 | if (!args.TryGetValue(0, 0, out var a)) { 12 | Console.WriteLine("Invalid number 1"); 13 | return 1; 14 | } 15 | 16 | if (!args.TryGetValue(1, 0, out var b) || b == 0) { 17 | Console.WriteLine("Invalid number 2"); 18 | return 1; 19 | } 20 | 21 | Console.WriteLine($"{a} / {b} = {a / b}"); 22 | 23 | return 0; 24 | } 25 | } -------------------------------------------------------------------------------- /demos/Calc/Calc/Commands/MultiplyCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Calc.Commands; 2 | 3 | public class MultiplyCommand : SynchronousCommand { 4 | public override string Name => "Multiply"; 5 | 6 | public override string Description => "Multiply two numbers"; 7 | 8 | public override string Usage => "Multiply -a -b "; 9 | 10 | public override int Execute(Arguments args) { 11 | if (!args.TryGetValue("a", 0, out var a)) { 12 | Console.WriteLine("Invalid number 1"); 13 | return 1; 14 | } 15 | 16 | if (!args.TryGetValue("b", 0, out var b)) { 17 | Console.WriteLine("Invalid number 2"); 18 | return 1; 19 | } 20 | 21 | Console.WriteLine($"{a} * {b} = {a * b}"); 22 | 23 | return 0; 24 | } 25 | } -------------------------------------------------------------------------------- /demos/Calc/Calc/Commands/SubtractCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Calc.Commands; 2 | 3 | public class SubtractCommand : SynchronousCommand { 4 | public override string Name => "Subtract"; 5 | 6 | public override string Description => "Subtract one number from another"; 7 | 8 | public override string Usage => 9 | """ 10 | Subtract [options] 11 | Options: 12 | --hex - Display the result in hexadecimal"; 13 | """; 14 | 15 | public override int Execute(Arguments args) { 16 | if (!args.TryGetValue(0, 0, out int a)) { 17 | Console.WriteLine("Invalid number 1"); 18 | return 1; 19 | } 20 | 21 | if (!args.TryGetValue(1, 0, out int b)) { 22 | Console.WriteLine("Invalid number 2"); 23 | return 1; 24 | } 25 | 26 | if (args.HasFlag("hex")) { 27 | Console.WriteLine($"{a} - {b} = {a - b:X}"); 28 | return 0; 29 | } else { 30 | Console.WriteLine($"{a} - {b} = {a - b}"); 31 | } 32 | 33 | return 0; 34 | } 35 | } -------------------------------------------------------------------------------- /demos/Calc/Calc/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Sharpify.CommandLineInterface; 2 | global using Sharpify; 3 | 4 | namespace Calc; -------------------------------------------------------------------------------- /demos/Calc/Calc/Program.cs: -------------------------------------------------------------------------------- 1 | using Calc.Commands; 2 | 3 | namespace Calc; 4 | 5 | public class Program { 6 | private static ReadOnlySpan Commands => new Command[] { 7 | new AddCommand(), 8 | new SubtractCommand(), 9 | new MultiplyCommand(), 10 | new DivideCommand() 11 | }; 12 | 13 | static async Task Main(string[] args) { 14 | var runner = CliRunner.CreateBuilder() 15 | .AddCommands(Commands) 16 | .SortCommandsAlphabetically() 17 | .UseConsoleAsOutputWriter() 18 | .WithMetadata(metadata => { 19 | metadata.Name = "Calc"; 20 | metadata.Description = "A simple calculator"; 21 | metadata.Version = "1.0.0"; 22 | metadata.Author = "Dave"; 23 | metadata.License = "MIT"; 24 | }) 25 | .Build(); 26 | 27 | return await runner.RunAsync(args); 28 | } 29 | } -------------------------------------------------------------------------------- /demos/Calc/README.md: -------------------------------------------------------------------------------- 1 | # Calc 2 | 3 | Calc is a demo to showcase the capabilities of `Sharpify.CommandLineInterface` by creating a NativeAot compatible calculator cli. 4 | 5 | ## Guide 6 | 7 | This demo is accompanied by a YouTube video showcasing the package and this project, watch it here: 8 | 9 | [![Your new favorite CLI framework for C# - Sharpify.CommandLineInterface showcase](https://github.com/dusrdev/Sharpify/blob/main/demos/Calc/yt.video.thumbnail.png)](https://www.youtube.com/watch?v=bcuPY96Zr4k) 10 | 11 | ## Map of file -> feature 12 | 13 | * `Program.cs` shows the main entry point and `CliRunner` 14 | * `AddCommand.cs` shows positional and optional arguments 15 | * `SubtractCommand.cs` shows positional arguments and flags 16 | * `DivideCommand.cs` shows additional validation logic 17 | * `MultiplyCommand.cs` shows how to use named arguments 18 | -------------------------------------------------------------------------------- /demos/Calc/yt.video.thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dusrdev/Sharpify/c30356c8585586c3c2aa2f4d84a7be4c84bb75e6/demos/Calc/yt.video.thumbnail.png -------------------------------------------------------------------------------- /docs/10.-Sharpify.Data.md: -------------------------------------------------------------------------------- 1 | # Sharpify.Data 2 | 3 | For basic information check [this](https://github.com/dusrdev/Sharpify/blob/main/Sharpify.Data/README.md) 4 | 5 | ## Usage Examples 6 | 7 | Lets see an high performance example, obviously, if you are using this package you care about performance, and here we appreciate that, a lot. 8 | 9 | ### Initialization 10 | 11 | ```csharp 12 | using var database = Database.CreateOrLoad(new DatabaseConfiguration { 13 | Path = path, // local 14 | EncryptionKey = "mykey", 15 | SerializeOnUpdate = true, 16 | IgnoreCase = true 17 | }); 18 | ``` 19 | 20 | ### CRUD 21 | 22 | lets first create a value type that implements `IMemoryPackable` such as: 23 | 24 | ```csharp 25 | [MemoryPackable] 26 | public readonly partial record struct Dog(string Species, int Age, float Weight); 27 | ``` 28 | 29 | Notice the `MemoryPackable` attribute from [MemoryPack](https://github.com/Cysharp/MemoryPack), this will implement the `IMemoryPackable` interface behind the scenes using a source generator. `Database` utilizes this to enable unrivaled performance. 30 | 31 | ```csharp 32 | database.Upsert("Brian", new("Bipedal Talking Dog", 20)); 33 | Dog? brian = database.Get("Brian"); // Get returns null if the key doesn't exist 34 | // because SerializeOnUpdate options was chosen, it will handle this automatically 35 | // otherwise use .Serialize() or .SerializeAsync() 36 | // We also use the filter 37 | var table = Database.CreateMemoryPackFilter(); 38 | table.Upsert("Buster", new Dog("Buster", 5)); 39 | bool exists = table.TryGetValue("Buster", out Dog buster); 40 | ``` 41 | 42 | #### FlexibleDatabaseFilter{T} 43 | 44 | As an alternative to `MemoryPackable` the database also has an option to create a `FlexibleDatabaseFilter{T}`. This filter can be used when you want to implement easy access to the existing apis with a type that can't implement `IMemoryPackable{T}`, one example would be collections. 45 | 46 | For that the type itself, would need to implement the interface `IFilterable{T}`. If the type has the `MemoryPackable` attribute, it means the serializer knows how to handle it, it would be required to add practically a single line of code to each of the methods from `IFilterable{T}` to implement it. If some methods aren't required, you could simply `return null;`. 47 | 48 | When your type lets say `TCustom` implements `IFilterable`, the following method will become available: 49 | 50 | ```csharp 51 | var table = Database.CreateFlexibleFilter(); 52 | ``` 53 | 54 | this filter implements the same interface as the `MemoryPackFilter` and will support the same apis. To allow a very uniform and cohesive codebase. 55 | -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/ArgumentsCore.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace Sharpify.CommandLineInterface; 4 | 5 | /// 6 | /// A wrapper class over a dictionary of string : string with additional features 7 | /// 8 | /// 9 | /// Arguments instances are created via 10 | /// 11 | public sealed partial class Arguments { 12 | private readonly string[] _args; 13 | private readonly Dictionary _arguments; 14 | 15 | /// 16 | /// Internal constructor for the class 17 | /// 18 | /// Copy or reference of the arguments before processing 19 | /// Ensure not null or empty 20 | internal Arguments(string[] args, Dictionary arguments) { 21 | _args = args; 22 | _arguments = arguments; 23 | } 24 | 25 | /// 26 | /// Gets the number of arguments. 27 | /// 28 | public int Count => _arguments.Count; 29 | 30 | /// 31 | /// Checks if the arguments are empty. 32 | /// 33 | public bool AreEmpty => _arguments.Count is 0; 34 | 35 | /// 36 | /// Returns an empty arguments object. 37 | /// 38 | public static readonly Arguments Empty = new([], []); 39 | 40 | /// 41 | /// Returns a of the arguments as they were before processing, but after splitting (if it was required) 42 | /// 43 | /// 44 | /// 45 | /// If you passed a collection of strings to be used for it will contain a copy of that array, if a was passed, it will contain a copy of the result of 46 | /// 47 | /// 48 | /// In normal use case you shouldn't need this, but in case you want to manufacture some sort of a nested command structure, you can use this to filter once more for after selectively parsing some of the arguments, in which case it is very powerful. 49 | /// 50 | /// 51 | public ReadOnlyMemory ArgsAsMemory() => _args; 52 | 53 | /// 54 | /// Returns a of the arguments as they were before processing, but after splitting (if it was required) 55 | /// 56 | /// 57 | /// 58 | /// If you passed a collection of strings to be used for it will contain a copy of that array, if a was passed, it will contain a copy of the result of 59 | /// 60 | /// 61 | /// In normal use case you shouldn't need this, but in case you want to manufacture some sort of a nested command structure, you can use this to filter once more for after selectively parsing some of the arguments, in which case it is very powerful. 62 | /// 63 | /// 64 | public ReadOnlySpan ArgsAsSpan() => _args; 65 | 66 | /// 67 | /// Returns new Arguments with positional arguments forwarded by 1, so that argument that was 1 is now 0, 2 is now 1 and so on. This is non-destructive, the original arguments are not modified. 68 | /// 69 | /// 70 | /// 71 | /// This is useful if you have a command that has a sub-command and you want to pass the arguments to the sub-command 72 | /// 73 | /// 74 | /// The first positional argument (0) will be skipped to actually forward 75 | /// 76 | /// 77 | public Arguments ForwardPositionalArguments() { 78 | if (!Contains("0")) { 79 | return this; 80 | } 81 | var dict = new Dictionary(_arguments.Comparer); 82 | 83 | foreach ((string prevKey, string prevValue) in _arguments) { 84 | // Handle non numeric 85 | if (!int.TryParse(prevKey.AsSpan(), out int numericIndex)) { 86 | dict.Add(prevKey, prevValue); 87 | } 88 | // Handle numeric 89 | if (numericIndex is 0) { // forwarding means the previous 0 is lost 90 | continue; 91 | } 92 | dict.Add((numericIndex - 1).ToString(), prevValue); // Add with the index reduced by 1. 93 | } 94 | 95 | // Because this is a new dictionary, if pos 1, isn't found, 0 still won't be present 96 | // So essentially 0 was forwarded to no longer exist 97 | return new Arguments(_args, dict); 98 | } 99 | 100 | /// 101 | /// Returns the underlying dictionary 102 | /// 103 | public ReadOnlyDictionary GetInnerDictionary() => _arguments.AsReadOnly(); 104 | } 105 | -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/CHANGELOGLATEST.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## Version 1.5.0 4 | 5 | * Updated to support NET9 with `Sharpify` 2.5.0 6 | * Optimized path of `Arguments` forwarding when no positional arguments are present. 7 | -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/CliBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface; 2 | 3 | /// 4 | /// Represents a builder for a CliRunner. 5 | /// 6 | public sealed class CliBuilder { 7 | private readonly CliRunnerConfiguration _config; 8 | 9 | internal CliBuilder() { 10 | _config = new CliRunnerConfiguration(); 11 | } 12 | 13 | /// 14 | /// Adds a command to the CLI runner. 15 | /// 16 | /// 17 | /// The same instance of 18 | public CliBuilder AddCommand(Command command) { 19 | _config.Commands.Add(command); 20 | return this; 21 | } 22 | 23 | /// 24 | /// Adds commands to the CLI runner. 25 | /// 26 | /// 27 | /// The same instance of 28 | public CliBuilder AddCommands(ReadOnlySpan commands) { 29 | _config.Commands.AddRange(commands); 30 | return this; 31 | } 32 | 33 | /// 34 | /// Sets the output writer for the CLI runner. 35 | /// 36 | /// 37 | /// The same instance of 38 | public CliBuilder SetOutputWriter(TextWriter writer) { 39 | CliRunner.SetOutputWriter(writer); 40 | return this; 41 | } 42 | 43 | /// 44 | /// Sets the output writer for the CLI runner to be . 45 | /// 46 | /// The same instance of 47 | public CliBuilder UseConsoleAsOutputWriter() { 48 | CliRunner.SetOutputWriter(Console.Out); 49 | return this; 50 | } 51 | 52 | /// 53 | /// Sorts the commands alphabetically. 54 | /// 55 | /// 56 | /// This change only affects the functionality of the help text. 57 | /// 58 | /// The same instance of 59 | public CliBuilder SortCommandsAlphabetically() { 60 | _config.SortCommandsAlphabetically = true; 61 | return this; 62 | } 63 | 64 | /// 65 | /// Add metadata - can be used to generate the general help text (Is the default source) 66 | /// 67 | /// 68 | /// Configure the help text source with 69 | /// 70 | /// The same instance of 71 | public CliBuilder WithMetadata(Action options) { 72 | options(_config.MetaData); 73 | return this; 74 | } 75 | 76 | /// 77 | /// Add a custom header - can be used instead of Metadata in the header of the help text 78 | /// 79 | /// 80 | /// Configure the help text source with 81 | /// 82 | /// The same instance of 83 | public CliBuilder WithCustomHeader(string header) { 84 | _config.CustomHeader = header; 85 | return this; 86 | } 87 | 88 | /// 89 | /// Sets the source of the general help text. 90 | /// 91 | /// Requested source of the help text. 92 | /// The same instance of 93 | public CliBuilder SetHelpTextSource(HelpTextSource source) { 94 | _config.HelpTextSource = source; 95 | return this; 96 | } 97 | 98 | /// 99 | /// Configures how the parser handles argument casing. 100 | /// 101 | /// 102 | /// By default it is set to to improve user experience 103 | /// 104 | /// The same instance of 105 | public CliBuilder ConfigureArgumentCaseHandling(ArgumentCaseHandling caseHandling) { 106 | _config.ArgumentCaseHandling = caseHandling; 107 | return this; 108 | } 109 | 110 | /// 111 | /// Show error codes next to the error messages. 112 | /// 113 | /// The same instance of 114 | public CliBuilder ShowErrorCodes() { 115 | _config.ShowErrorCodes = true; 116 | return this; 117 | } 118 | 119 | /// 120 | /// Configures how the CLI runner handles empty input. 121 | /// 122 | /// The same instance of 123 | public CliBuilder ConfigureEmptyInputBehavior(EmptyInputBehavior behavior) { 124 | _config.EmptyInputBehavior = behavior; 125 | return this; 126 | } 127 | 128 | /// 129 | /// Builds the CLI runner. 130 | /// 131 | public CliRunner Build() { 132 | if (_config.Commands.Count is 0) { 133 | throw new InvalidOperationException("No commands were added."); 134 | } 135 | return new CliRunner(_config); 136 | } 137 | } -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/CliMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface; 2 | 3 | /// 4 | /// Contains metadata for a CLI application. 5 | /// 6 | public record CliMetadata { 7 | /// 8 | /// The name of the CLI application. 9 | /// 10 | public string Name { get; set; } = string.Empty; 11 | 12 | /// 13 | /// The description of the CLI application. 14 | /// 15 | public string Description { get; set; } = string.Empty; 16 | 17 | /// 18 | /// The version of the CLI application. 19 | /// 20 | public string Version { get; set; } = string.Empty; 21 | 22 | /// 23 | /// The author of the CLI application. 24 | /// 25 | public string Author { get; set; } = string.Empty; 26 | 27 | /// 28 | /// The license of the CLI application. 29 | /// 30 | public string License { get; set; } = string.Empty; 31 | 32 | /// 33 | /// The default metadata for a CLI application. 34 | /// 35 | public static readonly CliMetadata Default = new() { 36 | Name = "Interface", 37 | Description = "Default description.", 38 | Version = "1.0.0", 39 | Author = "John Doe", 40 | License = "MIT", 41 | }; 42 | 43 | /// 44 | /// Returns the total length of the metadata. 45 | /// 46 | public int TotalLength => Name.Length + Description.Length + Version.Length + Author.Length + License.Length; 47 | } -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/CliRunnerConfiguration.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface; 2 | 3 | /// 4 | /// Represents the internal configuration of a CLI runner. 5 | /// 6 | internal sealed class CliRunnerConfiguration { 7 | /// 8 | /// The commands that the CLI runner can execute. 9 | /// 10 | public List Commands { get; set; } = []; 11 | 12 | /// 13 | /// The metadata of the CLI runner. 14 | /// 15 | public CliMetadata MetaData { get; set; } = CliMetadata.Default; 16 | 17 | /// 18 | /// The header to use in the help text. 19 | /// 20 | public string CustomHeader { get; set; } = string.Empty; 21 | 22 | /// 23 | /// The source of the help text. 24 | /// 25 | public HelpTextSource HelpTextSource { get; set; } = HelpTextSource.Metadata; 26 | 27 | /// 28 | /// Whether to sort commands alphabetically. 29 | /// 30 | /// 31 | /// It is set to false by default 32 | /// 33 | public bool SortCommandsAlphabetically { get; set; } 34 | 35 | /// 36 | /// Whether to show error codes in the help text. 37 | /// 38 | /// 39 | /// It is set to false by default to improve user experience 40 | /// 41 | public bool ShowErrorCodes { get; set; } 42 | 43 | /// 44 | /// Configures the case sensitivity of arguments parsing 45 | /// 46 | public ArgumentCaseHandling ArgumentCaseHandling { get; set; } = ArgumentCaseHandling.IgnoreCase; 47 | 48 | /// 49 | /// Configures the behavior of the CLI runner when empty input is provided. 50 | /// 51 | public EmptyInputBehavior EmptyInputBehavior { get; set; } = EmptyInputBehavior.DisplayHelpText; 52 | } -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/Command.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | using Sharpify.Collections; 4 | 5 | namespace Sharpify.CommandLineInterface; 6 | 7 | /// 8 | /// Represents a command for a CLI application. 9 | /// 10 | public abstract class Command { 11 | /// 12 | /// Gets the name of the command. 13 | /// 14 | public abstract string Name { get; } 15 | /// 16 | /// Gets the description of the command. 17 | /// 18 | public abstract string Description { get; } 19 | /// 20 | /// Gets the usage of the command. 21 | /// 22 | public abstract string Usage { get; } 23 | 24 | /// 25 | /// Executes the command. 26 | /// 27 | public abstract ValueTask ExecuteAsync(Arguments args); 28 | 29 | /// 30 | /// Gets the help for the command. 31 | /// 32 | public virtual string GetHelp() { 33 | var length = (Name.Length + Description.Length + Usage.Length) * 2; 34 | using var owner = MemoryPool.Shared.Rent(length); 35 | var buffer = StringBuffer.Create(owner.Memory.Span); 36 | buffer.AppendLine(); 37 | buffer.Append("Command: "); 38 | buffer.AppendLine(Name); 39 | buffer.AppendLine(); 40 | buffer.Append("Description: "); 41 | buffer.AppendLine(Description); 42 | buffer.AppendLine(); 43 | buffer.Append("Usage: "); 44 | buffer.AppendLine(Usage); 45 | return buffer.Allocate(); 46 | } 47 | 48 | /// 49 | /// Compares two commands by their name. 50 | /// 51 | /// The first command to compare. 52 | /// The second command to compare. 53 | /// 54 | /// A value indicating the relative order of the commands. 55 | /// The return value is less than 0 if x.Name is less than y.Name, 56 | /// 0 if x.Name is equal to y.Name, and greater than 0 if x.Name is greater than y.Name. 57 | /// 58 | public static int ByNameComparer(Command x, Command y) { 59 | return string.Compare(x.Name, y.Name, StringComparison.Ordinal); 60 | } 61 | } -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/ConfigurationEnums.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface; 2 | 3 | /// 4 | /// Controls how the CLI runner handles empty input. 5 | /// 6 | public enum EmptyInputBehavior { 7 | /// 8 | /// Displays the help text and exits. 9 | /// 10 | DisplayHelpText, 11 | /// 12 | /// Attempts to proceed with handling the commands. 13 | /// 14 | /// 15 | /// If a single command is used and command name is set to not required, this will execute the command with empty args, 16 | /// otherwise it will display the appropriate error message. 17 | /// 18 | AttemptToProceed, 19 | } 20 | 21 | /// 22 | /// Dictates the source of the general help text 23 | /// 24 | public enum HelpTextSource { 25 | /// 26 | /// Use the metadata to generate HelpText 27 | /// 28 | Metadata, 29 | /// 30 | /// Use the custom header to generate HelpText 31 | /// 32 | CustomHeader 33 | } 34 | 35 | /// 36 | /// Configures how to handle argument casing 37 | /// 38 | public enum ArgumentCaseHandling { 39 | /// 40 | /// Ignore argument case 41 | /// 42 | IgnoreCase, 43 | /// 44 | /// Sets the arguments parser to be case sensitive 45 | /// 46 | CaseSensitive 47 | } -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface; 2 | 3 | internal static class Helper { 4 | /// 5 | /// Tries to retrieve the value of the first specified key that exists in the dictionary. 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | internal static bool TryGetValue(this Dictionary dict, ReadOnlySpan keys, out string value) { 12 | foreach (var key in keys) { 13 | if (dict.TryGetValue(key, out var res)) { 14 | value = res!; 15 | return true; 16 | } 17 | } 18 | value = ""; 19 | return false; 20 | } 21 | 22 | /// 23 | /// Checks if the first argument is the specified value or if it is a flag. 24 | /// 25 | /// 26 | /// 27 | /// 28 | internal static bool IsFirstOrFlag(this Arguments args, string value) { 29 | if (args.TryGetValue(0, out string? first) && first == value) { 30 | return true; 31 | } 32 | return args.Count is 1 && args.HasFlag(value); 33 | } 34 | 35 | internal static StringComparer GetComparer(this CliRunnerConfiguration config) 36 | => config.ArgumentCaseHandling switch { 37 | ArgumentCaseHandling.IgnoreCase => StringComparer.OrdinalIgnoreCase, 38 | ArgumentCaseHandling.CaseSensitive => StringComparer.Ordinal, 39 | _ => throw new ArgumentOutOfRangeException(nameof(config.ArgumentCaseHandling), config.ArgumentCaseHandling, null) 40 | }; 41 | } -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/OutputHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface; 2 | 3 | /// 4 | /// Provides helper methods for outputting using 5 | /// 6 | public static class OutputHelper { 7 | /// 8 | /// Writes a line to the output writer. 9 | /// 10 | public static void WriteLine(string message) => CliRunner.OutputWriter.WriteLine(message); 11 | 12 | /// 13 | /// Writes a message to the output writer. 14 | /// 15 | public static void Write(string message) => CliRunner.OutputWriter.Write(message); 16 | 17 | /// 18 | /// Writes a line to the output writer and returns the specified code. 19 | /// 20 | /// The message to write. 21 | /// The code to return. 22 | /// Whether to append the code to the message. 23 | /// A containing the specified code. 24 | /// Using will append [Code: ] to 25 | public static ValueTask Return(string message, int code, bool appendCode = false) { 26 | var writer = CliRunner.OutputWriter; 27 | writer.Write(message); 28 | if (appendCode) { 29 | writer.Write($" [Code: {code}]"); 30 | } 31 | writer.WriteLine(); 32 | return ValueTask.FromResult(code); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/Sharpify.CommandLineInterface.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0;net8.0 5 | latest 6 | enable 7 | 1.5.0 8 | enable 9 | true 10 | David Shnayder 11 | David Shnayder 12 | MIT 13 | CHANGELOGLATEST.md 14 | True 15 | Sharpify.CommandLineInterface 16 | An extension of Sharpify, focused on creating command line interfaces 17 | https://github.com/dusrdev/Sharpify 18 | https://github.com/dusrdev/Sharpify 19 | git 20 | Extensions;HighPerformance;Cli;Parser;Interface;CommandLine 21 | true 22 | true 23 | false 24 | true 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | <_Parameter1>Sharpify.CommandLineInterface.Tests 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Sharpify.CommandLineInterface/SynchronousCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface; 2 | 3 | /// 4 | /// An alternative to that runs synchronously. 5 | /// 6 | /// 7 | /// This is syntactic sugar for wrapping returns from ExecuteAsync in ValueTask.FromResult 8 | /// 9 | public abstract class SynchronousCommand : Command { 10 | /// 11 | public override ValueTask ExecuteAsync(Arguments args) { 12 | return ValueTask.FromResult(Execute(args)); 13 | } 14 | 15 | /// 16 | /// Executes the command. 17 | /// 18 | /// 19 | /// Status code 20 | public abstract int Execute(Arguments args); 21 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/Build.txt: -------------------------------------------------------------------------------- 1 | nuget: 2 | dotnet clean -c Release 3 | dotnet build -c Release 4 | dotnet pack -c Release -p:SignAssembly="" 5 | 6 | dll: 7 | dotnet build -c Release -p:SignAssembly="" 8 | 9 | docs: 10 | git subtree push --prefix docs https://github.com/dusrdev/Sharpify.wiki.git master 11 | -------------------------------------------------------------------------------- /src/Sharpify.Data/CHANGELOGLATEST.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v2.6.0 4 | 5 | * Updated to support NET9 6 | * Updated to use `Sharpify 2.5.0` and `MemoryPack 1.21.3` 7 | * All `byte[]` value returning reads from the database, now return `ReadOnlyMemory` instead, previously, to maintain the integrity of the value, a copy was made and returned, because there wasn't any guarantee against modification, `ReadOnlyMemory` enforced this guarantee without creating a copy, if you just reading the data this is much more performant, and if you want to modify it, you can always create a copy at your own discretion. 8 | * Decreased memory allocations for the `Func` based `Remove` method. 9 | * Removed compiler directions that potentially could not allow the JIT compiler to perform Dynamic PGO. 10 | * `Upsert{T}` overloads now have a `Func updateCondition` parameter that can be used to ensure that a condition is met before being updated, this is a feature of NoSQL databases that protects against concurrent writes overwriting each other. Now you can use this feature in `Sharpify.Data` as well. 11 | * Of course this feature is also available in `UpsertMany{T}` overloads, and also in the overloads of the `JsonTypeInfo T`. 12 | * To make it easier to see the result, these `Upsert` methods now return `bool`. 13 | * `False` will only be returned IF ALL of the following conditions are met: 14 | 1. Previous value was stored under this key 15 | 2. The previous value was successfully deserialized with the right type 16 | 3. The `updateCondition` was not met 17 | * `Database` now tracks changes (additions, updates, removals) and compares them serialization events, to avoid serialization if no updates occurred since the previous serialization. 18 | * This means that you can automate serialization without worrying about potential waste of resources, for example you could omit `SerializeOnUpdate` from the `DatabaseConfiguration`, then create a background task that serializes on a given interval for example with `Sharpify.Routines.Routine` or `Sharpify.Routines.AsyncRoutine`, and it will only actually serialize if updates occurred. This can significantly improve performance in cases where there are write peaks, but the database is mostly read from. 19 | * You can now set the `Path` in the `DatabaseConfiguration` to an empty string `""` to receive an in-memory version of the database. 20 | It still has serialization methods, but they don't perform any operations, they are essentially duds. 21 | * `TryReadToRentedBuffer where T : IMemoryPackable` will now be able to retrieve the precise amount of needed space, so the size of the rented buffer will more accurately reflect the size of the data, this should help with dramatically improve performance when dealing with large objects. Before the buffer would've rented a capacity according to the length of the serialized object, meaning that the buffer was x times larger than needed when x is size(object) / size(byte). So the larger was each object, the `RentedBufferWriter` size would grow exponentially, now it grows linearly, maximizing efficiency. 22 | * And minor optimizations (same as every other release 😜) 23 | 24 | ### Reminder: Workaround for broken NativeAot support from MemoryPack 25 | 26 | As of writing this, MemoryPack's NativeAot support is broken, for any type that isn't already in their cached types, the `MemoryPackFormatterProvider` uses reflection to get the formatter, which fails in NativeAot. 27 | As a workaround, we need to add the formatters ourselves, to do this, take any 1 static entry point, that activates before the database is loaded, and add this: 28 | 29 | ```csharp 30 | // for every T type that relies on MemoryPack for serialization, and their inheritance hierarchy 31 | // This includes types that implement IMemoryPackable (i.e types that are decorated with MemoryPackable) 32 | MemoryPackFormatterProvider.Register(); 33 | // If the type is a collection or dictionary use the other corresponding overloads: 34 | MemoryPackFormatterProvider.RegisterCollection(); 35 | // or 36 | MemoryPackFormatterProvider.RegisterDictionary(); 37 | // and so on... 38 | // for all overloads check peek the definition of MemoryPackFormatterProvider, or their Github Repo 39 | ``` 40 | 41 | **Note:** Make sure you don't create a new static constructor in those types, `MemoryPack` already creates those, you will need to find a different entry point. 42 | 43 | With this the serializer should be able to bypass the part using reflection, and thus work even on NativeAot. 44 | 45 | P.S. The base type of the Database is already registered the same way on its own static constructor. 46 | -------------------------------------------------------------------------------- /src/Sharpify.Data/DataChangeType.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Data; 2 | 3 | /// 4 | /// The type of changed that occurred on a key 5 | /// 6 | public enum DataChangeType : byte { 7 | /// 8 | /// A key was inserted or updated 9 | /// 10 | Upsert = 1 << 0, 11 | /// 12 | /// A key was removed 13 | /// 14 | Remove = 1 << 1 15 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/DataChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Data; 2 | 3 | /// 4 | /// Event arguments for data changed (addition, update or removal of keys) 5 | /// 6 | public sealed class DataChangedEventArgs : EventArgs { 7 | /// 8 | /// The key that was changed 9 | /// 10 | public required string Key { get; init; } 11 | 12 | /// 13 | /// The value that was changed 14 | /// 15 | public required object? Value { get; init; } 16 | 17 | /// 18 | /// The type of change that occurred 19 | /// 20 | public required DataChangeType ChangeType { get; init; } 21 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/DatabaseConfiguration.cs: -------------------------------------------------------------------------------- 1 | using MemoryPack; 2 | 3 | namespace Sharpify.Data; 4 | 5 | /// 6 | /// Configuration for 7 | /// 8 | public record DatabaseConfiguration { 9 | /// 10 | /// The path to which the database file will be saved. 11 | /// 12 | /// 13 | /// Setting path to an empty string "" will create an in-memory database. 14 | /// 15 | public required string Path { get; init; } 16 | 17 | /// 18 | /// Whether the database keys case should be ignored. 19 | /// 20 | /// 21 | /// This impacts performance on reads and deserialization. 22 | /// 23 | public bool IgnoreCase { get; init; } = false; 24 | 25 | /// 26 | /// The encoding to use when serializing and deserializing strings in the database. 27 | /// 28 | public StringEncoding Encoding { get; init; } = StringEncoding.Utf8; 29 | 30 | /// 31 | /// Whether to serialize the database automatically when it is updated. 32 | /// 33 | /// 34 | /// This relates to adding, removing, and updating values. 35 | /// 36 | public bool SerializeOnUpdate { get; init; } = false; 37 | 38 | /// 39 | /// Whether to trigger update events when the database is updated. 40 | /// 41 | /// 42 | /// This relates to adding, removing, and updating values. 43 | /// 44 | public bool TriggerUpdateEvents { get; init; } = false; 45 | 46 | /// 47 | /// General encryption key, the entire file will be encrypted with this. 48 | /// 49 | public string EncryptionKey { get; init; } = string.Empty; 50 | 51 | /// 52 | /// Whether general encryption is enabled. 53 | /// 54 | public bool HasEncryption => EncryptionKey.Length > 0; 55 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/DatabaseRemovals.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Data; 2 | 3 | public sealed partial class Database { 4 | /// 5 | /// Removes the and its value from the inner dictionary. 6 | /// 7 | /// 8 | /// True if the key was removed, false if it didn't exist or couldn't be removed. 9 | public bool Remove(string key) { 10 | try { 11 | _lock.EnterWriteLock(); 12 | if (!_data.Remove(key, out var val)) { 13 | return false; 14 | } 15 | var estimatedSize = Helper.GetEstimatedSize(key, val); 16 | Interlocked.Add(ref _estimatedSize, -estimatedSize); 17 | Interlocked.Increment(ref _updatesCount); 18 | if (Config.SerializeOnUpdate) { 19 | Serialize(); 20 | } 21 | if (Config.TriggerUpdateEvents) { 22 | InvokeDataEvent(new DataChangedEventArgs { 23 | Key = key, 24 | Value = val, 25 | ChangeType = DataChangeType.Remove 26 | }); 27 | } 28 | return true; 29 | } finally { 30 | _lock.ExitWriteLock(); 31 | } 32 | } 33 | 34 | /// 35 | /// Removes all keys that match the . 36 | /// 37 | /// A predicate for the key 38 | /// 39 | /// 40 | /// This method is thread-safe and will lock the database while removing the keys. 41 | /// 42 | /// 43 | /// If TriggerUpdateEvents is enabled, this method will trigger a event for each key removed. 44 | /// 45 | /// 46 | public void Remove(Func keySelector) => Remove(keySelector, null); 47 | 48 | /// 49 | /// Removes all keys that match the . 50 | /// 51 | /// A predicate for the key 52 | /// A prefix to be removed from the keys prior to the keySelector (mainly used for IDatabaseFilter implementations), leaving it as null will skip pre-filtering 53 | /// 54 | /// 55 | /// This method is thread-safe and will lock the database while removing the keys. 56 | /// 57 | /// 58 | /// If TriggerUpdateEvents is enabled, this method will trigger a event for each key removed. 59 | /// 60 | /// 61 | public void Remove(Func keySelector, string? keyPrefix) { 62 | try { 63 | _lock.EnterWriteLock(); 64 | 65 | var predicate = keyPrefix is null 66 | ? keySelector 67 | : key => key.StartsWith(keyPrefix) && keySelector(key.Substring(keyPrefix.Length)); 68 | 69 | var matches = _data.Keys.Where(predicate); 70 | 71 | foreach (var key in matches) { 72 | _data.Remove(key, out var val); 73 | var estimatedSize = Helper.GetEstimatedSize(key, val); 74 | Interlocked.Add(ref _estimatedSize, -estimatedSize); 75 | Interlocked.Increment(ref _updatesCount); 76 | 77 | if (Config.TriggerUpdateEvents) { 78 | InvokeDataEvent(new DataChangedEventArgs { 79 | Key = key, 80 | Value = val, 81 | ChangeType = DataChangeType.Remove 82 | }); 83 | } 84 | } 85 | 86 | if (Config.SerializeOnUpdate) { 87 | Serialize(); 88 | } 89 | 90 | } finally { 91 | _lock.ExitWriteLock(); 92 | } 93 | } 94 | 95 | /// 96 | /// Clears all keys and values from the database. 97 | /// 98 | public void Clear() { 99 | try { 100 | _lock.EnterWriteLock(); 101 | _data.Clear(); 102 | Interlocked.Exchange(ref _estimatedSize, 0); 103 | Interlocked.Increment(ref _updatesCount); 104 | if (Config.SerializeOnUpdate) { 105 | Serialize(); 106 | } 107 | if (Config.TriggerUpdateEvents) { 108 | InvokeDataEvent(new DataChangedEventArgs { 109 | Key = "ALL", 110 | Value = null, 111 | ChangeType = DataChangeType.Remove 112 | }); 113 | } 114 | } finally { 115 | _lock.ExitWriteLock(); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/DatabaseSerialization.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Sharpify.Data; 4 | 5 | public sealed partial class Database { 6 | private void EnsureUpsertsAreFinished() { 7 | if (!Config.SerializeOnUpdate) { 8 | while (_queue.TryDequeue(out var kvp)) { 9 | _data[kvp.Key] = kvp.Value; 10 | int estimatedSize = Helper.GetEstimatedSize(kvp); 11 | Interlocked.Add(ref _estimatedSize, estimatedSize); 12 | Interlocked.Increment(ref _updatesCount); 13 | } 14 | } 15 | } 16 | 17 | /// 18 | /// Checks if the database needs to be serialized. 19 | /// 20 | /// 21 | private bool IsSerializationNecessary() { 22 | if (_isInMemory) { 23 | return false; 24 | } 25 | lock (_sLock) { 26 | if (_updatesCount == _serializationReference) { 27 | return false; 28 | } 29 | _serializationReference = _updatesCount; 30 | return true; 31 | } 32 | } 33 | 34 | /// 35 | /// Saves the database to the hard disk. 36 | /// 37 | public void Serialize() { 38 | EnsureUpsertsAreFinished(); 39 | 40 | if (!IsSerializationNecessary()) { 41 | return; 42 | } 43 | 44 | Debug.Assert(!_isInMemory); 45 | 46 | int estimatedSize = GetOverestimatedSize(); 47 | _serializer.Serialize(_data, estimatedSize); 48 | } 49 | 50 | /// 51 | /// Saves the database to the hard disk asynchronously. 52 | /// 53 | public ValueTask SerializeAsync(CancellationToken cancellationToken = default) { 54 | EnsureUpsertsAreFinished(); 55 | 56 | if (!IsSerializationNecessary()) { 57 | return ValueTask.CompletedTask; 58 | } 59 | 60 | Debug.Assert(!_isInMemory); 61 | 62 | return _serializer.SerializeAsync(_data, cancellationToken); 63 | } 64 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/FlexibleDatabaseFilter{T}.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | using Sharpify.Collections; 4 | 5 | namespace Sharpify.Data; 6 | 7 | /// 8 | /// Provides a light database filter by type. 9 | /// 10 | /// 11 | /// Items that are upserted into the database using the filter, should not be retrieved without the filter as the key is modified. 12 | /// 13 | /// 14 | public class FlexibleDatabaseFilter : IDatabaseFilter where T : IFilterable { 15 | /// 16 | /// The key filter, statically created for the type. 17 | /// 18 | public static readonly string KeyFilter = $"{typeof(T).Name}:"; 19 | 20 | /// 21 | /// Creates a combined key (filter) for the specified key. 22 | /// 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | protected string AcquireKey(ReadOnlySpan key) { 25 | return string.Intern(KeyFilter.Concat(key)); 26 | } 27 | 28 | /// 29 | /// The database. 30 | /// 31 | protected readonly Database _database; 32 | 33 | /// 34 | /// Creates a new database filter. 35 | /// 36 | /// 37 | public FlexibleDatabaseFilter(Database database) { 38 | _database = database; 39 | } 40 | 41 | /// 42 | public bool ContainsKey(string key) { 43 | return _database.ContainsKey(AcquireKey(key)); 44 | } 45 | 46 | 47 | /// 48 | public bool TryGetValue(string key, string encryptionKey, out T value) { 49 | if (!_database.TryGetValue(AcquireKey(key), encryptionKey, out var data)) { 50 | value = default!; 51 | return false; 52 | } 53 | value = T.Deserialize(data.Span)!; 54 | return true; 55 | } 56 | 57 | /// 58 | public bool TryGetValues(string key, string encryptionKey, out T[] values) { 59 | if (!_database.TryGetValue(AcquireKey(key), encryptionKey, out ReadOnlyMemory data)) { 60 | values = default!; 61 | return false; 62 | } 63 | values = T.DeserializeMany(data.Span)!; 64 | return true; 65 | } 66 | 67 | /// 68 | public RentedBufferWriter TryReadToRentedBuffer(string key, string encryptionKey = "", int reservedCapacity = 0) { 69 | if (!_database.TryGetValue(AcquireKey(key), encryptionKey, out ReadOnlyMemory data)) { 70 | return new RentedBufferWriter(0); 71 | } 72 | T[] values = T.DeserializeMany(data.Span)!; 73 | var buffer = new RentedBufferWriter(values.Length + reservedCapacity); 74 | buffer.WriteAndAdvance(values); 75 | return buffer; 76 | } 77 | 78 | /// 79 | public bool Upsert(string key, T value, string encryptionKey = "", Func? updateCondition = null) { 80 | if (updateCondition is not null) { 81 | if (TryGetValue(key, encryptionKey, out var existingValue) && !updateCondition(existingValue)) { 82 | return false; 83 | } 84 | } 85 | var bytes = T.Serialize(value)!; 86 | _database.Upsert(AcquireKey(key), bytes, encryptionKey); 87 | return true; 88 | } 89 | 90 | /// 91 | public bool UpsertMany(string key, T[] values, string encryptionKey = "", Func? updateCondition = null) { 92 | ArgumentNullException.ThrowIfNull(values, nameof(values)); 93 | if (updateCondition is not null) { 94 | if (TryGetValues(key, encryptionKey, out var existingValues) && !updateCondition(existingValues)) { 95 | return false; 96 | } 97 | } 98 | var bytes = T.SerializeMany(values)!; 99 | _database.Upsert(AcquireKey(key), bytes, encryptionKey); 100 | return true; 101 | } 102 | 103 | /// 104 | public bool UpsertMany(string key, ReadOnlySpan values, string encryptionKey = "", Func? updateCondition = null) { 105 | return UpsertMany(key, values.ToArray(), encryptionKey, updateCondition); 106 | } 107 | 108 | /// 109 | public bool Remove(string key) { 110 | return _database.Remove(AcquireKey(key)); 111 | } 112 | 113 | 114 | /// 115 | public void Remove(Func keySelector) { 116 | _database.Remove(keySelector, KeyFilter); 117 | } 118 | 119 | 120 | /// 121 | public void Serialize() { 122 | _database.Serialize(); 123 | } 124 | 125 | 126 | /// 127 | public ValueTask SerializeAsync(CancellationToken cancellationToken = default) { 128 | return _database.SerializeAsync(cancellationToken); 129 | } 130 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Trimming", "IL2091:Target generic argument does not satisfy 'DynamicallyAccessedMembersAttribute' in target method or type. The generic parameter of the source method or type does not have matching annotations.", Justification = "See \"NativeAot Guide\" in the README.md", Scope = "member", Target = "~M:Sharpify.Data.Database.TryGetValue``1(System.String,System.String,``0@)~System.Boolean")] 9 | -------------------------------------------------------------------------------- /src/Sharpify.Data/IFilterable{T}.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Data; 2 | 3 | /// 4 | /// Represents a filterable type. 5 | /// 6 | /// The type of the filterable object. 7 | public interface IFilterable { 8 | /// 9 | /// Serializes the specified value into a byte array. 10 | /// 11 | /// 12 | /// 13 | static abstract byte[]? Serialize(T? value); 14 | 15 | /// 16 | /// Serializes multiple values into a byte array. 17 | /// 18 | /// 19 | /// 20 | static abstract byte[]? SerializeMany(T[]? values); 21 | 22 | /// 23 | /// Deserializes the specified data into a value. 24 | /// 25 | /// 26 | /// 27 | static abstract T? Deserialize(ReadOnlySpan data); 28 | 29 | /// 30 | /// Deserializes the specified data into multiple values. 31 | /// 32 | /// 33 | /// 34 | static abstract T[]? DeserializeMany(ReadOnlySpan data); 35 | } 36 | -------------------------------------------------------------------------------- /src/Sharpify.Data/MemoryPackDatabaseFilter{T}.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | using MemoryPack; 4 | 5 | using Sharpify.Collections; 6 | 7 | namespace Sharpify.Data; 8 | 9 | /// 10 | /// Provides a light database filter by type. 11 | /// 12 | /// 13 | /// Items that are upserted into the database using the filter, should not be retrieved without the filter as the key is modified. 14 | /// 15 | /// 16 | public class MemoryPackDatabaseFilter : IDatabaseFilter where T : IMemoryPackable { 17 | /// 18 | /// The key filter, statically created for the type. 19 | /// 20 | public static readonly string KeyFilter = $"{typeof(T).Name}:"; 21 | 22 | /// 23 | /// Creates a combined key (filter) for the specified key. 24 | /// 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | protected string AcquireKey(ReadOnlySpan key) { 27 | return string.Intern(KeyFilter.Concat(key)); 28 | } 29 | 30 | /// 31 | /// The database. 32 | /// 33 | protected readonly Database _database; 34 | 35 | /// 36 | /// Creates a new database filter. 37 | /// 38 | /// 39 | public MemoryPackDatabaseFilter(Database database) { 40 | _database = database; 41 | } 42 | 43 | /// 44 | public bool ContainsKey(string key) { 45 | return _database.ContainsKey(AcquireKey(key)); 46 | } 47 | 48 | /// 49 | public bool TryGetValue(string key, string encryptionKey, out T value) { 50 | return _database.TryGetValue(AcquireKey(key), encryptionKey, out value); 51 | } 52 | 53 | /// 54 | public bool TryGetValues(string key, string encryptionKey, out T[] values) { 55 | return _database.TryGetValues(AcquireKey(key), encryptionKey, out values); 56 | } 57 | 58 | /// 59 | public RentedBufferWriter TryReadToRentedBuffer(string key, string encryptionKey = "", int reservedCapacity = 0) { 60 | return _database.TryReadToRentedBuffer(AcquireKey(key), encryptionKey, reservedCapacity); 61 | } 62 | 63 | /// 64 | public bool Upsert(string key, T value, string encryptionKey = "", Func? updateCondition = null) { 65 | return _database.Upsert(AcquireKey(key), value, encryptionKey, updateCondition); 66 | } 67 | 68 | /// 69 | public bool UpsertMany(string key, T[] values, string encryptionKey = "", Func? updateCondition = null) { 70 | return _database.UpsertMany(AcquireKey(key), values, encryptionKey, updateCondition); 71 | } 72 | 73 | /// 74 | public bool UpsertMany(string key, ReadOnlySpan values, string encryptionKey = "", Func? updateCondition = null) { 75 | return _database.UpsertMany(AcquireKey(key), values, encryptionKey, updateCondition); 76 | } 77 | 78 | /// 79 | public bool Remove(string key) { 80 | return _database.Remove(AcquireKey(key)); 81 | } 82 | 83 | /// 84 | public void Remove(Func keySelector) { 85 | _database.Remove(keySelector, KeyFilter); 86 | } 87 | 88 | /// 89 | public void Serialize() { 90 | _database.Serialize(); 91 | } 92 | 93 | /// 94 | public ValueTask SerializeAsync(CancellationToken cancellationToken = default) { 95 | return _database.SerializeAsync(cancellationToken); 96 | } 97 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/README.md: -------------------------------------------------------------------------------- 1 | # Sharpify.Data 2 | 3 | An extension of `Sharpify` focused on data. 4 | 5 | ## Features 6 | 7 | * `Database` is the base type for the data base, it is key-value-pair based local database - saved on disk. 8 | * `IDatabaseFilter` is an interface which acts as an alternative to `DbContext` and provides enhanced type safety for contexts. 9 | * `MemoryPackDatabaseFilter` is an implementation which focuses on types that implement `IMemoryPackable` from `MemoryPack`. 10 | * `FlexibleDatabaseFilter` is an implementation focusing on types which need custom serialization logic. To use this, you type `T` will need to implement `IFilterable` which has methods for serialization and deserialization of single `T` and `T[]`. If you can choose to implement only one of the two. 11 | * **Concurrency** - `Database` uses highly performant synchronous concurrency models and is completely thread-safe. 12 | * **Disk Usage** - `Database` tracks inner changes and skips serialization if no changes occurred, enabling usage of periodic serialization without resource waste. 13 | * **GC Optimization** - `Database` heavily uses pooling for encryption, decryption, type conversion, serialization and deserialization to minimize GC overhead, very rarely does it allocate single-use memory and only when absolutely necessary. 14 | * **HotPath APIs** - `Database` is optimized for hot paths, as such it provides a number of APIs that specifically combine features for maximum performance and minimal GC overhead. Like the `TryReadToRentedBuffer` methods which is optimized for adding data to a table. 15 | * **Runtime Optimization** - Upon initialization, `Database` chooses specific serializers and deserializers tailored for specific configurations, minimizing the amount of runnable code during runtime that would've been wasted on different checks. 16 | 17 | ## Notes 18 | 19 | * Initialization of with regular and async factory methods, they will guide you for using the options of configuration. 20 | * It is crucial to use the factory methods for database initialization, and **NOT** use activators or constructors, the factory methods select configuration specific abstractions that are optimized per the the type of database you want. 21 | * The heart of the performance of these databases which use [MemoryPack](https://github.com/Cysharp/MemoryPack) for extreme performance binary serialization. 22 | * `Database` has upsert overloads which support any `IMemoryPackable` from [MemoryPack](https://github.com/Cysharp/MemoryPack). 23 | * Both `Database` implements `IDisposable` and should be disposed after usage to make sure all resources are released, this should also prevent possible issues if the object is removed from memory while an operation is ongoing (i.e the user closes the application when a write isn't finished) 24 | * The database is key-value-pair based, and operation on each key have O(1) complexity, serialization scales rather linearly (No way around it). 25 | * For very large datasets, there might be more suitable databases, but if you still want to use this, you could enable `[gcAllowVeryLargeObjects](https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/runtime/gcallowverylargeobjects-element), as per the Microsoft docs, on 64 bit system it should allow the object to be larger than 2GB, which is normally the limit. 26 | * To ensure integrity data copies are kept to a minimum, and allocations are designed to happen only when required to ensure data integrity (i.e to ensure the database stores real data, and to ensure the actual data is not exposed to the outside), the database uses pooling for any disposable memory operations to ensure minimal GC overhead. 27 | 28 | ## NativeAot Guide 29 | 30 | As of writing this, `MemoryPack`'s NativeAot support is broken, for any type that isn't already in their cached types, the `MemoryPackFormatterProvider` uses reflection to get the formatter (that includes types decorated with `MemoryPackable` which in turn implement `IMemoryPackable`), which fails in NativeAot. 31 | As a workaround, we need to add the formatters ourselves, to do this, take any 1 static entry point, that activates before the database is loaded, and add this: 32 | 33 | ```csharp 34 | // for every T type that relies on MemoryPack for serialization, and their inheritance hierarchy 35 | // This includes types that implement IMemoryPackable (i.e types that are decorated with MemoryPackable) 36 | MemoryPackFormatterProvider.Register(); 37 | // If the type is a collection or dictionary use the other corresponding overloads: 38 | MemoryPackFormatterProvider.RegisterCollection(); 39 | // or 40 | MemoryPackFormatterProvider.RegisterDictionary(); 41 | // and so on... 42 | // for all overloads check peek the definition of MemoryPackFormatterProvider, or their Github Repo 43 | ``` 44 | 45 | **Note:** Make sure you don't create a new static constructor in those types, `MemoryPack` already creates those, you will need to find a different entry point. 46 | 47 | With this the serializer should be able to bypass the part using reflection, and thus work even on NativeAot. 48 | 49 | P.S. The base type of the Database is already registered the same way on its own static constructor. 50 | 51 | ## Contact 52 | 53 | For bug reports, feature requests or offers of support/sponsorship contact 54 | -------------------------------------------------------------------------------- /src/Sharpify.Data/Serializers/AbstractSerializer.cs: -------------------------------------------------------------------------------- 1 | using MemoryPack; 2 | 3 | namespace Sharpify.Data.Serializers; 4 | 5 | /// 6 | /// Provides an abstraction for creating a readonly serializer 7 | /// 8 | internal abstract class AbstractSerializer { 9 | protected readonly string _path; 10 | internal readonly MemoryPackSerializerOptions SerializerOptions; 11 | 12 | protected AbstractSerializer(string path, StringEncoding encoding = StringEncoding.Utf8) { 13 | _path = path; 14 | SerializerOptions = encoding switch { 15 | StringEncoding.Utf8 => MemoryPackSerializerOptions.Utf8, 16 | StringEncoding.Utf16 => MemoryPackSerializerOptions.Utf16, 17 | _ => MemoryPackSerializerOptions.Default 18 | }; 19 | } 20 | 21 | /// 22 | /// Serializes the given dictionary 23 | /// 24 | /// 25 | /// 26 | internal abstract void Serialize(Dictionary dict, int estimatedSize); 27 | 28 | /// 29 | /// Serializes the given dictionary asynchronously 30 | /// 31 | /// 32 | /// 33 | /// 34 | internal abstract ValueTask SerializeAsync(Dictionary dict, CancellationToken cancellationToken = default); 35 | 36 | /// 37 | /// Deserializes the path to a dictionary 38 | /// 39 | /// 40 | internal abstract Dictionary Deserialize(int estimatedSize); 41 | 42 | /// 43 | /// Deserializes the path to a dictionary asynchronously 44 | /// 45 | /// 46 | /// 47 | internal abstract ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default); 48 | 49 | /// 50 | /// Creates a serializer based on the given configuration 51 | /// 52 | /// 53 | /// 54 | /// 55 | internal static AbstractSerializer Create(DatabaseConfiguration configuration) { 56 | return configuration switch { 57 | { Path: "", IgnoreCase: false } => new DisabledSerializer(configuration.Path, configuration.Encoding), 58 | { Path: "", IgnoreCase: true } => new DisabledIgnoreCaseSerializer(configuration.Path, configuration.Encoding), 59 | { HasEncryption: true, IgnoreCase: true } => new IgnoreCaseEncryptedSerializer(configuration.Path, configuration.EncryptionKey), 60 | { HasEncryption: true, IgnoreCase: false } => new EncryptedSerializer(configuration.Path, configuration.EncryptionKey), 61 | { HasEncryption: false, IgnoreCase: true } => new IgnoreCaseSerializer(configuration.Path), 62 | { HasEncryption: false, IgnoreCase: false } => new Serializer(configuration.Path), 63 | _ => throw new ArgumentException("Invalid configuration") 64 | }; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/Serializers/DisabledSerializers.cs: -------------------------------------------------------------------------------- 1 | using MemoryPack; 2 | 3 | namespace Sharpify.Data.Serializers; 4 | 5 | /// 6 | /// A serializer for a database without encryption and case sensitive keys 7 | /// 8 | internal class DisabledSerializer : AbstractSerializer { 9 | internal DisabledSerializer(string path, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { 10 | } 11 | 12 | /// 13 | internal override Dictionary Deserialize(int estimatedSize) => new(); 14 | 15 | /// 16 | internal override ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) => ValueTask.FromResult(new Dictionary()); 17 | 18 | /// 19 | internal override void Serialize(Dictionary dict, int estimatedSize) { } 20 | 21 | /// 22 | internal override ValueTask SerializeAsync(Dictionary dict, CancellationToken cancellationToken = default) => ValueTask.CompletedTask; 23 | } 24 | 25 | /// 26 | /// A serializer for a database without encryption and case sensitive keys 27 | /// 28 | internal class DisabledIgnoreCaseSerializer : DisabledSerializer { 29 | internal DisabledIgnoreCaseSerializer(string path, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { 30 | } 31 | 32 | /// 33 | internal override Dictionary Deserialize(int estimatedSize) => new(StringComparer.OrdinalIgnoreCase); 34 | 35 | /// 36 | internal override ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) => ValueTask.FromResult(new Dictionary(StringComparer.OrdinalIgnoreCase)); 37 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/Serializers/EncryptedSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | using MemoryPack; 4 | 5 | using Sharpify.Collections; 6 | 7 | namespace Sharpify.Data.Serializers; 8 | 9 | /// 10 | /// A serializer for a database encryption and case sensitive keys 11 | /// 12 | internal class EncryptedSerializer : AbstractSerializer { 13 | protected readonly string _key; 14 | 15 | internal EncryptedSerializer(string path, string key, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { 16 | _key = key; 17 | } 18 | 19 | /// 20 | internal override Dictionary Deserialize(int estimatedSize) { 21 | if (estimatedSize is 0) { 22 | return new Dictionary(); 23 | } 24 | 25 | using var rawBuffer = new RentedBufferWriter(estimatedSize); 26 | using var file = new FileStream(_path, FileMode.Open); 27 | int rawRead = file.Read(rawBuffer.GetSpan()); 28 | rawBuffer.Advance(rawRead); 29 | ReadOnlySpan rawSpan = rawBuffer.WrittenSpan; 30 | using var decryptedBuffer = new RentedBufferWriter(rawSpan.Length); 31 | int decryptedRead = Helper.Instance.Decrypt(rawSpan, decryptedBuffer.GetSpan(), _key); 32 | decryptedBuffer.Advance(decryptedRead); 33 | ReadOnlySpan decrypted = decryptedBuffer.WrittenSpan; 34 | var dict = MemoryPackSerializer.Deserialize>(decrypted, SerializerOptions); 35 | return dict ?? new Dictionary(); 36 | } 37 | 38 | /// 39 | internal override async ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) { 40 | if (estimatedSize is 0) { 41 | return new Dictionary(); 42 | } 43 | using var file = new FileStream(_path, FileMode.Open); 44 | using var transform = Helper.Instance.GetDecryptor(_key); 45 | using var cryptoStream = new CryptoStream(file, transform, CryptoStreamMode.Read); 46 | var dict = await MemoryPackSerializer.DeserializeAsync>(cryptoStream, SerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); 47 | return dict ?? new Dictionary(); 48 | } 49 | 50 | /// 51 | internal override void Serialize(Dictionary dict, int estimatedSize) { 52 | using var buffer = new RentedBufferWriter(estimatedSize + AesProvider.ReservedBufferSize); 53 | MemoryPackSerializer.Serialize(buffer, dict, SerializerOptions); 54 | using var file = new FileStream(_path, FileMode.Create); 55 | using ICryptoTransform transform = Helper.Instance.GetEncryptor(_key); 56 | using var cryptoStream = new CryptoStream(file, transform, CryptoStreamMode.Write); 57 | cryptoStream.Write(buffer.WrittenSpan); 58 | } 59 | 60 | /// 61 | internal override async ValueTask SerializeAsync(Dictionary dict, CancellationToken cancellationToken = default) { 62 | using var file = new FileStream(_path, FileMode.Create); 63 | using ICryptoTransform transform = Helper.Instance.GetEncryptor(_key); 64 | using var cryptoStream = new CryptoStream(file, transform, CryptoStreamMode.Write); 65 | await MemoryPackSerializer.SerializeAsync(cryptoStream, dict, SerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); 66 | } 67 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/Serializers/IgnoreCaseEncryptedSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | using MemoryPack; 4 | 5 | using Sharpify.Collections; 6 | 7 | namespace Sharpify.Data.Serializers; 8 | 9 | /// 10 | /// A serializer for a database encryption and case-sensitive keys 11 | /// 12 | internal class IgnoreCaseEncryptedSerializer : EncryptedSerializer { 13 | internal IgnoreCaseEncryptedSerializer(string path, string key, StringEncoding encoding = StringEncoding.Utf8) : base(path, key, encoding) { 14 | } 15 | 16 | /// 17 | internal override Dictionary Deserialize(int estimatedSize) { 18 | if (estimatedSize is 0) { 19 | return new Dictionary(StringComparer.OrdinalIgnoreCase); 20 | } 21 | using var rawBuffer = new RentedBufferWriter(estimatedSize); 22 | using var file = new FileStream(_path, FileMode.Open); 23 | int rawRead = file.Read(rawBuffer.GetSpan()); 24 | rawBuffer.Advance(rawRead); 25 | ReadOnlySpan rawSpan = rawBuffer.WrittenSpan; 26 | using var decryptedBuffer = new RentedBufferWriter(rawSpan.Length); 27 | int decryptedRead = Helper.Instance.Decrypt(rawSpan, decryptedBuffer.GetSpan(), _key); 28 | decryptedBuffer.Advance(decryptedRead); 29 | ReadOnlySpan decrypted = decryptedBuffer.WrittenSpan; 30 | Dictionary dict = IgnoreCaseSerializer.FromSpan(decrypted); 31 | return dict; 32 | } 33 | 34 | /// 35 | internal override async ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) { 36 | if (estimatedSize is 0) { 37 | return new Dictionary(StringComparer.OrdinalIgnoreCase); 38 | } 39 | using var buffer = new RentedBufferWriter(estimatedSize); 40 | using var file = new FileStream(_path, FileMode.Open); 41 | using ICryptoTransform transform = Helper.Instance.GetDecryptor(_key); 42 | using var cryptoStream = new CryptoStream(file, transform, CryptoStreamMode.Read); 43 | int numRead = await cryptoStream.ReadAsync(buffer.GetMemory(), cancellationToken).ConfigureAwait(false); 44 | buffer.Advance(numRead); 45 | Dictionary dict = IgnoreCaseSerializer.FromSpan(buffer.WrittenMemory); 46 | return dict; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/Serializers/IgnoreCaseSerializer.cs: -------------------------------------------------------------------------------- 1 | using MemoryPack; 2 | 3 | using Sharpify.Collections; 4 | 5 | namespace Sharpify.Data.Serializers; 6 | 7 | /// 8 | /// A serializer for a database without encryption and case sensitive keys 9 | /// 10 | internal class IgnoreCaseSerializer : Serializer { 11 | internal IgnoreCaseSerializer(string path, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { 12 | } 13 | 14 | internal static Dictionary FromSpan(ReadOnlyMemory bin) { 15 | ReadOnlySpan data = bin.Span; 16 | return FromSpan(data); 17 | } 18 | 19 | internal static Dictionary FromSpan(ReadOnlySpan bin) { 20 | if (bin.Length is 0) { 21 | return new Dictionary(StringComparer.OrdinalIgnoreCase); 22 | } 23 | var formatter = new OrdinalIgnoreCaseStringDictionaryFormatter(); 24 | var state = MemoryPackReaderOptionalStatePool.Rent(MemoryPackSerializerOptions.Default); 25 | var reader = new MemoryPackReader(bin, state); 26 | Dictionary? dict = null; 27 | formatter.GetFormatter().Deserialize(ref reader, ref dict); 28 | return dict ?? new Dictionary(StringComparer.OrdinalIgnoreCase); 29 | } 30 | 31 | /// 32 | internal override Dictionary Deserialize(int estimatedSize) { 33 | if (estimatedSize is 0) { 34 | return new Dictionary(StringComparer.OrdinalIgnoreCase); 35 | } 36 | using var buffer = new RentedBufferWriter(estimatedSize); 37 | using var file = new FileStream(_path, FileMode.Open); 38 | int numRead = file.Read(buffer.Buffer, 0, estimatedSize); 39 | buffer.Advance(numRead); 40 | ReadOnlySpan deserialized = buffer.WrittenSpan; 41 | Dictionary dict = FromSpan(deserialized); 42 | return dict; 43 | } 44 | 45 | /// 46 | internal override async ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) { 47 | if (estimatedSize is 0) { 48 | return new Dictionary(StringComparer.OrdinalIgnoreCase); 49 | } 50 | using var buffer = new RentedBufferWriter(estimatedSize); 51 | using var file = new FileStream(_path, FileMode.Open); 52 | int numRead = await file.ReadAsync(buffer.GetMemory(), cancellationToken).ConfigureAwait(false); 53 | buffer.Advance(numRead); 54 | Dictionary dict = FromSpan(buffer.WrittenMemory); 55 | return dict; 56 | } 57 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/Serializers/Serializer.cs: -------------------------------------------------------------------------------- 1 | using MemoryPack; 2 | 3 | using Sharpify.Collections; 4 | 5 | namespace Sharpify.Data.Serializers; 6 | 7 | /// 8 | /// A serializer for a database without encryption and case sensitive keys 9 | /// 10 | internal class Serializer : AbstractSerializer { 11 | internal Serializer(string path, StringEncoding encoding = StringEncoding.Utf8) : base(path, encoding) { 12 | } 13 | 14 | /// 15 | internal override Dictionary Deserialize(int estimatedSize) { 16 | if (estimatedSize is 0) { 17 | return new Dictionary(); 18 | } 19 | using var buffer = new RentedBufferWriter(estimatedSize); 20 | using var file = new FileStream(_path, FileMode.Open); 21 | int numRead = file.Read(buffer.Buffer, 0, estimatedSize); 22 | buffer.Advance(numRead); 23 | Dictionary dict = 24 | MemoryPackSerializer.Deserialize>(buffer.WrittenSpan, SerializerOptions) 25 | ?? new Dictionary(); 26 | return dict; 27 | } 28 | 29 | /// 30 | internal override async ValueTask> DeserializeAsync(int estimatedSize, CancellationToken cancellationToken = default) { 31 | if (estimatedSize is 0) { 32 | return new Dictionary(); 33 | } 34 | using var file = new FileStream(_path, FileMode.Open); 35 | var dict = await MemoryPackSerializer.DeserializeAsync>(file, SerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); 36 | return dict ?? new Dictionary(); 37 | } 38 | 39 | /// 40 | internal override void Serialize(Dictionary dict, int estimatedSize) { 41 | using var file = new FileStream(_path, FileMode.Create); 42 | using var buffer = new RentedBufferWriter(estimatedSize); 43 | MemoryPackSerializer.Serialize(in buffer, in dict, SerializerOptions); 44 | file.Write(buffer.WrittenSpan); 45 | } 46 | 47 | /// 48 | internal override async ValueTask SerializeAsync(Dictionary dict, CancellationToken cancellationToken = default) { 49 | using var file = new FileStream(_path, FileMode.Create); 50 | await MemoryPackSerializer.SerializeAsync(file, dict, SerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); 51 | } 52 | } -------------------------------------------------------------------------------- /src/Sharpify.Data/Sharpify.Data.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0;net8.0 5 | enable 6 | 2.6.0 7 | enable 8 | true 9 | David Shnayder 10 | David Shnayder 11 | MIT 12 | CHANGELOGLATEST.md 13 | True 14 | Sharpify.Data 15 | An extension of Sharpify, focused on Data 16 | https://github.com/dusrdev/Sharpify 17 | https://github.com/dusrdev/Sharpify 18 | git 19 | Extensions;HighPerformance;Data;Database 20 | true 21 | true 22 | false 23 | true 24 | true 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | <_Parameter1>Sharpify.Data.Tests 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Sharpify/Build.txt: -------------------------------------------------------------------------------- 1 | nuget: 2 | dotnet clean -c Release 3 | dotnet build -c Release 4 | dotnet pack -c Release -p:SignAssembly="" 5 | 6 | dll: 7 | dotnet build -c Release -p:SignAssembly="" 8 | 9 | docs: 10 | git subtree push --prefix docs https://github.com/dusrdev/Sharpify.wiki.git master 11 | -------------------------------------------------------------------------------- /src/Sharpify/CHANGELOGLATEST.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v2.5.0 4 | 5 | * Updated to support .NET 9.0 and optimized certain methods to use .NET 9 specific API's wherever possible. 6 | * Added `BufferWrapper` which can be used to append items to a `Span` without managing indexes and capacity. This buffer also implement `IBufferWriter`, and as a `ref struct implementing an interface` it is only available on .NET 9.0 and above. 7 | * `Utils.String.FormatBytes` now uses a much larger buffer size of 512 chars by default, to handle the edge case of `double.MaxValue` which would previously cause an `ArgumentOutOfRangeException` to be thrown or similarly any number of bytes that would be bigger than 1024 petabytes. The result will now also include thousands separators to improve readability. 8 | * The inner implementation that uses this buffer size is pooled so this should not have any impact on performance. 9 | -------------------------------------------------------------------------------- /src/Sharpify/Collections/BufferWrapper{T}.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace Sharpify.Collections; 4 | 5 | #if NET9_0_OR_GREATER 6 | /// 7 | /// Represents a buffer than be used to efficiently append items to a span. 8 | /// 9 | public ref struct BufferWrapper : IBufferWriter { 10 | private readonly Span _buffer; 11 | 12 | /// 13 | /// The total length of the buffer. 14 | /// 15 | public readonly int Length; 16 | 17 | /// 18 | /// The current position of the buffer. 19 | /// 20 | public int Position { get; private set; } 21 | 22 | /// 23 | /// Initializes a string buffer that uses a pre-allocated buffer (potentially from the stack). 24 | /// 25 | public static BufferWrapper Create(Span buffer) => new(buffer); 26 | 27 | /// 28 | /// Represents a mutable interface over a buffer allocated in memory. 29 | /// 30 | private BufferWrapper(Span buffer) { 31 | _buffer = buffer; 32 | Length = _buffer.Length; 33 | Position = 0; 34 | } 35 | 36 | /// 37 | /// This returns an empty buffer. It will throw if you try to append anything to it. use instead. 38 | /// 39 | public BufferWrapper() : this(Span.Empty) { 40 | } 41 | 42 | /// 43 | /// Resets the buffer to the beginning. 44 | /// 45 | public void Reset() => Position = 0; 46 | 47 | /// 48 | /// Appends an item to the end of the buffer. 49 | /// 50 | public void Append(T item) { 51 | ArgumentOutOfRangeException.ThrowIfGreaterThan(Position + 1, Length); 52 | _buffer[Position++] = item; 53 | } 54 | 55 | /// 56 | /// Appends the span of items to the end of the buffer. 57 | /// 58 | public void Append(ReadOnlySpan items) { 59 | ArgumentOutOfRangeException.ThrowIfGreaterThan(Position + items.Length, Length); 60 | items.CopyTo(_buffer.Slice(Position)); 61 | Position += items.Length; 62 | } 63 | 64 | /// 65 | public void Advance(int count) => Position += count; 66 | 67 | /// 68 | public Memory GetMemory(int sizeHint = 0) => throw new NotSupportedException("BufferWrapper does not support GetMemory"); 69 | 70 | /// 71 | public Span GetSpan(int sizeHint = 0) => _buffer.Slice(Position); 72 | 73 | /// 74 | /// Returns the character at the specified index. 75 | /// 76 | /// 77 | public readonly T this[int index] => _buffer[index]; 78 | 79 | /// 80 | /// Returns the used portion of the buffer as a readonly span. 81 | /// 82 | public readonly ReadOnlySpan WrittenSpan => _buffer.Slice(0, Position); 83 | } 84 | #endif -------------------------------------------------------------------------------- /src/Sharpify/Collections/LazyLocalPersistentDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace Sharpify.Collections; 4 | 5 | /// 6 | /// Represents a dictionary that persists its data to a local file but not in memory. 7 | /// 8 | public class LazyLocalPersistentDictionary : PersistentDictionary { 9 | private readonly string _path; 10 | private readonly StringComparer _stringComparer; 11 | private static readonly Dictionary Empty = []; 12 | 13 | /// 14 | /// Creates a new instance of with the and specified. 15 | /// 16 | /// The full path to the file to persist the dictionary to. 17 | /// The comparer to use for the dictionary. 18 | public LazyLocalPersistentDictionary(string path, StringComparer comparer) { 19 | _path = path; 20 | _stringComparer = comparer; 21 | } 22 | 23 | /// 24 | /// Creates a new instance of with the and . 25 | /// 26 | /// The path to the file to persist the dictionary to. 27 | public LazyLocalPersistentDictionary(string path) : this(path, StringComparer.Ordinal) { } 28 | 29 | /// 30 | /// Retrieves the value associated with the specified key from the persistent dictionary. 31 | /// 32 | /// The key of the value to retrieve. 33 | /// 34 | /// The value associated with the specified key if it exists in the dictionary; otherwise, null. 35 | /// 36 | protected override string? GetValueByKey(string key) { 37 | if (!File.Exists(_path)) { 38 | return null; 39 | } 40 | var length = checked((int)new FileInfo(_path).Length); 41 | if (length is 0) { 42 | return null; 43 | } 44 | using var buffer = new RentedBufferWriter(length); 45 | using var file = File.Open(_path, FileMode.Open); 46 | var numRead = file.Read(buffer.GetSpan()); 47 | buffer.Advance(numRead); 48 | ReadOnlySpan jsonUtf8Bytes = buffer.WrittenSpan; 49 | var reader = new Utf8JsonReader(jsonUtf8Bytes, InternalHelper.JsonReaderOptions); 50 | while (reader.Read()) { 51 | if (reader.TokenType is not JsonTokenType.PropertyName) { 52 | continue; 53 | } 54 | var property = reader.GetString(); 55 | if (!_stringComparer.Equals(property, key)) { 56 | _ = reader.TrySkip(); 57 | continue; 58 | } 59 | reader.Read(); 60 | var value = reader.GetString(); 61 | return value; 62 | } 63 | return null; 64 | } 65 | 66 | /// 67 | /// Sets the key and value in the dictionary. 68 | /// If the dictionary file does not exist, a new dictionary is created and the key-value pair is added. 69 | /// If the dictionary file exists, the dictionary is deserialized and the key-value pair is added or updated. 70 | /// 71 | /// The key to set. 72 | /// The value to set. 73 | protected override void SetKeyAndValue(string key, string value) { 74 | if (!File.Exists(_path)) { 75 | _dict ??= new Dictionary(_stringComparer); 76 | _dict[key] = value; 77 | return; 78 | } 79 | var sDict = Deserialize(); 80 | if (sDict is null) { 81 | _dict ??= new Dictionary(_stringComparer); 82 | _dict[key] = value; 83 | return; 84 | } 85 | _dict = sDict; 86 | _dict[key] = value; 87 | } 88 | 89 | /// 90 | protected override Dictionary? Deserialize() { 91 | using var file = File.Open(_path, FileMode.Open); 92 | return JsonSerializer.Deserialize(file, JsonContext.Default.DictionaryStringString); 93 | } 94 | 95 | /// 96 | protected override async Task SerializeAsync() { 97 | await using var file = File.Open(_path, FileMode.Create); 98 | await JsonSerializer.SerializeAsync(file, _dict, JsonContext.Default.DictionaryStringString).ConfigureAwait(false); 99 | _dict = Empty; 100 | } 101 | } -------------------------------------------------------------------------------- /src/Sharpify/Collections/LocalPersistentDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace Sharpify.Collections; 4 | 5 | /// 6 | /// Represents a dictionary that persists its data to a local file. 7 | /// 8 | public class LocalPersistentDictionary : PersistentDictionary { 9 | private readonly string _path; 10 | 11 | /// 12 | /// Creates a new instance of with the and specified. 13 | /// 14 | /// The full path to the file to persist the dictionary to. 15 | /// The comparer to use for the dictionary. 16 | public LocalPersistentDictionary(string path, StringComparer comparer) { 17 | _path = path; 18 | if (!File.Exists(_path)) { 19 | _dict = new Dictionary(comparer); 20 | return; 21 | } 22 | var sDict = Deserialize(); 23 | if (sDict is null) { 24 | _dict = new Dictionary(comparer); 25 | return; 26 | } 27 | _dict = new Dictionary(sDict, comparer); 28 | } 29 | 30 | /// 31 | /// Creates a new instance of with the and . 32 | /// 33 | /// The path to the file to persist the dictionary to. 34 | public LocalPersistentDictionary(string path) : this(path, StringComparer.Ordinal) { } 35 | 36 | /// 37 | protected override Dictionary? Deserialize() { 38 | using var file = File.Open(_path, FileMode.Open); 39 | return JsonSerializer.Deserialize(file, JsonContext.Default.DictionaryStringString); 40 | } 41 | 42 | /// 43 | protected override async Task SerializeAsync() { 44 | await using var file = File.Open(_path, FileMode.Create); 45 | await JsonSerializer.SerializeAsync(file, _dict, JsonContext.Default.DictionaryStringString).ConfigureAwait(false); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Sharpify/Either.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify; 2 | 3 | /// 4 | /// Discriminated union of two types. 5 | /// 6 | public readonly record struct Either { 7 | private readonly T0? _value0; 8 | private readonly T1? _value1; 9 | 10 | /// 11 | /// Checks if the value is T0. 12 | /// 13 | public readonly bool IsT0; 14 | 15 | /// 16 | /// Checks if the value is T1. 17 | /// 18 | public readonly bool IsT1; 19 | 20 | /// 21 | /// Gets the value as T0. 22 | /// 23 | public T0 AsT0 => _value0 ?? throw new InvalidOperationException("T0 is null"); 24 | 25 | /// 26 | /// Gets the value as T1. 27 | /// 28 | public T1 AsT1 => _value1 ?? throw new InvalidOperationException("T1 is null"); 29 | 30 | /// 31 | /// Creates a new instance of with both values set to null. 32 | /// 33 | /// 34 | /// This is an implementation of the default constructor, do not use this. Use only the implicit converters from either T0 or T1. 35 | /// 36 | public Either() => throw new InvalidOperationException("Either cannot be instantiated directly. Use implicit converters from either T0 or T1."); 37 | 38 | private Either(T0 value) { 39 | _value0 = value; 40 | _value1 = default; 41 | IsT0 = true; 42 | IsT1 = false; 43 | } 44 | 45 | private Either(T1 value) { 46 | _value0 = default; 47 | _value1 = value; 48 | IsT0 = false; 49 | IsT1 = true; 50 | } 51 | 52 | /// 53 | /// Implicitly converts from T0 to . 54 | /// 55 | public static implicit operator Either(T0 value) => new(value); 56 | 57 | /// 58 | /// Implicitly converts from T1 to . 59 | /// 60 | public static implicit operator Either(T1 value) => new(value); 61 | 62 | /// 63 | /// Switches on the type of the value. 64 | /// 65 | public void Switch(Action handleT0, Action handleT1) { 66 | if (IsT0) { 67 | handleT0(_value0!); 68 | } else if (IsT1) { 69 | handleT1(_value1!); 70 | } else { 71 | throw new InvalidOperationException("T0 and T1 are both null"); 72 | } 73 | } 74 | 75 | /// 76 | /// Matches on the type of the value to return a . 77 | /// 78 | public TResult Match(Func handleT0, Func handleT1) => IsT0 79 | ? handleT0(_value0!) 80 | : handleT1(_value1!); 81 | } -------------------------------------------------------------------------------- /src/Sharpify/ExtensionsHeader.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify; 2 | 3 | /// 4 | /// Provides a set of static extension methods 5 | /// 6 | public static partial class Extensions {} -------------------------------------------------------------------------------- /src/Sharpify/IAsyncAction.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify; 2 | 3 | /// 4 | /// Interface to implement for a parallel async action. 5 | /// 6 | public interface IAsyncAction { 7 | /// 8 | /// The main action to be performed. 9 | /// 10 | Task InvokeAsync(T item, CancellationToken token = default); 11 | } -------------------------------------------------------------------------------- /src/Sharpify/InternalHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Sharpify; 5 | 6 | internal static class InternalHelper { 7 | internal static readonly JsonReaderOptions JsonReaderOptions = new() { 8 | AllowTrailingCommas = true, 9 | CommentHandling = JsonCommentHandling.Skip 10 | }; 11 | } 12 | 13 | [JsonSourceGenerationOptions(WriteIndented = true)] 14 | [JsonSerializable(typeof(Dictionary))] 15 | internal partial class JsonContext : JsonSerializerContext { } -------------------------------------------------------------------------------- /src/Sharpify/MonitoredSerializableObject{T}.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization.Metadata; 3 | 4 | namespace Sharpify; 5 | 6 | /// 7 | /// Represents a that is monitored for changes from the file system. 8 | /// 9 | /// The type of the value stored in the object. 10 | /// 11 | /// This class provides functionality to serialize and deserialize the object to/from a file, 12 | /// and raises an event whenever the file or the object is modified. 13 | /// 14 | public class MonitoredSerializableObject : SerializableObject { 15 | private readonly FileSystemWatcher _watcher; 16 | 17 | /// 18 | /// Represents a serializable object that is monitored for changes in a specified file path. 19 | /// 20 | /// The path to the file. validated on creation 21 | /// The json type info that can be used to serialize T without reflection 22 | /// Thrown when the directory of the path does not exist or when the filename is invalid. 23 | public MonitoredSerializableObject(string path, JsonTypeInfo jsonTypeInfo) : this(path, default!, jsonTypeInfo) { } 24 | 25 | /// 26 | /// Represents a serializable object that is monitored for changes in a specified file path. 27 | /// 28 | /// The path to the file. validated on creation 29 | /// the default value of T, will be used if the file doesn't exist or can't be deserialized 30 | /// The json type info that can be used to serialize T without reflection 31 | /// Thrown when the directory of the path does not exist or when the filename is invalid. 32 | public MonitoredSerializableObject(string path, T defaultValue, JsonTypeInfo jsonTypeInfo) : base(path, defaultValue, jsonTypeInfo) { 33 | _watcher = new FileSystemWatcher(_segmentedPath.Directory, _segmentedPath.FileName) { 34 | NotifyFilter = NotifyFilters.LastWrite, 35 | EnableRaisingEvents = true 36 | }; 37 | 38 | _watcher.Changed += OnFileChanged; 39 | } 40 | 41 | private void OnFileChanged(object sender, FileSystemEventArgs e) { 42 | if (e.ChangeType is not WatcherChangeTypes.Changed) { 43 | return; 44 | } 45 | if (!File.Exists(_path)) { 46 | return; 47 | } 48 | try { 49 | _lock.EnterWriteLock(); 50 | var json = File.ReadAllText(_path); 51 | _value = JsonSerializer.Deserialize(json, _jsonTypeInfo)!; 52 | InvokeOnChangedEvent(_value); 53 | } catch { 54 | // ignore 55 | } finally { 56 | _lock.ExitWriteLock(); 57 | } 58 | } 59 | 60 | /// 61 | public override void Modify(Func modifier) { 62 | _watcher.EnableRaisingEvents = false; 63 | base.Modify(modifier); 64 | _watcher.EnableRaisingEvents = true; 65 | } 66 | 67 | /// 68 | public override void Dispose() { 69 | if (_disposed) { 70 | return; 71 | } 72 | _watcher?.Dispose(); 73 | _lock?.Dispose(); 74 | _disposed = true; 75 | } 76 | } -------------------------------------------------------------------------------- /src/Sharpify/ParallelExtensions.cs: -------------------------------------------------------------------------------- 1 | using Sharpify.Collections; 2 | 3 | namespace Sharpify; 4 | 5 | public static partial class Extensions { 6 | /// 7 | /// An extension method to perform an action on a collection of items in parallel. 8 | /// 9 | /// The source collection. 10 | /// The action to be performed. 11 | /// The cancellation token. 12 | /// A task that completes when the entire has been processed. 13 | /// 14 | /// If is null or empty, the task will return immediately 15 | /// The cancellation token will be injected into for each item of the collection 16 | /// Unlike , this method is optimized for synchronous lambdas that don't need to allocate an AsyncStateMachine 17 | /// 18 | public static async Task ForAll( 19 | this ICollection? collection, 20 | Func body, 21 | CancellationToken token = default) { 22 | if (collection is null or { Count: 0 }) { 23 | return; 24 | } 25 | 26 | var length = collection.Count; 27 | 28 | using var taskBuffer = new RentedBufferWriter(length); 29 | 30 | foreach (var item in collection) { 31 | taskBuffer.WriteAndAdvance(body.Invoke(item, token)); 32 | } 33 | 34 | #if NET9_0_OR_GREATER 35 | await Task.WhenAll(taskBuffer.WrittenSpan).WaitAsync(token).ConfigureAwait(false); 36 | #else 37 | await Task.WhenAll(taskBuffer.WrittenSegment).WaitAsync(token).ConfigureAwait(false); 38 | #endif 39 | } 40 | 41 | /// 42 | /// An extension method to perform an action on a collection of items in parallel. 43 | /// 44 | /// The source collection. 45 | /// The action to be performed. 46 | /// The cancellation token. 47 | /// A task that completes when the entire has been processed. 48 | /// 49 | /// If is null or empty, the task will return immediately 50 | /// The cancellation token will be injected into for each item of the collection 51 | /// Unlike , this method is optimized for synchronous lambdas that don't need to allocate an AsyncStateMachine 52 | /// 53 | public static Task ForAll( 54 | this ICollection? collection, 55 | IAsyncAction asyncAction, 56 | CancellationToken token = default) 57 | => ForAll(collection, asyncAction.InvokeAsync, token); 58 | 59 | /// 60 | /// An extension method to perform an action on a collection of items in parallel. 61 | /// 62 | /// The source collection. 63 | /// The action to be performed. 64 | /// The cancellation token. 65 | /// A task that completes when the entire has been processed. 66 | /// 67 | /// If is null or empty, the task will return immediately 68 | /// The cancellation token will be injected into for each item of the collection 69 | /// 70 | public static async Task ForAllAsync( 71 | this ICollection? collection, 72 | Func body, 73 | CancellationToken token = default) { 74 | if (collection is null or { Count: 0 }) { 75 | return; 76 | } 77 | 78 | var length = collection.Count; 79 | 80 | using var taskBuffer = new RentedBufferWriter(length); 81 | 82 | foreach (var item in collection) { 83 | taskBuffer.WriteAndAdvance(Task.Run(() => body.Invoke(item, token), token)); 84 | } 85 | 86 | #if NET9_0_OR_GREATER 87 | await Task.WhenAll(taskBuffer.WrittenSpan).WaitAsync(token).ConfigureAwait(false); 88 | #else 89 | await Task.WhenAll(taskBuffer.WrittenSegment).WaitAsync(token).ConfigureAwait(false); 90 | #endif 91 | } 92 | 93 | /// 94 | /// An extension method to perform an action on a collection of items in parallel. 95 | /// 96 | /// The source collection. 97 | /// The action to be performed. 98 | /// The cancellation token. 99 | /// A task that completes when the entire has been processed. 100 | /// 101 | /// If is null or empty, the task will return immediately 102 | /// The cancellation token will be injected into for each item of the collection 103 | /// 104 | public static Task ForAllAsync( 105 | this ICollection? collection, 106 | IAsyncAction asyncAction, 107 | CancellationToken token = default) 108 | => ForAllAsync(collection, asyncAction.InvokeAsync, token); 109 | } -------------------------------------------------------------------------------- /src/Sharpify/Routines/Routine.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Routines; 2 | 3 | /// 4 | /// Represents a routine that executes a list of actions at a specified interval. 5 | /// 6 | public class Routine : IDisposable { 7 | private readonly System.Timers.Timer _timer; 8 | private volatile bool _disposed; 9 | 10 | /// 11 | /// List of actions to be executed by the routine. 12 | /// 13 | public readonly List Actions = []; 14 | 15 | /// 16 | /// Initializes a new instance of the class with the specified interval. 17 | /// 18 | /// The time interval between timer events, in milliseconds. 19 | public Routine(double intervalInMilliseconds) { 20 | _timer = new System.Timers.Timer(intervalInMilliseconds); 21 | _timer.Elapsed += OnTimerElapsed; 22 | } 23 | 24 | /// 25 | /// Adds an action to the routine. 26 | /// 27 | /// The action to add. 28 | /// The updated routine. 29 | public Routine Add(Action action) { 30 | Actions.Add(action); 31 | return this; 32 | } 33 | 34 | /// 35 | /// Adds a collection of actions to the routine. 36 | /// 37 | /// The collection of actions to add. 38 | /// The updated routine. 39 | public Routine AddRange(IEnumerable actions) { 40 | Actions.AddRange(actions); 41 | return this; 42 | } 43 | 44 | /// 45 | /// Starts the routine timer. 46 | /// 47 | /// The current Routine instance. 48 | public Routine Start() { 49 | _timer.Start(); 50 | return this; 51 | } 52 | 53 | /// 54 | /// Stops the routine. 55 | /// 56 | public void Stop() { 57 | _timer.Stop(); 58 | } 59 | 60 | private void OnTimerElapsed(object? sender, EventArgs args) { 61 | foreach (var action in Actions) { 62 | action(); 63 | } 64 | } 65 | 66 | /// 67 | /// Disposes the timer and suppresses finalization of the object. 68 | /// 69 | public void Dispose() { 70 | if (_disposed) { 71 | return; 72 | } 73 | _timer?.Close(); 74 | _disposed = true; 75 | GC.SuppressFinalize(this); 76 | } 77 | } -------------------------------------------------------------------------------- /src/Sharpify/Routines/RoutineOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Routines; 2 | 3 | /// 4 | /// Options that can be used to configure the behavior of an async routine. 5 | /// 6 | [Flags] 7 | public enum RoutineOptions : byte { 8 | /// 9 | /// Represents the possible states of an asynchronous routine. 10 | /// 11 | None = 0, 12 | 13 | /// 14 | /// Flag that indicates whether the async routine should execute all the actions in parallel. 15 | /// 16 | ExecuteInParallel = 1 << 0, 17 | 18 | /// 19 | /// Flag indicating whether to throw an exception when the async routine is cancelled. 20 | /// 21 | ThrowOnCancellation = 1 << 1 22 | } -------------------------------------------------------------------------------- /src/Sharpify/SerializableObjectEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify; 2 | 3 | /// 4 | /// Represents the event arguments for a serializable object. 5 | /// 6 | public class SerializableObjectEventArgs : EventArgs { 7 | /// 8 | /// Gets the value associated with the event. 9 | /// 10 | public T Value { get; } 11 | 12 | /// 13 | /// Initializes a new instance of the class with the specified value. 14 | /// 15 | /// The value associated with the event. 16 | public SerializableObjectEventArgs(T value) => Value = value; 17 | } -------------------------------------------------------------------------------- /src/Sharpify/Sharpify.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0;net8.0 4 | enable 5 | 2.5.0 6 | enable 7 | true 8 | David Shnayder 9 | David Shnayder 10 | MIT 11 | CHANGELOGLATEST.md 12 | True 13 | Sharpify 14 | A collection of high performance language extensions for C# 15 | https://github.com/dusrdev/Sharpify 16 | https://github.com/dusrdev/Sharpify 17 | git 18 | Extensions;HighPerformance;DiscriminatedUnion;Result 19 | true 20 | true 21 | false 22 | true 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Sharpify/Sharpify.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | Library -------------------------------------------------------------------------------- /src/Sharpify/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; // required for SearchValues 2 | using System.Globalization; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Sharpify; 6 | 7 | public static partial class Extensions { 8 | /// 9 | /// Gets a reference to the first character of the string. 10 | /// 11 | /// The string. 12 | /// A reference to the first character of the string. 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | public static ref char GetReference(this string text) { 15 | return ref Unsafe.AsRef(in text.GetPinnableReference()); 16 | } 17 | 18 | /// 19 | /// A simple wrapper over to make it easier to use. 20 | /// 21 | public static bool IsNullOrEmpty(this string str) => string.IsNullOrEmpty(str); 22 | 23 | /// 24 | /// A simple wrapper over to make it easier to use. 25 | /// 26 | public static bool IsNullOrWhiteSpace(this string str) => string.IsNullOrWhiteSpace(str); 27 | 28 | /// 29 | /// Tries to convert to an . 30 | /// 31 | /// The span of characters to convert. 32 | /// When this method returns, contains the converted if the conversion succeeded, or zero if the conversion failed. 33 | /// true if the conversion succeeded; otherwise, false. 34 | public static bool TryConvertToInt32(this ReadOnlySpan value, out int result) { 35 | result = 0; 36 | if (value.IsWhiteSpace() || value.Length > 11) { // 10 is the max length of an int32 + 1 for sign 37 | return false; 38 | } 39 | bool isNegative = value[0] is '-'; 40 | var length = value.Length; 41 | int i = 0; 42 | if (isNegative) { 43 | i++; 44 | } 45 | for (; (uint)i < (uint)length; i++) { 46 | var digit = value[i] - '0'; 47 | 48 | // Check for invalid digit 49 | if (digit is < 0 or > 9) { 50 | result = 0; 51 | return false; 52 | } 53 | 54 | unchecked { 55 | result = (result * 10) + digit; 56 | } 57 | } 58 | if (isNegative) { 59 | result *= -1; 60 | } 61 | return true; 62 | } 63 | 64 | /// 65 | /// A more convenient way to use 66 | /// 67 | /// 68 | /// 69 | /// 70 | /// The advantage of Concat over string interpolation diminishes when more than 2 strings are used. 71 | /// 72 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 73 | public static string Concat(this string value, ReadOnlySpan suffix) => string.Concat(value.AsSpan(), suffix); 74 | 75 | /// 76 | /// Method used to turn into Title format 77 | /// 78 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 79 | public static string ToTitle(this string str) => CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str); 80 | 81 | private const string BinaryChars = "01 \t\n\r"; 82 | private static readonly SearchValues BinarySearchValues = SearchValues.Create(BinaryChars); 83 | 84 | /// 85 | /// Checks if a string is a valid binary string (0,1,' ','\t','\n','\r') 86 | /// 87 | public static bool IsBinary(this string str) { 88 | return !str.AsSpan().ContainsAnyExcept(BinarySearchValues); 89 | } 90 | } -------------------------------------------------------------------------------- /src/Sharpify/ThreadSafe.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify; 2 | 3 | /// 4 | /// A wrapper around a value that makes it thread safe. 5 | /// 6 | public sealed class ThreadSafe : IEquatable, IEquatable> { 7 | //TODO: Switch to NET9 new Lock type 8 | #if NET9_0_OR_GREATER 9 | private readonly Lock _lock = new(); 10 | #elif NET8_0 11 | private readonly object _lock = new(); 12 | #endif 13 | private T _value; 14 | 15 | /// 16 | /// Creates a new instance of ThreadSafe with an initial value. 17 | /// 18 | public ThreadSafe(T value) { 19 | _value = value; 20 | } 21 | 22 | /// 23 | /// Creates a new instance of ThreadSafe with the default value of T. 24 | /// 25 | public ThreadSafe() : this(default!) { } 26 | 27 | /// 28 | /// A public getter and setter for the value. 29 | /// 30 | /// 31 | /// The inner operation are thread-safe, use this to change or access the value. 32 | /// 33 | public T Value { 34 | get { 35 | lock (_lock) { 36 | return _value; 37 | } 38 | } 39 | } 40 | 41 | /// 42 | /// Provides a thread-safe way to modify the value. 43 | /// 44 | /// The value after the modification 45 | public T Modify(Func modificationFunc) { 46 | lock (_lock) { 47 | _value = modificationFunc(_value); 48 | return _value; 49 | } 50 | } 51 | 52 | /// 53 | /// Checks if the value is equal to the other value. 54 | /// 55 | /// 56 | /// 57 | public bool Equals(ThreadSafe? other) => other is not null && Equals(other.Value); 58 | 59 | /// 60 | /// Checks if the value is equal to the other value. 61 | /// 62 | /// 63 | /// 64 | public bool Equals(T? other) { 65 | if (other is null) { 66 | return false; 67 | } 68 | lock (_lock) { 69 | return _value is not null && _value.Equals(other); 70 | } 71 | } 72 | 73 | /// 74 | /// Checks if the value is equal to the other value. 75 | /// 76 | /// 77 | /// 78 | public override bool Equals(object? obj) { 79 | return Equals(obj as ThreadSafe); 80 | } 81 | 82 | /// 83 | /// Gets the hash code of the value. 84 | /// 85 | /// 86 | public override int GetHashCode() => Value!.GetHashCode(); 87 | 88 | } -------------------------------------------------------------------------------- /src/Sharpify/UnmanangedExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Sharpify; 4 | 5 | public static partial class Extensions { 6 | /// 7 | /// Tries to parse an enum result from a string 8 | /// 9 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 10 | public static bool TryParseAsEnum( 11 | this string value, 12 | out TEnum result) where TEnum : struct, Enum { 13 | return Enum.TryParse(value, out result) && Enum.IsDefined(result); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Sharpify/UnsafeSpanAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Sharpify; 6 | 7 | /// 8 | /// Represents an unsafe span wrapper that can be used in async methods and be boxed to the heap. 9 | /// 10 | /// 11 | /// 12 | /// Only use it where you can guarantee the scope of the span, it is named "Unsafe" for a reason. 13 | /// 14 | public unsafe readonly struct UnsafeSpanIterator : IEnumerable 15 | { 16 | private readonly void* _pointer; 17 | 18 | /// 19 | /// The length of the span 20 | /// 21 | public readonly int Length; 22 | 23 | /// 24 | /// Creates a new instance of over the specified span. 25 | /// 26 | /// 27 | public UnsafeSpanIterator(ReadOnlySpan span) 28 | { 29 | _pointer = Unsafe.AsPointer(ref MemoryMarshal.GetReference(span)); 30 | Length = span.Length; 31 | } 32 | 33 | private UnsafeSpanIterator(void* start, int length) 34 | { 35 | _pointer = start; 36 | Length = length; 37 | } 38 | 39 | /// 40 | /// Returns a slice of the span 41 | /// 42 | /// 43 | /// 44 | /// 45 | public UnsafeSpanIterator Slice(int start, int length) 46 | { 47 | ArgumentOutOfRangeException.ThrowIfGreaterThan(start + length, Length); 48 | return new UnsafeSpanIterator(Unsafe.Add(_pointer, start), length); 49 | } 50 | 51 | /// 52 | /// Returns the element at the given index 53 | /// 54 | /// 55 | /// 56 | public ref readonly T this[int index] 57 | { 58 | get 59 | { 60 | ArgumentOutOfRangeException.ThrowIfGreaterThan(index, Length); 61 | void* item = Unsafe.Add(_pointer, index); 62 | return ref Unsafe.AsRef(item); 63 | } 64 | } 65 | 66 | /// 67 | /// Generates an IEnumerable of the elements in the span 68 | /// 69 | /// 70 | public IEnumerable ToEnumerable() 71 | { 72 | for (var i = 0; i < Length; i++) 73 | { 74 | yield return this[i]; 75 | } 76 | } 77 | 78 | /// 79 | /// Gets the enumerator for the span 80 | /// 81 | /// 82 | public IEnumerator GetEnumerator() => new UnsafeSpanIteratorEnumerator(this); 83 | 84 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 85 | 86 | internal struct UnsafeSpanIteratorEnumerator : IEnumerator 87 | { 88 | private readonly UnsafeSpanIterator _source; 89 | private int _index; 90 | private T? _current; 91 | 92 | internal UnsafeSpanIteratorEnumerator(UnsafeSpanIterator source) 93 | { 94 | _source = source; 95 | _index = 0; 96 | _current = default; 97 | } 98 | 99 | public void Dispose() {} 100 | 101 | public bool MoveNext() 102 | { 103 | UnsafeSpanIterator local = _source; 104 | 105 | if ((uint)_index < (uint)local.Length) 106 | { 107 | _current = local[_index]; 108 | _index++; 109 | return true; 110 | } 111 | return MoveNextRare(); 112 | } 113 | 114 | private bool MoveNextRare() 115 | { 116 | _index = _source.Length + 1; 117 | _current = default; 118 | return false; 119 | } 120 | 121 | public readonly T Current => _current!; 122 | 123 | readonly object? IEnumerator.Current 124 | { 125 | get 126 | { 127 | if ((uint)_index >= _source.Length + 1) 128 | { 129 | throw new InvalidOperationException("The enumerator has not been started or has already finished."); 130 | } 131 | return Current; 132 | } 133 | } 134 | 135 | void IEnumerator.Reset() 136 | { 137 | _index = 0; 138 | _current = default; 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /src/Sharpify/UtilsEnv.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Runtime.InteropServices; 3 | using System.Runtime.Versioning; 4 | using System.Security.Principal; 5 | 6 | namespace Sharpify; 7 | 8 | public static partial class Utils { 9 | /// 10 | /// Provides utility methods for 11 | /// 12 | public static class Env { 13 | /// 14 | /// Checks if the application is running on Windows. 15 | /// 16 | public static bool IsRunningOnWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 17 | 18 | /// 19 | /// Checks if the application is running with administrator privileges. 20 | /// 21 | /// 22 | /// On platforms other than Windows, it returns automatically. 23 | /// 24 | public static bool IsRunningAsAdmin() { 25 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { 26 | return false; 27 | } 28 | 29 | using var identity = WindowsIdentity.GetCurrent(); 30 | var principal = new WindowsPrincipal(identity); 31 | return principal.IsInRole(WindowsBuiltInRole.Administrator); 32 | } 33 | 34 | /// 35 | /// Returns the base directory of the application. 36 | /// 37 | /// 38 | /// This is tested and works on Windows 39 | /// This is not tested on Linux and Mac but should work 40 | /// Do not use in .NET Maui, it has a special api for this. 41 | /// 42 | public static string GetBaseDirectory() => AppDomain.CurrentDomain.BaseDirectory; 43 | 44 | /// 45 | /// Combines the base directory path with the specified filename. 46 | /// 47 | /// The name of the file. 48 | /// The combined path. 49 | public static string PathInBaseDirectory(ReadOnlySpan filename) => Path.Join(GetBaseDirectory(), filename); 50 | 51 | /// 52 | /// Checks whether Internet connection is available 53 | /// 54 | public static bool IsInternetAvailable => System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable(); 55 | 56 | /// 57 | /// Opens the specified URL in the default web browser based on the operating system. 58 | /// 59 | /// The URL to open. 60 | /// 61 | /// Currently only Windows, Linux and Mac are supported. 62 | /// 63 | [SupportedOSPlatform("Windows")] 64 | [SupportedOSPlatform("Linux")] 65 | [SupportedOSPlatform("MacOS")] 66 | public static void OpenLink(string url) { 67 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { 68 | var processInfo = new ProcessStartInfo { 69 | FileName = url, 70 | UseShellExecute = true 71 | }; 72 | using var process = Process.Start(processInfo); 73 | return; 74 | } 75 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { 76 | using var process = Process.Start("x-www-browser", url); 77 | return; 78 | } 79 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { 80 | using var process = Process.Start("open", url); 81 | return; 82 | } 83 | throw new PlatformNotSupportedException(); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/Sharpify/UtilsHeader.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify; 2 | 3 | /// 4 | /// Provides utility methods that are not extensions 5 | /// 6 | public static partial class Utils {} -------------------------------------------------------------------------------- /src/Sharpify/UtilsMathematics.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify; 2 | 3 | public static partial class Utils { 4 | /// 5 | /// Provides utility methods for 6 | /// 7 | public static class Mathematics { 8 | /// 9 | /// Returns a rolling average 10 | /// 11 | /// The previous average value 12 | /// The new statistic 13 | /// The number of total samples, previous + 1 14 | /// 15 | /// If the is less or equal to 0, the is returned. 16 | /// A message will be displayed during debug if that happens. 17 | /// An exception will not be thrown at runtime to increase performance. 18 | /// 19 | public static double RollingAverage(double oldAverage, double newNumber, int sampleCount) { 20 | ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(sampleCount, 0); 21 | if (sampleCount is 1) { 22 | return newNumber; 23 | } 24 | double denominator = 1d / sampleCount; 25 | return ((oldAverage * (sampleCount - 1)) + newNumber) * denominator; 26 | } 27 | 28 | /// 29 | /// Returns the factorial result of 30 | /// 31 | /// 32 | /// 33 | /// If the is less or equal to 0, is returned. 34 | /// A message will be displayed during debug if that happens. 35 | /// An exception will not be thrown at runtime to increase performance. 36 | /// 37 | public static double Factorial(double n) { 38 | ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(n, 0); 39 | if (n is <= 2) { 40 | return n; 41 | } 42 | var num = 1d; 43 | for (; n > 1; n--) { 44 | num *= n; 45 | } 46 | return num; 47 | } 48 | 49 | /// 50 | /// Returns an estimate of the -th number in the Fibonacci sequence 51 | /// 52 | public static double FibonacciApproximation(int n) { 53 | var sqrt5 = Math.Sqrt(5); 54 | var numerator = Math.Pow(1 + sqrt5, n) - Math.Pow(1 - sqrt5, n); 55 | var denominator = Math.ScaleB(sqrt5, n); 56 | return numerator / denominator; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/Sharpify/UtilsStrings.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | using Sharpify.Collections; 4 | 5 | namespace Sharpify; 6 | 7 | public static partial class Utils { 8 | /// 9 | /// Provides utility methods for 10 | /// 11 | public static class Strings { 12 | private static ReadOnlySpan FileSizeSuffix => new string[] { "B", "KB", "MB", "GB", "TB", "PB" }; 13 | 14 | /// 15 | /// The required length of the buffer to format bytes 16 | /// 17 | /// 18 | /// This is required to handle , usual cases are much smaller, and this internal uses are pooled. 19 | /// 20 | public const int FormatBytesRequiredLength = 512; 21 | private const double FormatBytesKb = 1024d; 22 | private const double FormatBytesDivisor = 1 / FormatBytesKb; 23 | 24 | /// 25 | /// Formats bytes to friendlier strings, i.e: B,KB,MB,TB,PB... 26 | /// 27 | /// string 28 | public static string FormatBytes(long bytes) 29 | => FormatBytes((double)bytes); 30 | 31 | /// 32 | /// Formats bytes to friendlier strings, i.e: B,KB,MB,TB,PB... 33 | /// 34 | /// string 35 | public static string FormatBytes(double bytes) { 36 | using var owner = MemoryPool.Shared.Rent(FormatBytesRequiredLength); 37 | return new string(FormatBytes(bytes, owner.Memory.Span)); 38 | } 39 | 40 | /// 41 | /// Formats bytes to friendlier strings, i.e: B,KB,MB,TB,PB... into the buffer and returns the written span 42 | /// 43 | /// 44 | /// Ensure capacity >= 45 | /// 46 | /// string 47 | public static ReadOnlySpan FormatBytes(double bytes, Span buffer) { 48 | var suffix = 0; 49 | while (suffix < FileSizeSuffix.Length - 1 && bytes >= FormatBytesKb) { 50 | bytes *= FormatBytesDivisor; 51 | suffix++; 52 | } 53 | return StringBuffer.Create(buffer) 54 | .Append(bytes, "#,##0.##") 55 | .Append(' ') 56 | .Append(FileSizeSuffix[suffix]) 57 | .Allocate(); 58 | } 59 | 60 | /// 61 | /// Formats bytes to friendlier strings, i.e: B,KB,MB,TB,PB... into the buffer and returns the written span 62 | /// 63 | /// 64 | /// Ensure capacity >= 65 | /// 66 | /// string 67 | public static ReadOnlySpan FormatBytes(long bytes, Span buffer) 68 | => FormatBytes((double)bytes, buffer); 69 | } 70 | } -------------------------------------------------------------------------------- /src/Sharpify/UtilsUnsafe.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | using @unsafe = System.Runtime.CompilerServices.Unsafe; 4 | 5 | namespace Sharpify; 6 | 7 | public static partial class Utils { 8 | /// 9 | /// Provides utility unsafe utility methods for utilization of other high performance apis 10 | /// 11 | public static class Unsafe { 12 | /// 13 | /// Creates an integer predicate from a given predicate function. 14 | /// 15 | /// The type of the input parameter. 16 | /// The predicate function. 17 | /// An integer predicate. 1 if the original predicate would've return true, otherwise 0 18 | /// 19 | /// This allows usage of a predicate to count elements that match a given condition, using hardware intrinsics to speed up the process. 20 | /// The integer return value allows to use this converted function with IEnumerable{T}.Sum which is a hardware accelerated method, but the result will be identical to calling Count(predicate). 21 | /// 22 | public static Func CreateIntegerPredicate(Func predicate) => 23 | @unsafe.As, Func>(ref predicate); 24 | 25 | /// 26 | /// Converts a read-only span to a mutable span. 27 | /// 28 | /// The type of elements in the span. 29 | /// The read-only span to convert. 30 | /// A mutable span. 31 | public static unsafe Span AsMutableSpan(ReadOnlySpan span) { 32 | ref var p = ref MemoryMarshal.GetReference(span); 33 | void* pointer = @unsafe.AsPointer(ref p); 34 | return new Span(pointer, span.Length); 35 | } 36 | 37 | /// 38 | /// Attempts to unbox an object to a specified value type. 39 | /// 40 | /// The value type to unbox to. 41 | /// The object to unbox. 42 | /// When this method returns, contains the unboxed value if the unboxing is successful; otherwise, the default value of . 43 | /// true if the unboxing is successful; otherwise, false. 44 | /// Copied from CommunityToolkit.HighPerformance 45 | public static bool TryUnbox(object obj, out T value) where T : struct { 46 | if (obj.GetType() == typeof(T)) { 47 | value = @unsafe.Unbox(obj); 48 | return true; 49 | } 50 | 51 | value = default; 52 | return false; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /tests/Sharpify.CommandLineInterface.Tests/AddCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface.Tests; 2 | 3 | public sealed class AddCommand : Command { 4 | public override string Name => "add"; 5 | 6 | public override string Description => "Adds 2 numbers."; 7 | 8 | public override string Usage => "add "; 9 | 10 | public override ValueTask ExecuteAsync(Arguments args) { 11 | if (!args.TryGetValue(0, 0, out var number1)) { 12 | return OutputHelper.Return(" not specified", 404, true); 13 | } 14 | if (!args.TryGetValue(1, 0, out var number2)) { 15 | return OutputHelper.Return(" not specified", 404, true); 16 | } 17 | return OutputHelper.Return($"{number1} + {number2} = {number1 + number2}", 0); 18 | } 19 | } 20 | 21 | public sealed class SynchronousAddCommand : SynchronousCommand { 22 | public override string Name => "sadd"; 23 | 24 | public override string Description => "Adds 2 numbers."; 25 | 26 | public override string Usage => "sadd "; 27 | 28 | public override int Execute(Arguments args) { 29 | if (!args.TryGetValue(0, 0, out var number1)) { 30 | Console.WriteLine(" not specified"); 31 | return 404; 32 | } 33 | if (!args.TryGetValue(1, 0, out var number2)) { 34 | Console.WriteLine(" not specified"); 35 | return 404; 36 | } 37 | Console.WriteLine($"{number1} + {number2} = {number1 + number2}"); 38 | return 0; 39 | } 40 | } -------------------------------------------------------------------------------- /tests/Sharpify.CommandLineInterface.Tests/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: CollectionBehavior(DisableTestParallelization = true)] -------------------------------------------------------------------------------- /tests/Sharpify.CommandLineInterface.Tests/EchoCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface.Tests; 2 | 3 | public sealed class EchoCommand : Command { 4 | public override string Name => "echo"; 5 | 6 | public override string Description => "Echoes the specified message."; 7 | 8 | public override string Usage => "echo "; 9 | 10 | public override ValueTask ExecuteAsync(Arguments args) { 11 | if (!args.TryGetValue("message", out string message)) { 12 | return OutputHelper.Return("No message specified", 404, true); 13 | } 14 | return OutputHelper.Return(message, 0); 15 | } 16 | } -------------------------------------------------------------------------------- /tests/Sharpify.CommandLineInterface.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using Sharpify.CommandLineInterface; -------------------------------------------------------------------------------- /tests/Sharpify.CommandLineInterface.Tests/Helper.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.CommandLineInterface.Tests; 2 | 3 | public static class Helper { 4 | public static Dictionary GetMapped(params (string, string)[] parameters) { 5 | var dict = new Dictionary(StringComparer.CurrentCultureIgnoreCase); 6 | foreach (var (key, value) in parameters) { 7 | dict[key] = value; 8 | } 9 | return dict; 10 | } 11 | } -------------------------------------------------------------------------------- /tests/Sharpify.CommandLineInterface.Tests/Sharpify.CommandLineInterface.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | enable 6 | enable 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | all 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/Sharpify.CommandLineInterface.Tests/SingleCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Sharpify.CommandLineInterface.Tests; 4 | 5 | public sealed class SingleCommand : Command { 6 | public override string Name => ""; 7 | 8 | public override string Description => "Echoes the specified message."; 9 | 10 | public override string Usage => "Single "; 11 | 12 | public override ValueTask ExecuteAsync(Arguments args) { 13 | if (!args.TryGetValue("message", out string message)) { 14 | return OutputHelper.Return("No message specified", 404, true); 15 | } 16 | return OutputHelper.Return(message, 0); 17 | } 18 | } 19 | 20 | public sealed class SingleCommandNoParams : SynchronousCommand { 21 | public override string Name => ""; 22 | 23 | public override string Description => "Changes the inner boxed value to true."; 24 | 25 | public override string Usage => ""; 26 | 27 | private readonly StrongBox _value; 28 | 29 | public SingleCommandNoParams(StrongBox value) { 30 | _value = value; 31 | } 32 | 33 | public override int Execute(Arguments args) { 34 | _value.Value = true; 35 | return 0; 36 | } 37 | } -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: CollectionBehavior(DisableTestParallelization = true)] -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/Color.cs: -------------------------------------------------------------------------------- 1 | using MemoryPack; 2 | 3 | namespace Sharpify.Data.Tests; 4 | 5 | public record Color { 6 | public string Name { get; set; } = ""; 7 | public byte Red { get; set; } 8 | public byte Green { get; set; } 9 | public byte Blue { get; set; } 10 | } -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/Dog.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | using MemoryPack; 4 | 5 | namespace Sharpify.Data.Tests; 6 | 7 | [MemoryPackable] 8 | public readonly partial record struct Dog(string Name, int Age); -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/FactoryResult.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Data.Tests; 2 | 3 | public record FactoryResult(string Path, T Database) : IDisposable where T : IDisposable { 4 | public void Dispose() => Database.Dispose(); 5 | } -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/HelperTests.cs: -------------------------------------------------------------------------------- 1 | using Bogus; 2 | 3 | using MemoryPack; 4 | 5 | using Sharpify.Collections; 6 | 7 | namespace Sharpify.Data.Tests; 8 | 9 | public class HelperTests { 10 | [Theory] 11 | [InlineData(new byte[] { 1, 2, 3, 4, 5, 6 }, 6)] 12 | [InlineData(new[] { 1, 2, 3, 4, 5, 6 }, 6)] 13 | [InlineData(new double[] { 1, 2, 3, 4, 5, 6 }, 6)] 14 | [InlineData(new[] { "1", "2", "3", "4", "5", "6" }, 6)] 15 | public void GetRequiredLength_Unmanaged(T[] data, int expectedLength) { 16 | var serialized = MemoryPackSerializer.Serialize(data); 17 | var requiredLength = Helper.GetRequiredLength(serialized); 18 | Assert.Equal(expectedLength, requiredLength); 19 | } 20 | 21 | [Fact] 22 | public void GetRequiredLength_Person() { 23 | var faker = new Faker(); 24 | var data = Enumerable.Range(1, faker.Random.Int(10, 100)).Select(_ => new Person(faker.Name.FullName(), faker.Random.Int(1, 100))).ToArray(); 25 | var serialized = MemoryPackSerializer.Serialize(data); 26 | var requiredLength = Helper.GetRequiredLength(serialized); 27 | Assert.Equal(data.Length, requiredLength); 28 | } 29 | 30 | [Theory] 31 | [InlineData(new byte[] { 1, 2, 3, 4, 5, 6 }, 6)] 32 | [InlineData(new[] { 1, 2, 3, 4, 5, 6 }, 6)] 33 | [InlineData(new double[] { 1, 2, 3, 4, 5, 6 }, 6)] 34 | [InlineData(new[] { "1", "2", "3", "4", "5", "6" }, 6)] 35 | public void ReadToRentedBufferWriter_Unmanaged(T[] data, int expectedLength) { 36 | var serialized = MemoryPackSerializer.Serialize(data); 37 | var requiredLength = Helper.GetRequiredLength(serialized); 38 | var buffer = new RentedBufferWriter(requiredLength + 5); 39 | try { 40 | Helper.ReadToRenterBufferWriter(ref buffer, serialized, requiredLength); 41 | Assert.Equal(expectedLength, buffer.Position); 42 | } finally { 43 | buffer?.Dispose(); 44 | } 45 | } 46 | 47 | [Fact] 48 | public void ReadToRentedBufferWriter_Person() { 49 | var faker = new Faker(); 50 | var data = Enumerable.Range(1, faker.Random.Int(10, 100)).Select(_ => new Person(faker.Name.FullName(), faker.Random.Int(1, 100))).ToArray(); 51 | var serialized = MemoryPackSerializer.Serialize(data); 52 | var requiredLength = Helper.GetRequiredLength(serialized); 53 | var buffer = new RentedBufferWriter(requiredLength + 5); 54 | try { 55 | Helper.ReadToRenterBufferWriter(ref buffer, serialized, requiredLength); 56 | Assert.Equal(requiredLength, buffer.Position); 57 | } finally { 58 | buffer?.Dispose(); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/JsonContext.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Sharpify.Data.Tests; 4 | 5 | [JsonSourceGenerationOptions(WriteIndented = true)] 6 | [JsonSerializable(typeof(Color))] 7 | public partial class JsonContext : JsonSerializerContext { } -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/Person.cs: -------------------------------------------------------------------------------- 1 | using MemoryPack; 2 | 3 | namespace Sharpify.Data.Tests; 4 | 5 | [MemoryPackable] 6 | public readonly partial record struct Person(string Name, int Age); -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/Sharpify.Data.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | enable 6 | latest 7 | enable 8 | 9 | false 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/Sharpify.Data.Tests/User.cs: -------------------------------------------------------------------------------- 1 | 2 | using MemoryPack; 3 | 4 | namespace Sharpify.Data.Tests; 5 | 6 | public sealed class User : IFilterable { 7 | public Person Person { get; init; } 8 | 9 | public User(Person person) { 10 | Person = person; 11 | } 12 | 13 | public static User Deserialize(ReadOnlySpan data) { 14 | var p = MemoryPackSerializer.Deserialize(data); 15 | return new(p); 16 | } 17 | 18 | public static User[]? DeserializeMany(ReadOnlySpan data) { 19 | var persons = MemoryPackSerializer.Deserialize(data); 20 | return persons!.Select(p => new User(p)).ToArray(); 21 | } 22 | 23 | public static byte[]? Serialize(User? value) { 24 | if (value is null) { 25 | return null; 26 | } 27 | return MemoryPackSerializer.Serialize(value.Person); 28 | } 29 | 30 | public static byte[]? SerializeMany(User[]? values) { 31 | if (values is null) { 32 | return null; 33 | } 34 | return MemoryPackSerializer.Serialize(values.Select(v => v.Person).ToArray()); 35 | } 36 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: CollectionBehavior(DisableTestParallelization = true)] -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Collections/BufferWrapperTests.cs: -------------------------------------------------------------------------------- 1 | using Sharpify.Collections; 2 | 3 | namespace Sharpify.Tests.Collections; 4 | 5 | #if NET9_0_OR_GREATER 6 | public class BufferWrapperTests { 7 | [Fact] 8 | public void BufferWrapper_NoCapacity_Throws() { 9 | // Arrange 10 | Action act = () => { 11 | var buffer = new BufferWrapper(); 12 | buffer.Append('a'); 13 | }; 14 | 15 | // Act & Assert 16 | Assert.Throws(act); 17 | } 18 | 19 | [Fact] 20 | public void BufferWrapper_Append_ToFullCapacity() { 21 | // Arrange 22 | const string text = "Hello world!"; 23 | 24 | // Act 25 | Action act = () => { 26 | var buffer = BufferWrapper.Create(new char[text.Length]); 27 | buffer.Append(text); 28 | }; 29 | 30 | // Assert 31 | act(); 32 | } 33 | 34 | [Fact] 35 | public void BufferWrapper_Append_BeyondCapacity() { 36 | // Arrange 37 | const string text = "Hello world!"; 38 | 39 | // Act 40 | Action act = () => { 41 | var buffer = BufferWrapper.Create(new char[text.Length - 1]); 42 | buffer.Append(text); 43 | }; 44 | 45 | // Assert 46 | Assert.Throws(act); 47 | } 48 | 49 | [Fact] 50 | public void BufferWrapper_Reset() { 51 | // Arrange 52 | var buffer = BufferWrapper.Create(new char[20]); 53 | 54 | // Act 55 | buffer.Append("Hello world!"); 56 | buffer.Reset(); 57 | buffer.Append("David"); 58 | 59 | // Assert 60 | Assert.Equal("David", buffer.WrittenSpan); 61 | } 62 | } 63 | #endif -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Collections/LazyLocalPersistentDictionaryTests.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | using Sharpify.Collections; 4 | 5 | namespace Sharpify.Tests.Collections; 6 | 7 | public class LazyLocalPersistentDictionaryTests { 8 | [Fact] 9 | public void LazyLocalPersistentDictionary_ReadKey_Null_WhenDoesNotExist() { 10 | // Arrange 11 | var path = Utils.Env.PathInBaseDirectory("lpdict.json"); 12 | if (File.Exists(path)) { 13 | File.Delete(path); 14 | } 15 | var dict = new LazyLocalPersistentDictionary(path); 16 | 17 | // Act 18 | var result = dict["test"]; 19 | 20 | // Assert 21 | Assert.Null(result); 22 | } 23 | 24 | [Fact] 25 | public async Task LazyLocalPersistentDictionary_ReadKey_Valid_WhenExists() { 26 | // Arrange 27 | var path = Utils.Env.PathInBaseDirectory("lpdict.json"); 28 | if (File.Exists(path)) { 29 | File.Delete(path); 30 | } 31 | var dict = new LazyLocalPersistentDictionary(path); 32 | 33 | var testJson = new { 34 | Name = "test", 35 | Age = 21 36 | }; 37 | 38 | // Act 39 | await dict.UpsertAsync("one", JsonSerializer.Serialize(testJson)); 40 | await dict.UpsertAsync("two", "2"); 41 | 42 | // Assert 43 | Assert.Equal("2", dict["two"]); 44 | } 45 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Collections/LocalPersistentDictionaryTests.cs: -------------------------------------------------------------------------------- 1 | using Sharpify.Collections; 2 | 3 | using Xunit.Abstractions; 4 | 5 | namespace Sharpify.Tests.Collections; 6 | 7 | public class LocalPersistentDictionaryTests { 8 | private readonly ITestOutputHelper _testOutputHelper; 9 | 10 | public LocalPersistentDictionaryTests(ITestOutputHelper testOutputHelper) { 11 | _testOutputHelper = testOutputHelper; 12 | } 13 | 14 | [Fact] 15 | public void LocalPersistentDictionary_ReadKey_Null_WhenDoesNotExist() { 16 | // Arrange 17 | var path = Utils.Env.PathInBaseDirectory("pdict.json"); 18 | if (File.Exists(path)) { 19 | File.Delete(path); 20 | } 21 | var dict = new TestLocalPersistentDictionary(path); 22 | 23 | // Act 24 | var result = dict["test"]; 25 | 26 | // Assert 27 | Assert.Null(result); 28 | } 29 | 30 | [Fact] 31 | public async Task LocalPersistentDictionary_ReadKey_Valid_WhenExists() { 32 | // Arrange 33 | var path = Utils.Env.PathInBaseDirectory("pdict.json"); 34 | if (File.Exists(path)) { 35 | File.Delete(path); 36 | } 37 | var dict = new TestLocalPersistentDictionary(path); 38 | 39 | // Act 40 | await dict.UpsertAsync("one", "1"); 41 | 42 | // Assert 43 | Assert.Equal("1", dict["one"]); 44 | } 45 | 46 | [Fact] 47 | public async Task LocalPersistentDictionary_GetOrCreate() { 48 | // Arrange 49 | var path = Utils.Env.PathInBaseDirectory("pdict.json"); 50 | if (File.Exists(path)) { 51 | File.Delete(path); 52 | } 53 | var dict = new TestLocalPersistentDictionary(path); 54 | 55 | // Act 56 | var result = await dict.GetOrCreateAsync("one", "1"); 57 | var check = dict["one"] is "1"; 58 | 59 | // Assert 60 | Assert.Equal("1", result); 61 | Assert.True(check); 62 | } 63 | 64 | [Fact] 65 | public async Task LocalPersistentDictionary_Upsert_Concurrent() { 66 | // Arrange 67 | var filename = Random.Shared.Next(999, 10000).ToString(); 68 | var path = Utils.Env.PathInBaseDirectory($"{filename}.json"); 69 | if (File.Exists(path)) { 70 | File.Delete(path); 71 | } 72 | var dict = new TestLocalPersistentDictionary(path); 73 | 74 | // Act 75 | Task[] upsertTasks = [ 76 | Task.Run(async () => await dict.UpsertAsync("one", "1")), 77 | Task.Run(async () => await dict.UpsertAsync("two", "2")), 78 | Task.Run(async () => await dict.UpsertAsync("three", "3")), 79 | Task.Run(async () => await dict.UpsertAsync("four", "4")), 80 | Task.Run(async () => await dict.UpsertAsync("five", "5")), 81 | ]; 82 | await Task.WhenAll(upsertTasks); 83 | 84 | // Assert 85 | // dict.SerializedCount.Should().BeLessThanOrEqualTo(upsertTasks.Length); 86 | _testOutputHelper.WriteLine($"PersistentDictionary serialized count: {dict.SerializedCount}"); 87 | // This is checking that the dictionary was serialized less than the number of upserts. 88 | // Ideally with perfectly concurrent updates, the dictionary would only be serialized once. 89 | // The reason not to check for 1 is that the tasks may not be executed perfectly in parallel. 90 | var sdict = new LocalPersistentDictionary(path); 91 | Assert.Equal(upsertTasks.Length, sdict.Count); 92 | File.Delete(path); 93 | } 94 | 95 | [Fact] 96 | public async Task LocalPersistentDictionary_Upsert_Sequential_NoItemsMissing() { 97 | // Arrange 98 | var filename = Random.Shared.Next(999, 10000).ToString(); 99 | var path = Utils.Env.PathInBaseDirectory($"{filename}.json"); 100 | if (File.Exists(path)) { 101 | File.Delete(path); 102 | } 103 | var dict = new TestLocalPersistentDictionary(path); 104 | 105 | // Act 106 | await dict.UpsertAsync("one", "1"); 107 | await dict.UpsertAsync("two", "2"); 108 | await dict.UpsertAsync("three", "3"); 109 | await dict.UpsertAsync("four", "4"); 110 | await dict.UpsertAsync("five", "5"); 111 | 112 | // Assert 113 | var sdict = new LocalPersistentDictionary(path); 114 | Assert.Equal(5, sdict.Count); 115 | File.Delete(path); 116 | } 117 | 118 | [Fact] 119 | public async Task LocalPersistentDictionary_GenericGetAndUpsert() { 120 | // Arrange 121 | var filename = Random.Shared.Next(999, 10000).ToString(); 122 | var path = Utils.Env.PathInBaseDirectory($"{filename}.json"); 123 | if (File.Exists(path)) { 124 | File.Delete(path); 125 | } 126 | var dict = new TestLocalPersistentDictionary(path); 127 | 128 | // Act 129 | await dict.UpsertAsync("one", 1); 130 | await dict.UpsertAsync("two", 2); 131 | var sdict = new LocalPersistentDictionary(path); 132 | int one = await sdict.GetOrCreateAsync("one", 0); 133 | int two = await sdict.GetOrCreateAsync("two", 0); 134 | 135 | // Assert 136 | Assert.Equal(1, one); 137 | Assert.Equal(2, two); 138 | File.Delete(path); 139 | } 140 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Collections/RentedBufferWriterTests.cs: -------------------------------------------------------------------------------- 1 | using Sharpify.Collections; 2 | 3 | namespace Sharpify.Tests.Collections; 4 | 5 | public class RentedBufferWriterTests { 6 | [Fact] 7 | public void RentedBufferWriter_InvalidCapacity_Throws() { 8 | // Arrange 9 | Action act = () => { 10 | using var buffer = new RentedBufferWriter(-1); 11 | }; 12 | 13 | // Act & Assert 14 | Assert.Throws(act); 15 | } 16 | 17 | [Fact] 18 | public void RentedBufferWriter_Capacity0IsDisabled() { 19 | // Arrange 20 | using var buffer = new RentedBufferWriter(0); 21 | 22 | // Assert 23 | Assert.True(buffer.IsDisabled); 24 | } 25 | 26 | [Fact] 27 | public void RentedBufferWriter_WriteToSpan() { 28 | // Arrange 29 | using var buffer = new RentedBufferWriter(20); 30 | 31 | // Act 32 | var span = buffer.GetSpan(); 33 | "Hello".AsSpan().CopyTo(span); 34 | buffer.Advance(5); 35 | 36 | // Assert 37 | Assert.Equal("Hello", buffer.WrittenSpan); 38 | } 39 | 40 | [Fact] 41 | public void RentedBufferWriter_WriteAndAdvance() { 42 | // Arrange 43 | using var buffer = new RentedBufferWriter(20); 44 | 45 | // Act 46 | buffer.WriteAndAdvance("Hello"); 47 | 48 | // Assert 49 | Assert.Equal("Hello", buffer.WrittenSpan); 50 | } 51 | 52 | [Fact] 53 | public void RentedBufferWriter_UseRefToWriteValue() { 54 | // Arrange 55 | using var buffer = new RentedBufferWriter(20); 56 | 57 | // Act 58 | ref var arr = ref buffer.GetReferenceUnsafe(); 59 | var length = WriteOnes(ref arr, 5); 60 | buffer.Advance(length); 61 | 62 | // Assert 63 | Assert.Equal([1, 1, 1, 1, 1], buffer.WrittenSpan); 64 | 65 | static int WriteOnes(ref int[] buffer, int length) { 66 | for (var i = 0; i < length; i++) { 67 | buffer[i] = 1; 68 | } 69 | 70 | return length; 71 | } 72 | } 73 | 74 | [Fact] 75 | public void RentedBufferWriter_GetSpanSlice() { 76 | // Arrange 77 | using var buffer = new RentedBufferWriter(20); 78 | 79 | // Act 80 | var span = buffer.GetSpan(); 81 | "Hello".AsSpan().CopyTo(span); 82 | buffer.Advance(5); 83 | 84 | // Assert 85 | Assert.Equal("Hel", buffer.GetSpanSlice(0, 3)); 86 | } 87 | 88 | [Fact] 89 | public void RentedBufferWriter_WriteToMemory() { 90 | // Arrange 91 | using var buffer = new RentedBufferWriter(20); 92 | 93 | // Act 94 | var mem = buffer.GetMemory(); 95 | "Hello".AsSpan().CopyTo(mem.Span); 96 | buffer.Advance(5); 97 | 98 | // Assert 99 | Assert.Equal("Hello".ToCharArray(), buffer.WrittenSegment); 100 | } 101 | 102 | [Fact] 103 | public void RentedBufferWriter_GetMemorySlice() { 104 | // Arrange 105 | using var buffer = new RentedBufferWriter(20); 106 | 107 | // Act 108 | var mem = buffer.GetMemory(); 109 | "Hello".AsSpan().CopyTo(mem.Span); 110 | buffer.Advance(5); 111 | 112 | // Assert 113 | Assert.Equal("Hello", buffer.GetMemorySlice(0, 5).Span); 114 | } 115 | 116 | [Fact] 117 | public void RentedBufferWriter_WrittenSegment() { 118 | // Arrange 119 | using var buffer = new RentedBufferWriter(20); 120 | 121 | // Act 122 | var span = buffer.GetSpan(); 123 | "Hello".AsSpan().CopyTo(span); 124 | buffer.Advance(5); 125 | 126 | // Assert 127 | Assert.Equal("Hello".ToCharArray(), buffer.WrittenSegment); 128 | } 129 | 130 | [Fact] 131 | public void RentedBufferWriter_Reset() { 132 | // Arrange 133 | using var buffer = new RentedBufferWriter(20); 134 | 135 | // Act 136 | var span = buffer.GetSpan(); 137 | "Hello".AsSpan().CopyTo(span); 138 | buffer.Advance(5); 139 | buffer.Reset(); 140 | 141 | // Assert 142 | Assert.Equal(ReadOnlySpan.Empty, buffer.WrittenSpan); 143 | } 144 | 145 | [Fact] 146 | public void RentedBufferWriter_ActualCapacity() { 147 | // Arrange 148 | using var buffer = new RentedBufferWriter(20); 149 | 150 | // Assert 151 | Assert.True(buffer.ActualCapacity >= 20); 152 | } 153 | 154 | [Fact] 155 | public void RentedBufferWriter_FreeCapacity() { 156 | // Arrange 157 | using var buffer = new RentedBufferWriter(20); 158 | 159 | // Act 160 | var span = buffer.GetSpan(); 161 | "Hello".AsSpan().CopyTo(span); 162 | buffer.Advance(5); 163 | 164 | // Assert 165 | Assert.True(buffer.FreeCapacity >= 15); 166 | } 167 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Collections/SortedListTests.cs: -------------------------------------------------------------------------------- 1 | using Sharpify.Collections; 2 | 3 | namespace Sharpify.Tests.Collections; 4 | 5 | public class SortedListTests { 6 | [Fact] 7 | public void SortedList_Add() { 8 | // Arrange 9 | var list = new SortedList([1, 2, 3, 4, 5]); 10 | 11 | // Act 12 | list.Add(6); 13 | 14 | // Assert 15 | Assert.Equal(list.Count - 1, list.GetIndex(6)); 16 | 17 | // Act 18 | int count = list.Count; 19 | list.Add(3); 20 | 21 | // Assert 22 | // Duplicates should be ignored, no change to count 23 | Assert.Equal(count, list.Count); 24 | } 25 | 26 | [Fact] 27 | public void SortedList_AddRange_Span() { 28 | // Arrange 29 | var list = new SortedList([1, 2, 3, 4, 5]); 30 | 31 | // Act 32 | list.AddRange(new ReadOnlySpan([6, 7, 8])); 33 | 34 | // Assert 35 | Assert.Equal([1, 2, 3, 4, 5, 6, 7, 8], list.Span); 36 | } 37 | 38 | [Fact] 39 | public void SortedList_AddRange_IEnumerable() { 40 | // Arrange 41 | var list = new SortedList([1, 2, 3, 4, 5]); 42 | 43 | // Act 44 | list.AddRange(new List() { 6, 7, 8 }); 45 | 46 | // Assert 47 | Assert.Equal([1, 2, 3, 4, 5, 6, 7, 8], list.Span); 48 | } 49 | 50 | [Fact] 51 | public void SortedList_Remove() { 52 | // Arrange 53 | var list = new SortedList([1, 2, 3, 4, 5], null, true); 54 | 55 | // Act 56 | list.Remove(3); 57 | 58 | // Assert 59 | Assert.True(list.GetIndex(3) < 0); 60 | 61 | // Act 62 | for (int i = 0; i < 5; i++) { 63 | list.Add(6); 64 | } 65 | Assert.Equal(5 - 1 + 5, list.Count); 66 | list.Remove(6); 67 | 68 | // Assert 69 | Assert.True(list.GetIndex(6) < 0); 70 | } 71 | 72 | [Fact] 73 | public void SortedList_GetIndex_Existing() { 74 | // Arrange 75 | var list = new SortedList([1, 2, 3, 4, 5]); 76 | 77 | // Assert 78 | Assert.Equal(3, list.GetIndex(4)); 79 | } 80 | 81 | [Fact] 82 | public void SortedList_GetIndex_OrIndexOfInsertion() { 83 | // Arrange 84 | var list = new SortedList([1, 2, 3, 5, 6]); 85 | 86 | // Assert 87 | Assert.Equal(3, ~list.GetIndex(4)); 88 | } 89 | 90 | [Fact] 91 | public void SortedList_GetIndex_OrIndexOfInsertion_Larger() { 92 | // Arrange 93 | var list = new SortedList([1, 2, 3, 5, 6]); 94 | 95 | // Assert 96 | Assert.True(~list.GetIndex(7) > list.Count - 1); 97 | } 98 | 99 | [Fact] 100 | public void SortedList_GetIndex_OrIndexOfInsertion_LargerSection() { 101 | // Arrange 102 | var list = new SortedList([1, 2, 3, 5, 6]); 103 | 104 | // Act 105 | var index = list.GetIndex(4); 106 | ReadOnlySpan section = list.Span.Slice(~index); 107 | 108 | // Assert 109 | Assert.Equal([5, 6], section); 110 | } 111 | 112 | [Fact] 113 | public void SortedList_GetIndex_OrIndexOfInsertion_LargerSection_Class() { 114 | // Arrange 115 | var list = new SortedList( 116 | [ 117 | new Person("a", 1), 118 | new Person("b", 2), 119 | new Person("c", 3), 120 | new Person("d", 5), 121 | new Person("e",6) ] 122 | ); 123 | 124 | // Act + Assert 125 | var section = list.Span.Slice(~list.GetIndex(new Person("f", 4))); 126 | Assert.Equal([new Person("d", 5), new Person("e", 6)], section); 127 | 128 | // Act + Assert 129 | section = list.Span.Slice(list.GetIndex(new Person("f", 3)) + 1); 130 | Assert.Equal([new Person("d", 5), new Person("e", 6)], section); 131 | } 132 | 133 | private record Person(string Name, int Age) : IComparable { 134 | public int CompareTo(Person? other) { 135 | if (other is null) { 136 | return 1; 137 | } 138 | return Age.CompareTo(other.Age); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Collections/StringBuffersTests.cs: -------------------------------------------------------------------------------- 1 | using Sharpify.Collections; 2 | 3 | namespace Sharpify.Tests.Collections; 4 | 5 | public class StringBuffersTests { 6 | [Fact] 7 | public void StringBuffer_NoCapacity_Throws() { 8 | // Arrange 9 | Action act = () => { 10 | var buffer = new StringBuffer(); 11 | buffer.Append('a'); 12 | }; 13 | 14 | // Act & Assert 15 | Assert.Throws(act); 16 | } 17 | 18 | [Fact] 19 | public void StringBuffer_Append_ToFullCapacity() { 20 | // Arrange 21 | string text = "Hello world!"; 22 | 23 | // Act 24 | Action act = () => { 25 | var buffer = StringBuffer.Create(stackalloc char[text.Length]); 26 | buffer.Append(text); 27 | }; 28 | 29 | // Assert 30 | act(); 31 | } 32 | 33 | [Fact] 34 | public void StringBuffer_Append_BeyondCapacity() { 35 | // Arrange 36 | string text = "Hello world!"; 37 | 38 | // Act 39 | Action act = () => { 40 | var buffer = StringBuffer.Create(stackalloc char[text.Length - 1]); 41 | buffer.Append(text); 42 | }; 43 | 44 | // Assert 45 | Assert.Throws(act); 46 | } 47 | 48 | [Fact] 49 | public void StringBuffer_Append_BeyondToCapacityAndBeyond() { 50 | // Arrange 51 | string text = "Hello world!"; 52 | 53 | // Act 54 | 55 | Action act1 = () => { 56 | var buffer = StringBuffer.Create(stackalloc char[text.Length]); 57 | buffer.Append(text); 58 | buffer.Append(text); 59 | }; 60 | Action act2 = () => { 61 | var buffer = StringBuffer.Create(stackalloc char[text.Length]); 62 | buffer.Append(text); 63 | buffer.Append(1); 64 | }; 65 | Action act3 = () => { 66 | var buffer = StringBuffer.Create(stackalloc char[text.Length]); 67 | buffer.Append(text); 68 | buffer.Append('a'); 69 | }; 70 | 71 | // Assert 72 | Assert.Throws(act1); 73 | Assert.Throws(act2); 74 | Assert.Throws(act3); 75 | } 76 | 77 | [Fact] 78 | public void StringBuffer_AppendLine_OnElement() { 79 | // Arrange 80 | var buffer = StringBuffer.Create(stackalloc char[20]); 81 | 82 | // Act 83 | buffer.AppendLine("Hello"); 84 | buffer.Append("World"); 85 | 86 | var expected = string.Create(null, stackalloc char[20], $"Hello{Environment.NewLine}World"); 87 | 88 | // Assert 89 | Assert.Equal(expected, buffer.Allocate(true)); 90 | } 91 | 92 | [Fact] 93 | public void StringBuffer_AppendLine_NoParams() { 94 | // Arrange 95 | var buffer = StringBuffer.Create(stackalloc char[20]); 96 | 97 | // Act 98 | buffer.Append("Hello"); 99 | buffer.AppendLine(); 100 | buffer.Append("World"); 101 | 102 | var expected = string.Create(null, stackalloc char[20], $"Hello{Environment.NewLine}World"); 103 | 104 | // Assert 105 | Assert.Equal(expected, buffer.Allocate(true)); 106 | } 107 | 108 | [Fact] 109 | public void StringBuffer_AppendLine_NoParams_Builder() { 110 | // Arrange 111 | var buffer = StringBuffer.Create(stackalloc char[20]); 112 | 113 | // Act 114 | buffer.Append("Hello") 115 | .AppendLine() 116 | .Append("World"); 117 | 118 | var expected = string.Create(null, stackalloc char[20], $"Hello{Environment.NewLine}World"); 119 | 120 | // Assert 121 | Assert.Equal(expected, buffer.Allocate(true)); 122 | } 123 | 124 | [Fact] 125 | public void StringBuffer_NoTrimming_ReturnFullString() { 126 | // Arrange 127 | var buffer = StringBuffer.Create(stackalloc char[5]); 128 | 129 | // Act 130 | buffer.Append('a'); 131 | buffer.Append('b'); 132 | buffer.Append('c'); 133 | buffer.Append('d'); 134 | 135 | // Assert 136 | Assert.Equal("abcd\0", buffer.Allocate(false)); 137 | } 138 | 139 | [Fact] 140 | public void StringBuffer_WithTrimming_ReturnTrimmedString() { 141 | // Arrange 142 | var buffer = StringBuffer.Create(stackalloc char[5]); 143 | 144 | // Act 145 | buffer.Append('a'); 146 | buffer.Append('b'); 147 | buffer.Append('c'); 148 | buffer.Append('d'); 149 | 150 | // Assert 151 | Assert.Equal("abcd", buffer.Allocate(true)); 152 | } 153 | 154 | [Fact] 155 | public void StringBuffer_WithWhiteSpaceTrimming_ReturnTrimmedString() { 156 | // Arrange 157 | var buffer = StringBuffer.Create(stackalloc char[5]); 158 | 159 | // Act 160 | buffer.Append('a'); 161 | buffer.Append('b'); 162 | buffer.Append('c'); 163 | buffer.Append('d'); 164 | buffer.Append(' '); 165 | 166 | // Assert 167 | Assert.Equal("abcd", buffer.Allocate(true, true)); 168 | } 169 | 170 | [Fact] 171 | public void StringBuffer_Reset() { 172 | // Arrange 173 | var buffer = StringBuffer.Create(stackalloc char[20]); 174 | 175 | // Act 176 | buffer.Append("Hello world!"); 177 | buffer.Reset(); 178 | buffer.Append("David"); 179 | 180 | // Assert 181 | Assert.Equal("David", buffer.WrittenSpan); 182 | } 183 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Collections/TestLocalPersistentDictionary.cs: -------------------------------------------------------------------------------- 1 | using Sharpify.Collections; 2 | 3 | namespace Sharpify.Tests.Collections; 4 | 5 | public class TestLocalPersistentDictionary : LocalPersistentDictionary { 6 | private volatile int _serializedCount; 7 | 8 | public TestLocalPersistentDictionary(string path) : base(path) { 9 | } 10 | 11 | public int SerializedCount => _serializedCount; 12 | 13 | public override async Task SerializeDictionaryAsync() { 14 | Interlocked.Increment(ref _serializedCount); 15 | await base.SerializeDictionaryAsync(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Sharpify.Tests/EitherTests.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | 3 | public class EitherTests { 4 | [Fact] 5 | public void ImplicitOperatorFromT0_CreatesEitherWithT0Value() { 6 | // Arrange 7 | Either either = 42; 8 | 9 | // Assert 10 | Assert.True(either.IsT0); 11 | Assert.Equal(42, either.AsT0); 12 | } 13 | 14 | [Fact] 15 | public void ImplicitOperatorFromT1_CreatesEitherWithT1Value() { 16 | // Arrange 17 | Either either = "Hello"; 18 | 19 | // Assert 20 | Assert.True(either.IsT1); 21 | Assert.Equal("Hello", either.AsT1); 22 | } 23 | 24 | [Fact] 25 | public void Switch_WhenT0Value_IsUsed_CallsT0Handler() { 26 | // Arrange 27 | Either either = 42; 28 | bool t0HandlerCalled = false; 29 | bool t1HandlerCalled = false; 30 | 31 | // Act 32 | either.Switch(t0 => t0HandlerCalled = true, t1 => t1HandlerCalled = false); 33 | 34 | // Assert 35 | Assert.True(t0HandlerCalled); 36 | Assert.False(t1HandlerCalled); 37 | } 38 | 39 | [Fact] 40 | public void Switch_WhenT1Value_IsUsed_CallsT1Handler() { 41 | // Arrange 42 | Either either = "Hello"; 43 | bool t0HandlerCalled = false; 44 | bool t1HandlerCalled = false; 45 | 46 | // Act 47 | either.Switch(t0 => t0HandlerCalled = true, t1 => t1HandlerCalled = true); 48 | 49 | // Assert 50 | Assert.False(t0HandlerCalled); 51 | Assert.True(t1HandlerCalled); 52 | } 53 | 54 | [Fact] 55 | public void Match_WhenT0Value_IsUsed_ReturnsResultFromT0Handler() { 56 | // Arrange 57 | Either either = 42; 58 | 59 | // Act 60 | var result = either.Match(t0 => t0 * 2, t1 => t1.Length); 61 | 62 | // Assert 63 | Assert.Equal(84, result); 64 | } 65 | 66 | [Fact] 67 | public void Match_WhenT1Value_IsUsed_ReturnsResultFromT1Handler() { 68 | // Arrange 69 | Either either = "Hello"; 70 | 71 | // Act 72 | var result = either.Match(t0 => t0 * 2, t1 => t1.Length); 73 | 74 | // Assert 75 | Assert.Equal(5, result); 76 | } 77 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/ParallelExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | namespace Sharpify.Tests; 4 | 5 | public class ParallelExtensionsTests { 6 | [Fact] 7 | public async Task ForAll_WithAsyncAction() { 8 | // Arrange 9 | var dict = Enumerable.Range(1, 100).ToDictionary(x => x, x => x); 10 | var results = new ConcurrentDictionary(Environment.ProcessorCount, dict.Count); 11 | var action = new MultiplyActionDict(results); 12 | 13 | // Act 14 | await dict.ForAll(action); 15 | var expected = dict.ToDictionary(x => x.Key, x => x.Value * 2); 16 | 17 | // Assert 18 | Assert.Equal(expected, results); 19 | } 20 | 21 | [Fact] 22 | public async Task ForAll_WithLambda() { 23 | // Arrange 24 | var dict = Enumerable.Range(1, 100).ToDictionary(x => x, x => x); 25 | var results = new ConcurrentDictionary(Environment.ProcessorCount, dict.Count); 26 | var threads = new ConcurrentStack(); 27 | 28 | // Act 29 | await dict.ForAll((x, token) => { 30 | results[x.Key] = x.Value * 2; 31 | threads.Push(Environment.CurrentManagedThreadId); 32 | return Task.CompletedTask; 33 | }); 34 | var expected = dict.ToDictionary(x => x.Key, x => x.Value * 2); 35 | 36 | // Assert 37 | Assert.Equal(expected, results); 38 | } 39 | 40 | [Fact] 41 | public async Task ForAllAsync_WithAsyncAction() { 42 | // Arrange 43 | var dict = Enumerable.Range(1, 100).ToDictionary(x => x, x => x); 44 | var results = new ConcurrentDictionary(Environment.ProcessorCount, dict.Count); 45 | var action = new MultiplyActionDictAsync(results); 46 | 47 | // Act 48 | await dict.ForAllAsync(action); 49 | var expected = dict.ToDictionary(x => x.Key, x => x.Value * 2); 50 | 51 | // Assert 52 | Assert.Equal(expected, results); 53 | } 54 | 55 | [Fact] 56 | public async Task ForAllAsync_WithLambda() { 57 | // Arrange 58 | var dict = Enumerable.Range(1, 100).ToDictionary(x => x, x => x); 59 | var results = new ConcurrentDictionary(Environment.ProcessorCount, dict.Count); 60 | var threads = new ConcurrentStack(); 61 | 62 | // Act 63 | await dict.ForAllAsync(async (x, token) => { 64 | results[x.Key] = x.Value * 2; 65 | threads.Push(Environment.CurrentManagedThreadId); 66 | await Task.Delay(50, token); 67 | }); 68 | var expected = dict.ToDictionary(x => x.Key, x => x.Value * 2); 69 | 70 | // Assert 71 | Assert.Equal(expected, results); 72 | } 73 | 74 | [Fact] 75 | public async Task ForAllAsync_WithLambda_Asynchronous() { 76 | // Arrange 77 | var dict = Enumerable.Range(1, 100).ToDictionary(x => x, x => x); 78 | var results = new ConcurrentDictionary(Environment.ProcessorCount, dict.Count); 79 | var threads = new ConcurrentStack(); 80 | 81 | // Act 82 | await dict.ForAllAsync(async (x, token) => { 83 | results[x.Key] = x.Value * 2; 84 | threads.Push(Environment.CurrentManagedThreadId); 85 | await Task.Delay(50, token); 86 | }); 87 | var expected = dict.ToDictionary(x => x.Key, x => x.Value * 2); 88 | 89 | // Assert 90 | Assert.Equal(expected, results); 91 | } 92 | } 93 | 94 | public readonly struct MultiplyActionDict : IAsyncAction> { 95 | private readonly ConcurrentDictionary _results; 96 | public MultiplyActionDict(ConcurrentDictionary results) { 97 | _results = results; 98 | } 99 | public Task InvokeAsync(KeyValuePair value, CancellationToken token = default) { 100 | _results[value.Key] = value.Value * 2; 101 | return Task.CompletedTask; 102 | } 103 | } 104 | 105 | public readonly struct MultiplyActionDictAsync : IAsyncAction> { 106 | private readonly ConcurrentDictionary _results; 107 | public MultiplyActionDictAsync(ConcurrentDictionary results) { 108 | _results = results; 109 | } 110 | public async Task InvokeAsync(KeyValuePair value, CancellationToken token = default) { 111 | _results[value.Key] = value.Value * 2; 112 | await Task.Delay(50, token); 113 | } 114 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/ResultTests.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | 3 | public class ResultTests { 4 | [Fact] 5 | public void Result_DefaultConstructor_ThrowsException() { 6 | // Act 7 | Action act = () => new Result() { 8 | IsOk = true 9 | }; 10 | 11 | // Assert 12 | Assert.Throws(act); 13 | } 14 | 15 | [Fact] 16 | public void ResultT_DefaultConstructor_ThrowsException() { 17 | // Act 18 | Action act = () => new Result() { 19 | IsOk = true 20 | }; 21 | 22 | // Assert 23 | Assert.Throws(act); 24 | } 25 | 26 | [Fact] 27 | public void Ok_ResultWithoutMessage_ReturnsResultWithIsOkTrueAndNoMessage() { 28 | // Act 29 | var result = Result.Ok(); 30 | 31 | // Assert 32 | Assert.True(result.IsOk); 33 | Assert.Empty(result.Message); 34 | } 35 | 36 | [Fact] 37 | public void Ok_ResultWithMessage_ReturnsResultWithIsOkTrueAndMessage() { 38 | // Act 39 | var result = Result.Ok("Success"); 40 | 41 | // Assert 42 | Assert.True(result.IsOk); 43 | Assert.Equal("Success", result.Message); 44 | } 45 | 46 | [Fact] 47 | public void Fail_ResultWithMessage_ReturnsResultWithIsOkFalseAndMessage() { 48 | // Act 49 | var result = Result.Fail("Failure"); 50 | 51 | // Assert 52 | Assert.False(result.IsOk); 53 | Assert.Equal("Failure", result.Message); 54 | } 55 | 56 | [Fact] 57 | public void Ok_ResultWithValue_ReturnsResultWithIsOkTrueAndValue() { 58 | // Act 59 | var result = Result.Ok(42); 60 | 61 | // Assert 62 | Assert.True(result.IsOk); 63 | Assert.Equal(42, result.Value); 64 | } 65 | 66 | [Fact] 67 | public void Ok_ResultWithValueAndMessage_ReturnsResultWithIsOkTrueAndValueAndMessage() { 68 | // Act 69 | var result = Result.Ok("Success", 42); 70 | 71 | // Assert 72 | Assert.True(result.IsOk); 73 | Assert.Equal("Success", result.Message); 74 | Assert.Equal(42, result.Value); 75 | } 76 | 77 | [Fact] 78 | public void WithValue_ResultWithValue_ReturnsResultWithValueAndIsOkAndMessage() { 79 | // Arrange 80 | var result = Result.Ok("Success"); 81 | 82 | // Act 83 | var valueResult = result.WithValue(42); 84 | 85 | // Assert 86 | Assert.True(valueResult.IsOk); 87 | Assert.Equal("Success", valueResult.Message); 88 | Assert.Equal(42, valueResult.Value); 89 | } 90 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Routines/AsyncRoutineTests.cs: -------------------------------------------------------------------------------- 1 | using Sharpify.Routines; 2 | 3 | namespace Sharpify.Tests.Routines; 4 | 5 | public class AsyncRoutineTests { 6 | [Theory] 7 | [InlineData(5)] 8 | [InlineData(1)] 9 | [InlineData(10)] 10 | [InlineData(0)] 11 | public async Task AsyncRoutine_GivenIncreaseValueFunction_IncreasesValue(int expected) { 12 | // Arrange 13 | var count = 0; 14 | var tcs = new TaskCompletionSource(); 15 | var options = RoutineOptions.ExecuteInParallel; 16 | using var routine = new AsyncRoutine(TimeSpan.FromMilliseconds(50)) 17 | .ChangeOptions(options) 18 | .Add(_ => { 19 | var newCount = Interlocked.Increment(ref count); 20 | if (newCount >= expected) { 21 | tcs.TrySetResult(); 22 | } 23 | return Task.CompletedTask; 24 | }); 25 | 26 | // Act 27 | _ = routine.Start(); 28 | await tcs.Task; 29 | 30 | // Assert 31 | Assert.True(count >= expected); 32 | } 33 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Routines/RoutineTests.cs: -------------------------------------------------------------------------------- 1 | using Sharpify.Routines; 2 | 3 | namespace Sharpify.Tests.Routines; 4 | 5 | public class RoutineTests { 6 | [Theory] 7 | [InlineData(5)] 8 | [InlineData(1)] 9 | [InlineData(10)] 10 | [InlineData(0)] 11 | public async Task Routine_GivenIncreaseValueFunction_IncreasesValue(int expected) { 12 | // Arrange 13 | var count = 0; 14 | var tcs = new TaskCompletionSource(); 15 | using var routine = new Routine(50).Add(() => { 16 | Interlocked.Increment(ref count); 17 | if (count >= expected) { 18 | tcs.TrySetResult(); 19 | } 20 | }); 21 | 22 | // Act 23 | routine.Start(); 24 | await tcs.Task; 25 | 26 | // Assert 27 | Assert.True(count >= expected); 28 | } 29 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Sharpify.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0 5 | latest 6 | enable 7 | enable 8 | 9 | false 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /tests/Sharpify.Tests/StringExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | #pragma warning disable 3 | 4 | public class StringExtensionsTests { 5 | [Fact] 6 | public void IsNullOrEmpty_GivenNullString_ReturnsTrue() { 7 | // Arrange 8 | string value = null; 9 | 10 | // Act 11 | var result = value.IsNullOrEmpty(); 12 | 13 | // Assert 14 | Assert.True(result); 15 | } 16 | 17 | [Fact] 18 | public void IsNullOrEmpty_GivenEmptyString_ReturnsTrue() { 19 | // Arrange 20 | const string value = ""; 21 | 22 | // Act 23 | var result = value.IsNullOrEmpty(); 24 | 25 | // Assert 26 | Assert.True(result); 27 | } 28 | 29 | [Fact] 30 | public void IsNullOrWhiteSpace_GivenNullString_ReturnsTrue() { 31 | // Arrange 32 | string value = null; 33 | 34 | // Act 35 | var result = value.IsNullOrWhiteSpace(); 36 | 37 | // Assert 38 | Assert.True(result); 39 | } 40 | 41 | [Fact] 42 | public void IsNullOrWhiteSpace_GivenEmptyString_ReturnsTrue() { 43 | // Arrange 44 | const string value = ""; 45 | 46 | // Act 47 | var result = value.IsNullOrWhiteSpace(); 48 | 49 | // Assert 50 | Assert.True(result); 51 | } 52 | 53 | [Fact] 54 | public void IsNullOrWhiteSpace_GivenWhiteSpaceString_ReturnsTrue() { 55 | // Arrange 56 | const string value = " "; 57 | 58 | // Act 59 | var result = value.IsNullOrWhiteSpace(); 60 | 61 | // Assert 62 | Assert.True(result); 63 | } 64 | 65 | [Theory] 66 | [InlineData("0", 0)] 67 | [InlineData("1", 1)] 68 | [InlineData("123", 123)] 69 | [InlineData("2147483647", 2147483647)] // int.MaxValue 70 | public void TryConvertToInt32_ValidString_ReturnsTrue(string input, int expected) { 71 | bool result = input.AsSpan().TryConvertToInt32(out var output); 72 | 73 | Assert.True(result); 74 | Assert.Equal(expected, output); 75 | } 76 | 77 | [Theory] 78 | [InlineData("")] 79 | [InlineData("-1 5")] // whitespace 80 | [InlineData("214748364841232131231")] // larger than int.MaxValue 81 | [InlineData("1.23")] // decimal 82 | [InlineData("123abc")] // alphanumeric 83 | public void TryConvertToInt32_InvalidString_ReturnsFalse(string input) { 84 | bool result = input.AsSpan().TryConvertToInt32(out var output); 85 | 86 | Assert.False(result); 87 | Assert.Equal(0, output); // Ensure that the value is not changed in case of failure 88 | } 89 | 90 | // Tests for Concat 91 | [Theory] 92 | [InlineData("", "", "")] 93 | [InlineData("hello", "", "hello")] 94 | [InlineData("", "world", "world")] 95 | [InlineData("hello", "world", "helloworld")] 96 | public void Concat_WithVariousInputs_ReturnsCorrectResult( 97 | string value, string suffixString, string expectedResult) { 98 | // Arrange 99 | ReadOnlySpan suffix = suffixString; 100 | 101 | // Act 102 | string result = value.Concat(suffix); 103 | 104 | // Assert 105 | Assert.Equal(expectedResult, result); 106 | } 107 | 108 | // Tests for ToTitle 109 | [Theory] 110 | [InlineData("", "")] 111 | [InlineData("hello world", "Hello World")] 112 | public void ToTitle_WithVariousInputs_ReturnsTitleCase( 113 | string input, string expectedResult) { 114 | // Act 115 | string result = input.ToTitle(); 116 | 117 | // Assert 118 | Assert.Equal(expectedResult, result); 119 | } 120 | 121 | // Tests for IsBinary 122 | [Theory] 123 | [InlineData("", true)] 124 | [InlineData("0", true)] 125 | [InlineData("1", true)] 126 | [InlineData("00 11\n\t01\r10", true)] 127 | [InlineData("0012", false)] 128 | [InlineData("hello", false)] 129 | public void IsBinary_WithVariousInputs_ReturnsCorrectResult( 130 | string input, bool expectedResult) { 131 | // Act 132 | bool result = input.IsBinary(); 133 | 134 | // Assert 135 | Assert.Equal(expectedResult, result); 136 | } 137 | } 138 | #pragma warning restore -------------------------------------------------------------------------------- /tests/Sharpify.Tests/TempFile.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | 3 | public record TempFile { 4 | public string Path { get; } 5 | private const int Retries = 5; 6 | 7 | public static async Task CreateAsync() { 8 | int retries = Retries; 9 | create: 10 | try { 11 | return new TempFile(); 12 | } catch { 13 | if (Interlocked.Decrement(ref retries) < 0) { 14 | await Task.Delay(100); 15 | goto create; 16 | } else { 17 | throw; 18 | } 19 | } 20 | } 21 | 22 | private TempFile() { 23 | Path = Utils.Env.PathInBaseDirectory(Random.Shared.Next(1000000, 9999999).ToString()); 24 | using var _ = File.Create(Path); 25 | } 26 | 27 | public static implicit operator string(TempFile file) => file.Path; 28 | 29 | public async Task DeleteAsync() { 30 | if (!File.Exists(Path)) { 31 | return; 32 | } 33 | 34 | int retries = Retries; 35 | int delayInMs = 100; 36 | 37 | bool wasDeleted = false; 38 | do { 39 | try { 40 | File.Delete(Path); 41 | wasDeleted = true; 42 | } catch { 43 | if (Interlocked.Decrement(ref retries) >= 0) { 44 | delayInMs *= 2; 45 | await Task.Delay(delayInMs); 46 | } else { 47 | throw; 48 | } 49 | } 50 | } while (!wasDeleted); 51 | } 52 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/ThreadSafeTests.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | 3 | public class ThreadSafeTests { 4 | [Fact] 5 | public void ThreadSafe_EmptyConstructor() { 6 | ThreadSafe wrapper = new(); 7 | 8 | Assert.Equal(0, wrapper.Value); 9 | } 10 | 11 | [Fact] 12 | public void ThreadSafe_ValueConstructor() { 13 | ThreadSafe wrapper = new(42); 14 | 15 | int result = wrapper.Value; 16 | 17 | Assert.Equal(42, result); 18 | } 19 | 20 | [Fact] 21 | public void ThreadSafe_UpdateValue() { 22 | ThreadSafe wrapper = new(5); 23 | const int newValue = 99; 24 | 25 | int result = wrapper.Modify(_ => newValue); 26 | 27 | Assert.Equal(newValue, result); 28 | } 29 | 30 | [Theory] 31 | [InlineData(1, 2, 3)] 32 | [InlineData(2, 3, 5)] 33 | [InlineData(3, 4, 7)] 34 | public void ThreadSafe_ModifyValue(int original, int addition, int expected) { 35 | ThreadSafe wrapper = new(original); 36 | 37 | int result = wrapper.Modify(value => value + addition); 38 | 39 | Assert.Equal(expected, result); 40 | } 41 | 42 | [Theory] 43 | [InlineData(100,100)] 44 | [InlineData(200, 200)] 45 | [InlineData(300, 300)] 46 | public async Task ThreadSafe_MultiThreadedAccess(int amount, int expected) { 47 | ThreadSafe wrapper = new(0); 48 | 49 | var tasks = Enumerable.Range(0, amount).AsParallel().Select(i => Task.Run(() => wrapper.Modify(value => value + 1))); 50 | await Task.WhenAll(tasks); 51 | 52 | Assert.Equal(expected, wrapper.Value); 53 | } 54 | 55 | [Fact] 56 | public void ThreadSafe_GetHashCode() { 57 | int val = 42; 58 | 59 | ThreadSafe wrapper = new(val); 60 | 61 | int actual = wrapper.GetHashCode(); 62 | int expected = val.GetHashCode(); 63 | 64 | Assert.Equal(expected, actual); 65 | } 66 | 67 | [Theory] 68 | [InlineData(1, 1)] 69 | [InlineData(2, 2)] 70 | [InlineData(3, 3)] 71 | [InlineData(-4, -4)] 72 | public void ThreadSafe_Equals(int actual, int expected) { 73 | ThreadSafe wrapper = new(actual); 74 | 75 | Assert.True(wrapper.Equals(expected)); 76 | } 77 | 78 | [Fact] 79 | public void ThreadSafe_Equals_Null() { 80 | int val = 42; 81 | 82 | ThreadSafe wrapper = new(val); 83 | 84 | Assert.False(wrapper.Equals(null)); 85 | } 86 | 87 | [Fact] 88 | public void ThreadSafe_Equals_ThreadSafe() { 89 | int val = 42; 90 | 91 | ThreadSafe wrapper = new(val); 92 | 93 | Assert.True(wrapper.Equals(new ThreadSafe(val))); 94 | } 95 | 96 | [Fact] 97 | public void ThreadSafe_Equals_Object() { 98 | int val = 42; 99 | 100 | ThreadSafe wrapper = new(val); 101 | var other = (object)new ThreadSafe(val); 102 | 103 | Assert.True(wrapper.Equals(other)); 104 | } 105 | 106 | [Fact] 107 | public void ThreadSafe_Equals_NullObject() { 108 | int val = 42; 109 | 110 | ThreadSafe wrapper = new(val); 111 | object? other = null; 112 | 113 | Assert.False(wrapper.Equals(other)); 114 | } 115 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/UnmanagedExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | 3 | public class UnmanagedExtensionsTests { 4 | public enum ExampleEnum { 5 | FirstValue, 6 | SecondValue, 7 | ThirdValue 8 | } 9 | 10 | [Theory] 11 | [InlineData("FirstValue", true, ExampleEnum.FirstValue)] 12 | [InlineData("SecondValue", true, ExampleEnum.SecondValue)] 13 | [InlineData("ThirdValue", true, ExampleEnum.ThirdValue)] 14 | [InlineData("InvalidValue", false, default(ExampleEnum))] 15 | [InlineData("", false, default(ExampleEnum))] 16 | public void TryParseAsEnum_WithVariousInputs_ReturnsCorrectResult( 17 | string value, bool expectedResult, ExampleEnum expectedEnum) { 18 | // Act 19 | bool result = value.TryParseAsEnum(out ExampleEnum parsedEnum); 20 | 21 | // Assert 22 | Assert.Equal(expectedResult, result); 23 | Assert.Equal(expectedEnum, parsedEnum); 24 | } 25 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/UnsafeSpanIteratorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | 3 | public class UnsafeSpanIteratorTests { 4 | [Fact] 5 | public void UnsafeSpanIterator_UseLinq() { 6 | // Arrange 7 | Span items = stackalloc int[100]; 8 | Enumerable.Range(0, 100).ToArray().CopyTo(items); 9 | 10 | // Act 11 | var iterator = new UnsafeSpanIterator(items); 12 | var sum = iterator.Select(x => x + 1).Sum(); // [0-99] -> [1-100] 13 | 14 | // Assert 15 | Assert.Equal(5050, sum); 16 | } 17 | 18 | [Fact] 19 | public async Task UnsafeSpanIterator_UseConcurrentlyInAsync() { 20 | // Arrange 21 | var arr = Enumerable.Range(1, 100).ToArray(); 22 | int sum = 0; 23 | 24 | // Act 25 | async Task Increment(int item) { 26 | await Task.Delay(20); 27 | Interlocked.Add(ref sum, item); 28 | } 29 | var iterator = new UnsafeSpanIterator(arr.AsSpan()); 30 | var tasks = iterator.AsParallel().Select(Increment); 31 | await Task.WhenAll(tasks); 32 | 33 | // Assert 34 | Assert.Equal(5050, sum); 35 | } 36 | 37 | [Fact] 38 | public void UnsafeSpanIterator_Slice() { 39 | // Arrange 40 | Span items = [1, 2, 3, 4, 5, 6]; 41 | 42 | // Act 43 | var iterator = new UnsafeSpanIterator(items); 44 | var slice = iterator.Slice(3, 2); 45 | 46 | // Assert 47 | Assert.Equal(slice, [4, 5]); 48 | } 49 | 50 | [Fact] 51 | public void UnsafeSpanIterator_ToEnumerable() { 52 | // Arrange 53 | Span items = [1, 2, 3, 4, 5, 6]; 54 | 55 | // Act 56 | var iterator = new UnsafeSpanIterator(items); 57 | var count = iterator.ToEnumerable().Count(); 58 | 59 | // Assert 60 | Assert.Equal(items.Length, count); 61 | } 62 | 63 | [Fact] 64 | public void UnsafeSpanIterator_SliceBeyondBounds_Throws() { 65 | // Arrange 66 | Span items = [1, 2, 3, 4, 5, 6]; 67 | 68 | // Act 69 | var iterator = new UnsafeSpanIterator(items); 70 | var act = () => { var slice = iterator.Slice(4, 4); }; 71 | 72 | // Assert 73 | Assert.Throws(act); 74 | } 75 | 76 | [Fact] 77 | public void UnsafeSpanIterator_GetByIndex() { 78 | // Arrange 79 | Span items = [1, 2, 3, 4, 5, 6]; 80 | 81 | // Act 82 | var iterator = new UnsafeSpanIterator(items); 83 | 84 | // Assert 85 | Assert.Equal(4, iterator[3]); 86 | } 87 | 88 | [Fact] 89 | public void UnsafeSpanIterator_GetByIndexBeyondBounds_Throws() { 90 | // Arrange 91 | Span items = [1, 2, 3, 4, 5, 6]; 92 | 93 | // Act 94 | var iterator = new UnsafeSpanIterator(items); 95 | var act = () => { var item = iterator[9]; }; 96 | 97 | // Assert 98 | Assert.Throws(act); 99 | } 100 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using System.Collections.Generic; 2 | global using System.Runtime.CompilerServices; 3 | global using System.Runtime.InteropServices; 4 | global using Sharpify; 5 | global using Xunit; -------------------------------------------------------------------------------- /tests/Sharpify.Tests/UtilsDateAndTimeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | 3 | namespace Sharpify.Tests; 4 | 5 | public partial class UtilsTests { 6 | [Theory] 7 | [InlineData(0.00001, "0ms")] 8 | [InlineData(0.01, "10ms")] 9 | [InlineData(0.5, "500ms")] 10 | [InlineData(1, "01:000s")] 11 | [InlineData(59.99, "59:990s")] 12 | [InlineData(60, "01:00m")] 13 | [InlineData(61, "01:01m")] 14 | [InlineData(121.234, "02:01m")] 15 | public void FormatTimeSpan_ReturnsFormattedSpan(double seconds, string expected) { 16 | // Arrange 17 | var elapsed = TimeSpan.FromSeconds(seconds); 18 | using var owner = MemoryPool.Shared.Rent(30); 19 | 20 | // Act 21 | ReadOnlySpan result = Utils.DateAndTime.FormatTimeSpan(elapsed, owner.Memory.Span); 22 | 23 | // Assert 24 | Assert.Equal(expected, result); 25 | } 26 | 27 | [Theory] 28 | [InlineData(0.00001, "0ms")] 29 | [InlineData(0.01, "10ms")] 30 | [InlineData(0.5, "500ms")] 31 | [InlineData(1, "01:000s")] 32 | [InlineData(59.99, "59:990s")] 33 | [InlineData(60, "01:00m")] 34 | [InlineData(61, "01:01m")] 35 | [InlineData(121.234, "02:01m")] 36 | public void FormatTimeSpan_ReturnsFormattedString(double seconds, string expected) { 37 | // Arrange 38 | var elapsed = TimeSpan.FromSeconds(seconds); 39 | 40 | // Act 41 | string result = Utils.DateAndTime.FormatTimeSpan(elapsed); 42 | 43 | // Assert 44 | Assert.Equal(expected, result); 45 | } 46 | 47 | [Fact] 48 | public void ToTimeStamp_ReturnsFormattedSpan() { 49 | // Arrange 50 | var dateTime = new DateTime(2022, 04, 06, 13, 55, 00); 51 | using var owner = MemoryPool.Shared.Rent(30); 52 | 53 | // Act 54 | ReadOnlySpan result = Utils.DateAndTime.FormatTimeStamp(dateTime, owner.Memory.Span); 55 | 56 | // Assert 57 | Assert.Equal("1355-6-Apr-22", result); 58 | } 59 | 60 | [Fact] 61 | public void ToTimeStamp_ReturnsFormattedString() { 62 | // Arrange 63 | var dateTime = new DateTime(2022, 04, 06, 13, 55, 00); 64 | 65 | // Act 66 | string result = Utils.DateAndTime.FormatTimeStamp(dateTime); 67 | 68 | // Assert 69 | Assert.Equal("1355-6-Apr-22", result); 70 | } 71 | 72 | [Fact] 73 | public async Task GetCurrentTimeAsync_ReturnsCurrentTime() { 74 | // Arrange 75 | var expected = DateTime.Now; 76 | 77 | // Act 78 | var result = await Utils.DateAndTime.GetCurrentTimeAsync(); 79 | 80 | // Assert 81 | Assert.Equal(expected, result, TimeSpan.FromSeconds(1)); 82 | } 83 | 84 | [Fact] 85 | public async Task GetCurrentTimeInBinaryAsync_ReturnsCurrentTimeInBinary() { 86 | // Arrange 87 | var expected = DateTime.Now; 88 | 89 | // Act 90 | var result = await Utils.DateAndTime.GetCurrentTimeInBinaryAsync(); 91 | var fromResult = DateTime.FromBinary(result); 92 | 93 | // Assert 94 | Assert.Equal(expected, fromResult, TimeSpan.FromSeconds(1)); 95 | } 96 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/UtilsMathematicsTests.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | 3 | public partial class UtilsTests { 4 | [Theory] 5 | [InlineData(0, 5, 2, 2.5)] 6 | [InlineData(20, 10, 2, 15)] 7 | [InlineData(30, 30, 2, 30)] 8 | public void RollingAverage_WithVariousInputs_ReturnsCorrectResult( 9 | double val, double newVal, int count, double expectedResult) { 10 | // Arrange 11 | expectedResult = Math.Round(expectedResult, 15); 12 | 13 | // Act 14 | double result = Utils.Mathematics.RollingAverage(val, newVal, count); 15 | result = Math.Round(result, 3); 16 | 17 | // Assert 18 | Assert.Equal(expectedResult, result); 19 | } 20 | 21 | [Theory] 22 | [InlineData(5, 120)] 23 | [InlineData(8, 40320)] 24 | [InlineData(11, 39916800)] 25 | public void Factorial_ValidInput_ValidResult(double n, double expected) { 26 | // Act 27 | var result = Utils.Mathematics.Factorial(n); 28 | 29 | // Assert 30 | Assert.Equal(expected, result); 31 | } 32 | 33 | [Theory] 34 | [InlineData(5, 5)] 35 | [InlineData(6, 8)] 36 | [InlineData(15, 610)] 37 | [InlineData(33, 3524578)] 38 | public void FibonacciApproximation_ValidInput_ValidResult(int n, double expected) { 39 | // Act 40 | var result = Utils.Mathematics.FibonacciApproximation(n); 41 | 42 | // Assert 43 | const double margin = 0.01; 44 | Assert.Equal(expected, result, margin); 45 | } 46 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/UtilsStringsTests.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | 3 | public partial class UtilsTests { 4 | [Theory] 5 | [InlineData(0.0, "0 B")] 6 | [InlineData(1023.0, "1,023 B")] 7 | [InlineData(1024.0, "1 KB")] 8 | [InlineData(1057.393, "1.03 KB")] 9 | [InlineData(1048576.0, "1 MB")] 10 | [InlineData(1073741824.0, "1 GB")] 11 | [InlineData(1099511627776.0, "1 TB")] 12 | [InlineData(1125899906842624.0, "1 PB")] 13 | public void FormatBytes_DoubleWithVariousInputs_ReturnsCorrectResult( 14 | double bytes, string expectedResult) { 15 | // Act 16 | string result = Utils.Strings.FormatBytes(bytes); 17 | 18 | // Assert 19 | Assert.Equal(expectedResult, result); 20 | } 21 | 22 | [Theory] 23 | [InlineData(0L, "0 B")] 24 | [InlineData(1023L, "1,023 B")] 25 | [InlineData(1024L, "1 KB")] 26 | [InlineData(1048576L, "1 MB")] 27 | [InlineData(1073741824L, "1 GB")] 28 | [InlineData(1099511627776L, "1 TB")] 29 | [InlineData(1125899906842624L, "1 PB")] 30 | public void FormatBytes_LongWithVariousInputs_ReturnsCorrectResult( 31 | long bytes, string expectedResult) { 32 | // Act 33 | string result = Utils.Strings.FormatBytes(bytes); 34 | 35 | // Assert 36 | Assert.Equal(expectedResult, result); 37 | } 38 | 39 | [Theory] 40 | [InlineData(1610612736L, "1.5 GB")] // 1.5 * 1024^3 41 | [InlineData(1627389952L, "1.52 GB")] // 1.52 * 1024^3 42 | [InlineData(1644167168L, "1.53 GB")] // 1.53 * 1024^3 43 | public void FormatBytes_LongWithNonRoundedInputs_ReturnsCorrectResult( 44 | long bytes, string expectedResult) { 45 | // Act 46 | string result = Utils.Strings.FormatBytes(bytes); 47 | 48 | // Assert 49 | Assert.Equal(expectedResult, result); 50 | } 51 | 52 | [Theory] 53 | [InlineData(1610612736.0, "1.5 GB")] // 1.5 * 1024^3 54 | [InlineData(1627389952.0, "1.52 GB")] // 1.52 * 1024^3 55 | [InlineData(1644167168.0, "1.53 GB")] // 1.53 * 1024^3 56 | public void FormatBytes_DoubleWithNonRoundedInputs_ReturnsCorrectResult( 57 | double bytes, string expectedResult) { 58 | // Act 59 | string result = Utils.Strings.FormatBytes(bytes); 60 | 61 | // Assert 62 | Assert.Equal(expectedResult, result); 63 | } 64 | 65 | [Fact] 66 | public void FormatBytes_Double_HasEnoughCapacity() { 67 | // Arrange 68 | Action act = () => _ = Utils.Strings.FormatBytes(double.MaxValue); 69 | 70 | // Assert 71 | act(); 72 | } 73 | 74 | [Fact] 75 | public void FormatBytes_Long_HasEnoughCapacity() { 76 | // Arrange 77 | Action act = () => _ = Utils.Strings.FormatBytes(long.MaxValue); 78 | 79 | // Assert 80 | act(); 81 | } 82 | } -------------------------------------------------------------------------------- /tests/Sharpify.Tests/UtilsUnsafeTests.cs: -------------------------------------------------------------------------------- 1 | namespace Sharpify.Tests; 2 | 3 | public partial class UtilsTests { 4 | [Fact] 5 | public void CreateIntegerPredicate_ForCharIsDigit_Valid() { 6 | // Arrange 7 | var predicate = Utils.Unsafe.CreateIntegerPredicate(char.IsDigit); 8 | 9 | // Act 10 | var one = predicate('1'); 11 | var a = predicate('a'); 12 | 13 | // Assert 14 | Assert.Equal(1, one); 15 | Assert.Equal(0, a); 16 | } 17 | 18 | [Fact] 19 | public void TryUnbox_ForValidInput_ValidResult() { 20 | // Arrange 21 | var obj = (object) 5; 22 | 23 | // Act 24 | var result = Utils.Unsafe.TryUnbox(obj, out var value); 25 | 26 | // Assert 27 | Assert.True(result); 28 | Assert.Equal(5, value); 29 | } 30 | 31 | [Fact] 32 | public void AsMutableSpan_ForValidInput_ValidResult() { 33 | // Arrange 34 | string str = "abc"; 35 | var span = str.AsSpan(); 36 | 37 | // Act 38 | var mutableSpan = Utils.Unsafe.AsMutableSpan(span); 39 | mutableSpan[2] = '1'; 40 | 41 | // Assert 42 | Assert.Equal("ab1", str); 43 | } 44 | } --------------------------------------------------------------------------------