diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f2e5b97 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,145 @@ +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8 +end_of_line = crlf + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false : error +dotnet_style_qualification_for_property = false : error +dotnet_style_qualification_for_method = false : error +dotnet_style_qualification_for_event = false : error + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true : error +dotnet_style_predefined_type_for_member_access = true : error + +# Suggest more modern language features when available +dotnet_style_object_initializer = true : suggestion +dotnet_style_collection_initializer = true : suggestion +dotnet_style_coalesce_expression = true : error +dotnet_style_null_propagation = true : error +dotnet_style_explicit_tuple_names = true : error + +# Naming rule: private static fields should use pascal casing +dotnet_naming_symbols.private_static_field.applicable_kinds = field +dotnet_naming_symbols.private_static_field.applicable_accessibilities = private +dotnet_naming_symbols.private_static_field.required_modifiers = static +dotnet_naming_style.private_static_field.capitalization = pascal_case +dotnet_naming_rule.private_static_field.severity = error +dotnet_naming_rule.private_static_field.symbols = private_static_field +dotnet_naming_rule.private_static_field.style = private_static_field + +# Naming rule: private fields should use camel casing and use underscore as a prefix +dotnet_naming_symbols.private_field.applicable_kinds = field +dotnet_naming_symbols.private_field.applicable_accessibilities = private +dotnet_naming_style.private_field.capitalization = camel_case +dotnet_naming_style.private_field.required_prefix = _ +dotnet_naming_rule.private_field.severity = error +dotnet_naming_rule.private_field.symbols = private_field +dotnet_naming_rule.private_field.style = private_field + +# Naming rule: non-private fields should use pascal casing +dotnet_naming_symbols.non_private_field.applicable_kinds = field +dotnet_naming_symbols.non_private_field.applicable_accessibilities = public,internal,friend,protected,protected_internal,protected_friend +dotnet_naming_style.non_private_field.capitalization = pascal_case +dotnet_naming_rule.non_private_field.severity = error +dotnet_naming_rule.non_private_field.symbols = non_private_field +dotnet_naming_rule.non_private_field.style = non_private_field + +# Naming rule: parameters should use camel casing +dotnet_naming_symbols.parameter.applicable_kinds = parameter +dotnet_naming_style.parameter.capitalization = camel_case +dotnet_naming_rule.parameter.severity = error +dotnet_naming_rule.parameter.symbols = parameter +dotnet_naming_rule.parameter.style = parameter + +# Naming rule: interface types should use pascal casing with I as a prefix +dotnet_naming_symbols.interface_type.applicable_kinds = interface +dotnet_naming_style.interface_type.capitalization = pascal_case +dotnet_naming_style.interface_type.required_prefix = I +dotnet_naming_rule.interface_type.severity = error +dotnet_naming_rule.interface_type.symbols = interface_type +dotnet_naming_rule.interface_type.style = interface_type + +# Naming rule: non-interface types should use pascal casing +dotnet_naming_symbols.non_interface_type.applicable_kinds = class,struct,enum,delegate +dotnet_naming_style.non_interface_type.capitalization = pascal_case +dotnet_naming_rule.non_interface_type.severity = error +dotnet_naming_rule.non_interface_type.symbols = non_interface_type +dotnet_naming_rule.non_interface_type.style = non_interface_type + +# Naming rule: methods, properties and events should use pascal casing +dotnet_naming_symbols.member.applicable_kinds = method,property,event +dotnet_naming_style.member.capitalization = pascal_case +dotnet_naming_rule.member.severity = error +dotnet_naming_rule.member.symbols = member +dotnet_naming_rule.member.style = member + +# CSharp code style settings: + +# IDE0060: Remove unused parameter +dotnet_code_quality_unused_parameters = non_public:suggestion + +[*.cs] +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true : suggestion +csharp_style_var_when_type_is_apparent = true : error +csharp_style_var_elsewhere = true : suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false : none +csharp_style_expression_bodied_constructors = false : error +csharp_style_expression_bodied_operators = false : none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true : suggestion +csharp_style_expression_bodied_indexers = true : suggestion +csharp_style_expression_bodied_accessors = true : suggestion + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true : suggestion +csharp_style_pattern_matching_over_as_with_null_check = true : suggestion +csharp_style_inlined_variable_declaration = true : suggestion +csharp_style_throw_expression = true : suggestion +csharp_style_conditional_delegate_call = true : suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +# Spacing +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 99b4b1e..690c193 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1 @@ github: [chrissainty] - diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 5778a3d..ac74e15 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,9 +1,5 @@ -name-template: 'v$NEXT_PATCH_VERSION' +name-template: 'v$NEXT_PATCH_VERSION 🌈' tag-template: 'v$NEXT_PATCH_VERSION' -template: | - # What's Changed - - $CHANGES categories: - title: '🚀 Features' label: 'Feature' @@ -13,4 +9,8 @@ categories: label: 'Maintenance' - title: '📖 Documentation' label: 'Docs' -change-template: '- (#$NUMBER) $TITLE - Thanks to @$AUTHOR ' \ No newline at end of file +change-template: '- (#$NUMBER) $TITLE - Thanks to @$AUTHOR ' +template: | + # What's Changed + + $CHANGES \ No newline at end of file diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index b827250..b7c42a5 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -7,12 +7,10 @@ on: types: [ published ] env: - NETCORE_VERSION: '3.1.301' + NETCORE_VERSION: '7.0.x' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true PROJECT_NAME: SessionStorage - GITHUB_FEED: https://nuget.pkg.github.com/Blazored/ - GITHUB_USER: chrissainty GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NUGET_FEED: https://api.nuget.org/v3/index.json NUGET_KEY: ${{ secrets.NUGET_KEY }} @@ -38,60 +36,24 @@ jobs: - name: Build run: dotnet build -c Release --no-restore src/Blazored.$PROJECT_NAME/Blazored.$PROJECT_NAME.csproj - - name: Test - run: dotnet test -c Release + - name: Build Test Extensions + run: dotnet build -c Release --no-restore src/Blazored.$PROJECT_NAME.TestExtensions/Blazored.$PROJECT_NAME.TestExtensions.csproj - - name: Pack - run: dotnet pack -v normal -c Release --no-restore --include-symbols --include-source -p:PackageVersion=$GITHUB_RUN_ID src/Blazored.$PROJECT_NAME/Blazored.$PROJECT_NAME.*proj + - name: Test Local Storage + run: dotnet test -c Release --no-restore --verbosity normal tests/Blazored.$PROJECT_NAME.Tests - - name: Publish Sample Site - run: dotnet publish -c Release samples/BlazorWebAssembly/BlazorWebAssembly.csproj - - - name: Rewrite base href - uses: SteveSandersonMS/ghaction-rewrite-base-href@v1 - with: - html_path: samples/BlazorWebAssembly/bin/Release/netstandard2.1/publish/wwwroot/index.html - base_href: /${{ env.PROJECT_NAME }}/ - - - name: Deploy to Github Pages - uses: JamesIves/github-pages-deploy-action@releases/v3 - with: - ACCESS_TOKEN: $GITHUB_TOKEN - BASE_BRANCH: main # The branch the action should deploy from. - BRANCH: gh-pages # The branch the action should deploy to. - FOLDER: samples/BlazorWebAssembly/bin/Release/netstandard2.1/publish/wwwroot # The folder the action should deploy. - SINGLE_COMMIT: true - - - name: Upload Artifact - uses: actions/upload-artifact@v2 - with: - name: nupkg - path: ./src/Blazored.${{ env.PROJECT_NAME }}/bin/Release/*.nupkg - prerelease: - needs: build - if: github.event_name != 'release' - runs-on: ubuntu-latest - steps: - - name: Download Artifact - uses: actions/download-artifact@v1 - with: - name: nupkg - - name: Push to GitHub Feed - run: | - for f in ./nupkg/*.nupkg - do - curl -vX PUT -u "$GITHUB_USER:$GITHUB_TOKEN" -F package=@$f $GITHUB_FEED - done deploy: needs: build if: github.event_name == 'release' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ env.NETCORE_VERSION }} + - name: Create Release NuGet package run: | arrTag=(${GITHUB_REF//\// }) @@ -100,11 +62,25 @@ jobs: VERSION="${VERSION//v}" echo Clean Version: $VERSION dotnet pack -v normal -c Release --include-symbols --include-source -p:PackageVersion=$VERSION -o nupkg src/Blazored.$PROJECT_NAME/Blazored.$PROJECT_NAME.*proj - - name: Push to GitHub Feed - run: | - for f in ./nupkg/*.nupkg - do - curl -vX PUT -u "$GITHUB_USER:$GITHUB_TOKEN" -F package=@$f $GITHUB_FEED - done + dotnet pack -v normal -c Release --include-symbols --include-source -p:PackageVersion=$VERSION -o nupkg src/Blazored.$PROJECT_NAME.TestExtensions/Blazored.$PROJECT_NAME.TestExtensions.csproj + - name: Push to NuGet Feed - run: dotnet nuget push ./nupkg/*.nupkg --source $NUGET_FEED --skip-duplicate --api-key $NUGET_KEY + run: dotnet nuget push ./nupkg/*.nupkg --source $NUGET_FEED --api-key $NUGET_KEY --skip-duplicate + + - name: Publish Sample Site + run: dotnet publish -c Release samples/BlazorWebAssembly/BlazorWebAssembly.csproj + + - name: Rewrite base href + uses: SteveSandersonMS/ghaction-rewrite-base-href@v1 + with: + html_path: samples/BlazorWebAssembly/bin/Release/net7.0/publish/wwwroot/index.html + base_href: /${{ env.PROJECT_NAME }}/ + + - name: Deploy to Github Pages + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + ACCESS_TOKEN: $GITHUB_TOKEN + BASE_BRANCH: main # The branch the action should deploy from. + BRANCH: gh-pages # The branch the action should deploy to. + FOLDER: samples/BlazorWebAssembly/bin/Release/net7.0/publish/wwwroot # The folder the action should deploy. + SINGLE_COMMIT: true diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index bdc0578..1584fc7 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -5,8 +5,8 @@ on: branches: [ main ] env: - PROJECT_NAME: SessionStorage - NETCORE_VERSION: '3.1.301' + NETCORE_VERSION: '7.0.x' + PROJECT_NAME: Blazored.SessionStorage jobs: build: @@ -23,8 +23,11 @@ jobs: - name: Restoring packages... run: dotnet restore - - name: Building project... - run: dotnet build --configuration Release --no-restore src/Blazored.$PROJECT_NAME/Blazored.$PROJECT_NAME.csproj + - name: Building Session Storage... + run: dotnet build --configuration Release --no-restore src/$PROJECT_NAME/$PROJECT_NAME.csproj + + - name: Build Session Storage Test Extensions + run: dotnet build -c Release --no-restore src/$PROJECT_NAME.TestExtensions/$PROJECT_NAME.TestExtensions.csproj - name: Testing... - run: dotnet test --no-restore --verbosity normal \ No newline at end of file + run: dotnet test -c Release --no-restore --verbosity normal tests/$PROJECT_NAME.Tests \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..67341dd --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '28 17 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore index 3e759b7..d2d12b1 100644 --- a/.gitignore +++ b/.gitignore @@ -179,6 +179,8 @@ PublishScripts/ # NuGet Packages *.nupkg +*.snupkg + # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. diff --git a/Blazored.SessionStorage.sln b/Blazored.SessionStorage.sln index 126452d..4e05906 100644 --- a/Blazored.SessionStorage.sln +++ b/Blazored.SessionStorage.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28531.58 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31717.71 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazored.SessionStorage", "src\Blazored.SessionStorage\Blazored.SessionStorage.csproj", "{EFA12DC3-35DE-449C-88F2-2BD7576CAF2C}" EndProject @@ -9,14 +9,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F20B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWebAssembly", "samples\BlazorWebAssembly\BlazorWebAssembly.csproj", "{D6953CE0-6AD5-434C-99AA-7FF1E26724D4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorServerSide", "samples\BlazorServerSide\BlazorServerSide.csproj", "{767D657A-5410-4714-9477-99929D3FF709}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorServer", "samples\BlazorServer\BlazorServer.csproj", "{767D657A-5410-4714-9477-99929D3FF709}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B1C958A-85F6-4387-98D9-71CEEE0A2A03}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9170D7A9-70CE-48E3-88A3-F11D2983103E}" ProjectSection(SolutionItems) = preProject - azure-pipelines.yml = azure-pipelines.yml + .gitignore = .gitignore + .github\workflows\ci-main.yml = .github\workflows\ci-main.yml + .github\workflows\ci-pr.yml = .github\workflows\ci-pr.yml README.md = README.md + .github\workflows\release-drafter.yml = .github\workflows\release-drafter.yml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B82A5126-EE01-4B4D-B642-F954B6E4E695}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazored.SessionStorage.Tests", "tests\Blazored.SessionStorage.Tests\Blazored.SessionStorage.Tests.csproj", "{5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "bUnitExample", "samples\bUnitExample\bUnitExample.csproj", "{10F6FB83-3135-4B73-B0EB-D008A31AE8FA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazored.SessionStorage.TestExtensions", "src\Blazored.SessionStorage.TestExtensions\Blazored.SessionStorage.TestExtensions.csproj", "{528AE63D-0043-4A63-9151-92107397EE02}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +74,42 @@ Global {767D657A-5410-4714-9477-99929D3FF709}.Release|x64.Build.0 = Release|Any CPU {767D657A-5410-4714-9477-99929D3FF709}.Release|x86.ActiveCfg = Release|Any CPU {767D657A-5410-4714-9477-99929D3FF709}.Release|x86.Build.0 = Release|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Debug|x64.Build.0 = Debug|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Debug|x86.Build.0 = Debug|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Release|Any CPU.Build.0 = Release|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Release|x64.ActiveCfg = Release|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Release|x64.Build.0 = Release|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Release|x86.ActiveCfg = Release|Any CPU + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC}.Release|x86.Build.0 = Release|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Debug|x64.Build.0 = Debug|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Debug|x86.Build.0 = Debug|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Release|Any CPU.Build.0 = Release|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Release|x64.ActiveCfg = Release|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Release|x64.Build.0 = Release|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Release|x86.ActiveCfg = Release|Any CPU + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA}.Release|x86.Build.0 = Release|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Debug|x64.ActiveCfg = Debug|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Debug|x64.Build.0 = Debug|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Debug|x86.ActiveCfg = Debug|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Debug|x86.Build.0 = Debug|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Release|Any CPU.Build.0 = Release|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Release|x64.ActiveCfg = Release|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Release|x64.Build.0 = Release|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Release|x86.ActiveCfg = Release|Any CPU + {528AE63D-0043-4A63-9151-92107397EE02}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -70,6 +117,8 @@ Global GlobalSection(NestedProjects) = preSolution {D6953CE0-6AD5-434C-99AA-7FF1E26724D4} = {F20B2AAB-FB71-4A1C-82FE-F1750B922BF6} {767D657A-5410-4714-9477-99929D3FF709} = {F20B2AAB-FB71-4A1C-82FE-F1750B922BF6} + {5A6A013E-325D-4A5F-B2FA-659B4FD2BDBC} = {B82A5126-EE01-4B4D-B642-F954B6E4E695} + {10F6FB83-3135-4B73-B0EB-D008A31AE8FA} = {F20B2AAB-FB71-4A1C-82FE-F1750B922BF6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2B91883F-A85A-49EB-A9C1-ABD567A9375E} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..8f1e9e9 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + 6.0.12 + 7.0.1 + + + \ No newline at end of file diff --git a/README.md b/README.md index fa8fafb..3d1e274 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,50 @@ +[![Nuget version](https://img.shields.io/nuget/v/Blazored.SessionStorage.svg?logo=nuget)](https://www.nuget.org/packages/Blazored.SessionStorage/) +[![Nuget downloads](https://img.shields.io/nuget/dt/Blazored.SessionStorage?logo=nuget)](https://www.nuget.org/packages/Blazored.SessionStorage/) +![Build & Test Main](https://github.com/Blazored/SessionStorage/workflows/Build%20&%20Test%20Main/badge.svg) + # Blazored SessionStorage -A library to provide access to session storage in Blazor applications +Blazored SessionStorage is a library that provides access to the browsers session storage APIs for Blazor applications. An additional benefit of using this library is that it will handle serializing and deserializing values when saving or retrieving them. + +## Breaking Change (v1 > v2) + +### JsonSerializerOptions +From v4 onwards we use the default the `JsonSerializerOptions` for `System.Text.Json` instead of using custom ones. This will cause values saved to session storage with v3 to break things. +To retain the old settings use the following configuration when adding Blazored SessionStorage to the DI container: + +```csharp +builder.Services.AddBlazoredSessionStorage(config => { + config.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase; + config.JsonSerializerOptions.IgnoreNullValues = true; + config.JsonSerializerOptions.IgnoreReadOnlyProperties = true; + config.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + config.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip; + config.JsonSerializerOptions.WriteIndented = false; + } +); +``` -[![Build Status](https://dev.azure.com/blazored/SessionStorage/_apis/build/status/Blazored.SessionStorage?branchName=master)](https://dev.azure.com/blazored/SessionStorage/_build/latest?definitionId=1&branchName=master) +### SetItem[Async] method now serializes string values +Prior to v2 we bypassed the serialization of string values as it seemed pointless as string can be stored directly. However, this led to some edge cases where nullable strings were being saved as the string `"null"`. Then when retrieved, instead of being null the value was `"null"`. By serializing strings this issue is taken care of. +For those who wish to save raw string values, a new method `SetValueAsString[Async]` is available. This will save a string value without attempting to serialize it and will throw an exception if a null string is attempted to be saved. -[![Nuget](https://img.shields.io/nuget/v/blazored.sessionstorage.svg)](https://www.nuget.org/packages/Blazored.SessionStorage/) +## Installing -### Installing +To install the package add the following line to you csproj file replacing x.x.x with the latest version number (found at the top of this file): -You can install from NuGet using the following command: +``` + +``` + +You can also install via the .NET CLI with the following command: -`Install-Package Blazored.SessionStorage` +``` +dotnet add package Blazored.SessionStorage +``` -Or via the Visual Studio package manager. +If you're using Jetbrains Rider or Visual Studio you can also install via the built in NuGet package manager. -### Setup +## Setup You will need to register the session storage services with the service collection in your _Startup.cs_ file in Blazor Server. @@ -38,34 +69,18 @@ public static async Task Main(string[] args) } ``` -### Configuration +### Registering services as Singleton - Blazor WebAssembly **ONLY** +99% of developers will want to register Blazored SessionStorage using the method described above. However, in some very specific scenarios, +developers may have a need to register services as Singleton as apposed to Scoped. This is possible by using the following method: -The session storage provides options that can be modified by you at registration in your _Startup.cs_ file in Blazor Server. - - -```c# -public void ConfigureServices(IServiceCollection services) -{ - services.AddBlazoredSessionStorage(config => - config.JsonSerializerOptions.WriteIndented = true); -} +```csharp +builder.Services.AddBlazoredSessionStorageAsSingleton(); ``` -Or in your _Program.cs_ file in Blazor WebAssembly. -```c# -public static async Task Main(string[] args) -{ - var builder = WebAssemblyHostBuilder.CreateDefault(args); - builder.RootComponents.Add("app"); +**This method will not work with Blazor Server applications as Blazor's JS interop services are registered as Scoped and cannot be injected into Singletons.** - builder.Services.AddBlazoredSessionStorage(config => - config.JsonSerializerOptions.WriteIndented = true); - - await builder.Build().RunAsync(); -} -``` -### Usage (Blazor WebAssembly) +## Usage (Blazor WebAssembly) To use Blazored.SessionStorage in Blazor WebAssembly, inject the `ISessionStorageService` per the example below. ```c# @@ -98,7 +113,7 @@ With Blazor WebAssembly you also have the option of a synchronous API, if your u } ``` -### Usage (Blazor Server) +## Usage (Blazor Server) **NOTE:** Due to pre-rendering in Blazor Server you can't perform any JS interop until the `OnAfterRender` lifecycle method. @@ -120,20 +135,103 @@ The APIs available are: - asynchronous via `ISessionStorageService`: - SetItemAsync() + - SetItemAsStringAsync() - GetItemAsync() + - GetItemAsStringAsync() - RemoveItemAsync() + - RemoveItemsAsync() - ClearAsync() - LengthAsync() - KeyAsync() - - ContainsKeyAsync() + - KeysAsync() + - ContainKeyAsync() - synchronous via `ISyncSessionStorageService` (Synchronous methods are **only** available in Blazor WebAssembly): - SetItem() + - SetItemAsString() - GetItem() + - GetItemAsString() - RemoveItem() + - RemovesItem() - Clear() - Length() - Key() - - ContainsKey() + - Keys() + - ContainKey() + +**Note:** Blazored.SessionStorage methods will handle the serialisation and de-serialisation of the data for you, the exception is the `GetItemAsString[Async]` method which will return the raw string value from session storage. + +## Configuring JSON Serializer Options +You can configure the options for the default serializer (System.Text.Json) when calling the `AddBlazoredSessionStorage` method to register services. + +```c# +public static async Task Main(string[] args) +{ + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); + + builder.Services.AddBlazoredSessionStorage(config => + config.JsonSerializerOptions.WriteIndented = true + ); + + await builder.Build().RunAsync(); +} +``` + +## Using a custom JSON serializer +By default, the library uses `System.Text.Json`. If you prefer to use a different JSON library for serialization--or if you want to add some custom logic when serializing or deserializing--you can provide your own serializer which implements the `Blazored.SessionStorage.Serialization.IJsonSerializer` interface. + +To register your own serializer in place of the default one, you can do the following: -**Note:** Blazored.SessionStorage methods will handle the serialisation and de-serialisation of the data for you. +```csharp +builder.Services.AddBlazoredSessionStorage(); +builder.Services.Replace(ServiceDescriptor.Scoped()); +``` + +You can find an example of this in the Blazor Server sample project. The standard serializer has been replaced with a new serializer which uses NewtonsoftJson. + +## Testing with bUnit +This library provides test extensions for use with the [bUnit testing library](https://bunit.dev/). Using these test extensions will provide an in memory implementation which mimics session storage allowing more realistic testing of your components. + +### Installing + +To install the package add the following line to you csproj file replacing x.x.x with the latest version number (found at the top of this file): + +``` + +``` + +You can also install via the .NET CLI with the following command: + +``` +dotnet add package Blazored.SessionStorage.TestExtensions +``` + +If you're using Jetbrains Rider or Visual Studio you can also install via the built in NuGet package manager. + +### Usage example + +Below is an example test which uses these extensions. You can find an example project which shows this code in action in the samples folder. + +```c# +public class IndexPageTests : TestContext +{ + [Fact] + public async Task SavesNameToSessionStorage() + { + // Arrange + const string inputName = "John Smith"; + var sessionStorage = this.AddBlazoredSessionStorage(); + var cut = RenderComponent(); + + // Act + cut.Find("#Name").Change(inputName); + cut.Find("#NameButton").Click(); + + // Assert + var name = await sessionStorage.GetItemAsync("name"); + + Assert.Equal(inputName, name); + } +} +``` diff --git a/samples/BlazorServerSide/App.razor b/samples/BlazorServer/App.razor similarity index 62% rename from samples/BlazorServerSide/App.razor rename to samples/BlazorServer/App.razor index 0c89788..b941644 100644 --- a/samples/BlazorServerSide/App.razor +++ b/samples/BlazorServer/App.razor @@ -1,8 +1,4 @@ -@* - The Router component displays whichever component has a @page - directive matching the current URI. -*@ - + @@ -11,4 +7,4 @@

Sorry, there's nothing at this address.

-
\ No newline at end of file +
diff --git a/samples/BlazorServerSide/BlazorServerSide.csproj b/samples/BlazorServer/BlazorServer.csproj similarity index 60% rename from samples/BlazorServerSide/BlazorServerSide.csproj rename to samples/BlazorServer/BlazorServer.csproj index 0728b6a..98d95a3 100644 --- a/samples/BlazorServerSide/BlazorServerSide.csproj +++ b/samples/BlazorServer/BlazorServer.csproj @@ -1,11 +1,13 @@  - netcoreapp3.0 - 7.3 - true + net7.0 + + + + diff --git a/samples/BlazorServer/Pages/Error.cshtml b/samples/BlazorServer/Pages/Error.cshtml new file mode 100644 index 0000000..5f5a016 --- /dev/null +++ b/samples/BlazorServer/Pages/Error.cshtml @@ -0,0 +1,42 @@ +@page +@model WebApplication2.Pages.ErrorModel + + + + + + + + Error + + + + + +
+
+

Error.

+

An error occurred while processing your request.

+ + @if (Model.ShowRequestId) + { +

+ Request ID: @Model.RequestId +

+ } + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+
+
+ + + diff --git a/samples/BlazorServer/Pages/Error.cshtml.cs b/samples/BlazorServer/Pages/Error.cshtml.cs new file mode 100644 index 0000000..67ebdb8 --- /dev/null +++ b/samples/BlazorServer/Pages/Error.cshtml.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +namespace WebApplication2.Pages +{ + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [IgnoreAntiforgeryToken] + public class ErrorModel : PageModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + private readonly ILogger _logger; + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} diff --git a/samples/BlazorServerSide/Pages/Index.razor b/samples/BlazorServer/Pages/Index.razor similarity index 79% rename from samples/BlazorServerSide/Pages/Index.razor rename to samples/BlazorServer/Pages/Index.razor index 86c0409..d0e96fb 100644 --- a/samples/BlazorServerSide/Pages/Index.razor +++ b/samples/BlazorServer/Pages/Index.razor @@ -1,5 +1,4 @@ @page "/" -@using Microsoft.AspNetCore.Components.Web @inject ISessionStorageService sessionStorage

Blazored SessionStorage Sample

@@ -7,7 +6,7 @@
- +
Add Item to session storage
@@ -40,27 +39,33 @@
Items in session storage
@ItemsInSessionStorage
+
+
Item exist in session storage
@(ItemExist) +
@code { - string NameFromSessionStorage { get; set; } - int ItemsInSessionStorage { get; set; } - string Name { get; set; } + string NameFromSessionStorage { get; set; } + int ItemsInSessionStorage { get; set; } + string Name { get; set; } + bool ItemExist { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - await GetNameFromSessionStorage(); - await GetSessionStorageLength(); - - sessionStorage.Changed += (sender, e) => - { - Console.WriteLine($"Value for key {e.Key} changed from {e.OldValue} to {e.NewValue}"); - }; - - StateHasChanged(); + { + if (firstRender) + { + await GetNameFromSessionStorage(); + await GetSessionStorageLength(); + + sessionStorage.Changed += (sender, e) => + { + Console.WriteLine($"Value for key {e.Key} changed from {e.OldValue} to {e.NewValue}"); + }; + await TestTimespan(); + + + StateHasChanged(); } } @@ -104,6 +109,17 @@ { Console.WriteLine(await sessionStorage.LengthAsync()); ItemsInSessionStorage = await sessionStorage.LengthAsync(); + ItemExist = await sessionStorage.ContainKeyAsync("name"); + } + + async Task TestTimespan() + { + var timespan = await sessionStorage.GetItemAsync("timespan"); + if(timespan == TimeSpan.Zero) + { + await sessionStorage.SetItemAsync("timespan", new TimeSpan(0,15,0)); + timespan = await sessionStorage.GetItemAsync("timespan"); + } } } diff --git a/samples/BlazorServer/Pages/_Host.cshtml b/samples/BlazorServer/Pages/_Host.cshtml new file mode 100644 index 0000000..a136266 --- /dev/null +++ b/samples/BlazorServer/Pages/_Host.cshtml @@ -0,0 +1,32 @@ +@page "/" +@namespace BlazorServer.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + Blazored SessionStorage (Server) + + + + + + + + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + diff --git a/samples/BlazorServerSide/Program.cs b/samples/BlazorServer/Program.cs similarity index 66% rename from samples/BlazorServerSide/Program.cs rename to samples/BlazorServer/Program.cs index 23a8797..1f2b71a 100644 --- a/samples/BlazorServerSide/Program.cs +++ b/samples/BlazorServer/Program.cs @@ -1,15 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -namespace BlazorServerSide +namespace BlazorServer { public class Program { diff --git a/samples/BlazorServer/Shared/MainLayout.razor b/samples/BlazorServer/Shared/MainLayout.razor new file mode 100644 index 0000000..2281c4f --- /dev/null +++ b/samples/BlazorServer/Shared/MainLayout.razor @@ -0,0 +1,17 @@ +@inherits LayoutComponentBase + +
+ + +
+
+ About +
+ +
+ @Body +
+
+
diff --git a/samples/BlazorServer/Shared/MainLayout.razor.css b/samples/BlazorServer/Shared/MainLayout.razor.css new file mode 100644 index 0000000..4ad9519 --- /dev/null +++ b/samples/BlazorServer/Shared/MainLayout.razor.css @@ -0,0 +1,70 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +.main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + } + + .top-row a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } + + .top-row a, .top-row .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .main > div { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/samples/BlazorServerSide/Shared/NavMenu.razor b/samples/BlazorServer/Shared/NavMenu.razor similarity index 100% rename from samples/BlazorServerSide/Shared/NavMenu.razor rename to samples/BlazorServer/Shared/NavMenu.razor diff --git a/samples/BlazorServer/Shared/NavMenu.razor.css b/samples/BlazorServer/Shared/NavMenu.razor.css new file mode 100644 index 0000000..f7f4dbe --- /dev/null +++ b/samples/BlazorServer/Shared/NavMenu.razor.css @@ -0,0 +1,62 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.oi { + width: 2rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + + .nav-item ::deep a.active { + background-color: rgba(255,255,255,0.25); + color: white; + } + + .nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; + } + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } +} diff --git a/samples/BlazorServerSide/Startup.cs b/samples/BlazorServer/Startup.cs similarity index 62% rename from samples/BlazorServerSide/Startup.cs rename to samples/BlazorServer/Startup.cs index 64fb58b..afb1033 100644 --- a/samples/BlazorServerSide/Startup.cs +++ b/samples/BlazorServer/Startup.cs @@ -3,18 +3,31 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Blazored.SessionStorage; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Blazored.SessionStorage.Serialization; +using Newtonsoft.Json; +using System; -namespace BlazorServerSide +namespace BlazorServer { public class Startup { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); - services.AddServerSideBlazor(); + services.AddServerSideBlazor(config => config.DetailedErrors = true); services.AddBlazoredSessionStorage(); + services.Replace(ServiceDescriptor.Scoped()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -26,12 +39,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } else { + app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); - app.UseStaticFiles(); app.UseRouting(); @@ -43,4 +56,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) }); } } + + public class NewtonSoftJsonSerializer : IJsonSerializer + { + public T Deserialize(string text) + =>JsonConvert.DeserializeObject(text); + + public string Serialize(T obj) + => JsonConvert.SerializeObject(obj); + } } diff --git a/samples/BlazorServerSide/_Imports.razor b/samples/BlazorServer/_Imports.razor similarity index 51% rename from samples/BlazorServerSide/_Imports.razor rename to samples/BlazorServer/_Imports.razor index 020af93..f71dbdc 100644 --- a/samples/BlazorServerSide/_Imports.razor +++ b/samples/BlazorServer/_Imports.razor @@ -1,8 +1,12 @@ @using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop -@using BlazorServerSide.Shared +@using BlazorServer +@using BlazorServer.Shared @using Blazored.SessionStorage diff --git a/samples/BlazorServerSide/appsettings.Development.json b/samples/BlazorServer/appsettings.Development.json similarity index 100% rename from samples/BlazorServerSide/appsettings.Development.json rename to samples/BlazorServer/appsettings.Development.json diff --git a/samples/BlazorServerSide/appsettings.json b/samples/BlazorServer/appsettings.json similarity index 100% rename from samples/BlazorServerSide/appsettings.json rename to samples/BlazorServer/appsettings.json diff --git a/samples/BlazorServerSide/wwwroot/css/bootstrap/bootstrap.min.css b/samples/BlazorServer/wwwroot/css/bootstrap/bootstrap.min.css similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/bootstrap/bootstrap.min.css rename to samples/BlazorServer/wwwroot/css/bootstrap/bootstrap.min.css diff --git a/samples/BlazorServerSide/wwwroot/css/bootstrap/bootstrap.min.css.map b/samples/BlazorServer/wwwroot/css/bootstrap/bootstrap.min.css.map similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/bootstrap/bootstrap.min.css.map rename to samples/BlazorServer/wwwroot/css/bootstrap/bootstrap.min.css.map diff --git a/samples/BlazorServerSide/wwwroot/css/open-iconic/FONT-LICENSE b/samples/BlazorServer/wwwroot/css/open-iconic/FONT-LICENSE similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/open-iconic/FONT-LICENSE rename to samples/BlazorServer/wwwroot/css/open-iconic/FONT-LICENSE diff --git a/samples/BlazorServerSide/wwwroot/css/open-iconic/ICON-LICENSE b/samples/BlazorServer/wwwroot/css/open-iconic/ICON-LICENSE similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/open-iconic/ICON-LICENSE rename to samples/BlazorServer/wwwroot/css/open-iconic/ICON-LICENSE diff --git a/samples/BlazorServerSide/wwwroot/css/open-iconic/README.md b/samples/BlazorServer/wwwroot/css/open-iconic/README.md similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/open-iconic/README.md rename to samples/BlazorServer/wwwroot/css/open-iconic/README.md diff --git a/samples/BlazorServerSide/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/samples/BlazorServer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css rename to samples/BlazorServer/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css diff --git a/samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.eot rename to samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.eot diff --git a/samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.otf rename to samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.otf diff --git a/samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.svg similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.svg rename to samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.svg diff --git a/samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf rename to samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf diff --git a/samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff similarity index 100% rename from samples/BlazorServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.woff rename to samples/BlazorServer/wwwroot/css/open-iconic/font/fonts/open-iconic.woff diff --git a/samples/BlazorServer/wwwroot/css/site.css b/samples/BlazorServer/wwwroot/css/site.css new file mode 100644 index 0000000..caebf2a --- /dev/null +++ b/samples/BlazorServer/wwwroot/css/site.css @@ -0,0 +1,50 @@ +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +a, .btn-link { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/samples/BlazorServerSide/wwwroot/favicon.ico b/samples/BlazorServer/wwwroot/favicon.ico similarity index 100% rename from samples/BlazorServerSide/wwwroot/favicon.ico rename to samples/BlazorServer/wwwroot/favicon.ico diff --git a/samples/BlazorServerSide/Pages/_Host.cshtml b/samples/BlazorServerSide/Pages/_Host.cshtml deleted file mode 100644 index 469597c..0000000 --- a/samples/BlazorServerSide/Pages/_Host.cshtml +++ /dev/null @@ -1,29 +0,0 @@ -@page "/" -@namespace BlazorServerSide.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - - - - - - - Blazored LocalStorage - Server-Side - - - - - - - - - - - @(await Html.RenderComponentAsync(RenderMode.Server)) - - - - diff --git a/samples/BlazorServerSide/Pages/_Imports.razor b/samples/BlazorServerSide/Pages/_Imports.razor deleted file mode 100644 index 0f24eda..0000000 --- a/samples/BlazorServerSide/Pages/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@layout MainLayout diff --git a/samples/BlazorServerSide/Shared/MainLayout.razor b/samples/BlazorServerSide/Shared/MainLayout.razor deleted file mode 100644 index 97e764d..0000000 --- a/samples/BlazorServerSide/Shared/MainLayout.razor +++ /dev/null @@ -1,15 +0,0 @@ -@inherits LayoutComponentBase - - - -
-
- About -
- -
- @Body -
-
diff --git a/samples/BlazorServerSide/wwwroot/css/site.css b/samples/BlazorServerSide/wwwroot/css/site.css deleted file mode 100644 index 8b2f196..0000000 --- a/samples/BlazorServerSide/wwwroot/css/site.css +++ /dev/null @@ -1,154 +0,0 @@ -@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); - -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -app { - position: relative; - display: flex; - flex-direction: column; -} - -.top-row { - height: 3.5rem; - display: flex; - align-items: center; -} - -.main { - flex: 1; -} - - .main .top-row { - background-color: #e6e6e6; - border-bottom: 1px solid #d6d5d5; - } - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - - .sidebar .top-row { - background-color: rgba(0,0,0,0.4); - } - - .sidebar .navbar-brand { - font-size: 1.1rem; - } - - .sidebar .oi { - width: 2rem; - font-size: 1.1rem; - vertical-align: text-top; - top: -2px; - } - -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; -} - - .nav-item:first-of-type { - padding-top: 1rem; - } - - .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .nav-item a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - } - - .nav-item a.active { - background-color: rgba(255,255,255,0.25); - color: white; - } - - .nav-item a:hover { - background-color: rgba(255,255,255,0.1); - color: white; - } - -.content { - padding-top: 1.1rem; -} - -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); -} - -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid red; -} - -.validation-message { - color: red; -} - -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(83, 64, 64, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } - -@media (max-width: 767.98px) { - .main .top-row { - display: none; - } -} - -@media (min-width: 768px) { - app { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .main .top-row { - position: sticky; - top: 0; - } - - .main > div { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } - - .navbar-toggler { - display: none; - } - - .sidebar .collapse { - /* Never collapse the sidebar for wide screens */ - display: block; - } -} diff --git a/samples/BlazorWebAssembly/App.razor b/samples/BlazorWebAssembly/App.razor index 9f62581..b941644 100644 --- a/samples/BlazorWebAssembly/App.razor +++ b/samples/BlazorWebAssembly/App.razor @@ -1,10 +1,10 @@ - + - + - -

Sorry, but there's nothing here!

+ +

Sorry, there's nothing at this address.

-
\ No newline at end of file +
diff --git a/samples/BlazorWebAssembly/BlazorWebAssembly.csproj b/samples/BlazorWebAssembly/BlazorWebAssembly.csproj index 144cb19..f1d4e0d 100644 --- a/samples/BlazorWebAssembly/BlazorWebAssembly.csproj +++ b/samples/BlazorWebAssembly/BlazorWebAssembly.csproj @@ -1,15 +1,13 @@ - + - netstandard2.1 - 3.0 + net7.0 - - - - + + + diff --git a/samples/BlazorWebAssembly/Pages/Index.razor b/samples/BlazorWebAssembly/Pages/Index.razor index b94913b..31293dc 100644 --- a/samples/BlazorWebAssembly/Pages/Index.razor +++ b/samples/BlazorWebAssembly/Pages/Index.razor @@ -10,9 +10,9 @@
Add Item to session storage
- +
- +
@@ -28,7 +28,7 @@
-
+
Value Read from session storage
@@ -39,17 +39,32 @@
Items in session storage
@ItemsInSessionStorage
+ +
+
Item exist in session storage
@(ItemExist) +
+
+ +
+ +
+
String Read from session storage
+ @StringFromSessionStorage +
@code { string NameFromSessionStorage { get; set; } + string StringFromSessionStorage { get; set; } int ItemsInSessionStorage { get; set; } string Name { get; set; } + bool ItemExist { get; set; } protected override async Task OnInitializedAsync() { await GetNameFromSessionStorage(); + await GetStringFromSessionStorage(); await GetSessionStorageLength(); sessionStorage.Changed += (sender, e) => @@ -62,6 +77,7 @@ { await sessionStorage.SetItemAsync("name", Name); await GetNameFromSessionStorage(); + await GetStringFromSessionStorage(); await GetSessionStorageLength(); Name = ""; @@ -69,11 +85,29 @@ async Task GetNameFromSessionStorage() { - NameFromSessionStorage = await sessionStorage.GetItemAsync("name"); + try + { + NameFromSessionStorage = await sessionStorage.GetItemAsync("name"); + + if (string.IsNullOrEmpty(NameFromSessionStorage)) + { + NameFromSessionStorage = "Nothing Saved"; + } + } + catch(Exception) + { + Console.WriteLine("error reading 'name'"); + } + } + + + async Task GetStringFromSessionStorage() + { + StringFromSessionStorage = await sessionStorage.GetItemAsStringAsync("name"); - if (string.IsNullOrEmpty(NameFromSessionStorage)) + if (string.IsNullOrEmpty(StringFromSessionStorage)) { - NameFromSessionStorage = "Nothing Saved"; + StringFromSessionStorage = "Nothing Saved"; } } @@ -81,6 +115,7 @@ { await sessionStorage.RemoveItemAsync("name"); await GetNameFromSessionStorage(); + await GetStringFromSessionStorage(); await GetSessionStorageLength(); } @@ -90,6 +125,7 @@ await sessionStorage.ClearAsync(); Console.WriteLine("Getting name from session storage..."); await GetNameFromSessionStorage(); + await GetStringFromSessionStorage(); Console.WriteLine("Calling Get Length..."); await GetSessionStorageLength(); } @@ -98,6 +134,7 @@ { Console.WriteLine(await sessionStorage.LengthAsync()); ItemsInSessionStorage = await sessionStorage.LengthAsync(); + ItemExist = await sessionStorage.ContainKeyAsync("name"); } } diff --git a/samples/BlazorWebAssembly/Pages/Sync.razor b/samples/BlazorWebAssembly/Pages/Sync.razor index d625680..59a5f03 100644 --- a/samples/BlazorWebAssembly/Pages/Sync.razor +++ b/samples/BlazorWebAssembly/Pages/Sync.razor @@ -37,17 +37,31 @@
Items in session storage
@ItemsInSessionStorage
+
+
Item exist in session storage
@(ItemExist) +
+ + +
+ +
+
String Read from session storage
+ @StringFromSessionStorage +
@code { string NameFromSessionStorage { get; set; } + string StringFromSessionStorage { get; set; } int ItemsInSessionStorage { get; set; } string Name { get; set; } + bool ItemExist { get; set; } protected override void OnInitialized() { GetNameFromSessionStorage(); + GetStringFromSessionStorage(); GetSessionStorageLength(); sessionStorage.Changed += (sender, e) => @@ -58,8 +72,10 @@ void SaveName() { + Console.WriteLine(Name); sessionStorage.SetItem("name", Name); GetNameFromSessionStorage(); + GetStringFromSessionStorage(); GetSessionStorageLength(); Name = ""; @@ -67,11 +83,28 @@ void GetNameFromSessionStorage() { - NameFromSessionStorage = sessionStorage.GetItem("name"); + try + { + NameFromSessionStorage = sessionStorage.GetItem("name"); + + if (string.IsNullOrEmpty(NameFromSessionStorage)) + { + NameFromSessionStorage = "Nothing Saved"; + } + } + catch (Exception) + { + Console.WriteLine("error reading 'name'"); + } + } + + void GetStringFromSessionStorage() + { + StringFromSessionStorage = sessionStorage.GetItemAsString("name"); - if (string.IsNullOrEmpty(NameFromSessionStorage)) + if (string.IsNullOrEmpty(StringFromSessionStorage)) { - NameFromSessionStorage = "Nothing Saved"; + StringFromSessionStorage = "Nothing Saved"; } } @@ -79,6 +112,7 @@ { sessionStorage.RemoveItem("name"); GetNameFromSessionStorage(); + GetStringFromSessionStorage(); GetSessionStorageLength(); } @@ -88,6 +122,7 @@ sessionStorage.Clear(); Console.WriteLine("Getting name from session storage..."); GetNameFromSessionStorage(); + GetStringFromSessionStorage(); Console.WriteLine("Calling Get Length..."); GetSessionStorageLength(); } @@ -96,6 +131,7 @@ { Console.WriteLine(sessionStorage.Length()); ItemsInSessionStorage = sessionStorage.Length(); + ItemExist = sessionStorage.ContainKey("name"); } } diff --git a/samples/BlazorWebAssembly/Program.cs b/samples/BlazorWebAssembly/Program.cs index 3bf7221..0fff24f 100644 --- a/samples/BlazorWebAssembly/Program.cs +++ b/samples/BlazorWebAssembly/Program.cs @@ -1,6 +1,6 @@ -using System.Threading.Tasks; +using Blazored.SessionStorage; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Blazored.SessionStorage; +using System.Threading.Tasks; namespace BlazorWebAssembly { @@ -9,7 +9,8 @@ public class Program public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); - builder.RootComponents.Add("app"); + builder.RootComponents.Add("#app"); + builder.Services.AddBlazoredSessionStorage(); await builder.Build().RunAsync(); diff --git a/samples/BlazorWebAssembly/Shared/MainLayout.razor b/samples/BlazorWebAssembly/Shared/MainLayout.razor index 902e3ed..a76e097 100644 --- a/samples/BlazorWebAssembly/Shared/MainLayout.razor +++ b/samples/BlazorWebAssembly/Shared/MainLayout.razor @@ -1,15 +1,17 @@ @inherits LayoutComponentBase - - -
-
- About +
+ -
- @Body +
+
+ About +
+ +
+ @Body +
diff --git a/samples/BlazorWebAssembly/Shared/MainLayout.razor.css b/samples/BlazorWebAssembly/Shared/MainLayout.razor.css new file mode 100644 index 0000000..4ad9519 --- /dev/null +++ b/samples/BlazorWebAssembly/Shared/MainLayout.razor.css @@ -0,0 +1,70 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +.main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + } + + .top-row a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row:not(.auth) { + display: none; + } + + .top-row.auth { + justify-content: space-between; + } + + .top-row a, .top-row .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .main > div { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} diff --git a/samples/BlazorWebAssembly/Shared/NavMenu.razor b/samples/BlazorWebAssembly/Shared/NavMenu.razor index 2a22094..4869261 100644 --- a/samples/BlazorWebAssembly/Shared/NavMenu.razor +++ b/samples/BlazorWebAssembly/Shared/NavMenu.razor @@ -5,7 +5,7 @@
-
+
@code { - bool collapseNavMenu = true; + private bool collapseNavMenu = true; - void ToggleNavMenu() + private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } diff --git a/samples/BlazorWebAssembly/Shared/NavMenu.razor.css b/samples/BlazorWebAssembly/Shared/NavMenu.razor.css new file mode 100644 index 0000000..f7f4dbe --- /dev/null +++ b/samples/BlazorWebAssembly/Shared/NavMenu.razor.css @@ -0,0 +1,62 @@ +.navbar-toggler { + background-color: rgba(255, 255, 255, 0.1); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.oi { + width: 2rem; + font-size: 1.1rem; + vertical-align: text-top; + top: -2px; +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + } + + .nav-item ::deep a.active { + background-color: rgba(255,255,255,0.25); + color: white; + } + + .nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; + } + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .collapse { + /* Never collapse the sidebar for wide screens */ + display: block; + } +} diff --git a/samples/BlazorWebAssembly/_Imports.razor b/samples/BlazorWebAssembly/_Imports.razor index 07e96c3..baf3f18 100644 --- a/samples/BlazorWebAssembly/_Imports.razor +++ b/samples/BlazorWebAssembly/_Imports.razor @@ -1,7 +1,10 @@ @using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop @using BlazorWebAssembly @using BlazorWebAssembly.Shared diff --git a/samples/BlazorWebAssembly/wwwroot/css/app.css b/samples/BlazorWebAssembly/wwwroot/css/app.css new file mode 100644 index 0000000..82fc22a --- /dev/null +++ b/samples/BlazorWebAssembly/wwwroot/css/app.css @@ -0,0 +1,50 @@ +@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); + +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +a, .btn-link { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.content { + padding-top: 1.1rem; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/samples/BlazorWebAssembly/wwwroot/css/site.css b/samples/BlazorWebAssembly/wwwroot/css/site.css deleted file mode 100644 index 7338534..0000000 --- a/samples/BlazorWebAssembly/wwwroot/css/site.css +++ /dev/null @@ -1,123 +0,0 @@ -@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); - -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -app { - position: relative; - display: flex; - flex-direction: column; -} - -.top-row { - height: 3.5rem; - display: flex; - align-items: center; -} - -.main { - flex: 1; -} - - .main .top-row { - background-color: #e6e6e6; - border-bottom: 1px solid #d6d5d5; - } - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - - .sidebar .top-row { - background-color: rgba(0,0,0,0.4); - } - - .sidebar .navbar-brand { - font-size: 1.1rem; - } - - .sidebar .oi { - width: 2rem; - font-size: 1.1rem; - vertical-align: text-top; - top: -2px; - } - -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; -} - - .nav-item:first-of-type { - padding-top: 1rem; - } - - .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .nav-item a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - } - - .nav-item a.active { - background-color: rgba(255,255,255,0.25); - color: white; - } - - .nav-item a:hover { - background-color: rgba(255,255,255,0.1); - color: white; - } - -.content { - padding-top: 1.1rem; -} - -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); -} - -@media (max-width: 767.98px) { - .main .top-row { - display: none; - } -} - -@media (min-width: 768px) { - app { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .main .top-row { - position: sticky; - top: 0; - } - - .main > div { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } - - .navbar-toggler { - display: none; - } - - .sidebar .collapse { - /* Never collapse the sidebar for wide screens */ - display: block; - } -} diff --git a/samples/BlazorWebAssembly/wwwroot/index.html b/samples/BlazorWebAssembly/wwwroot/index.html index bc8f750..980817a 100644 --- a/samples/BlazorWebAssembly/wwwroot/index.html +++ b/samples/BlazorWebAssembly/wwwroot/index.html @@ -1,13 +1,14 @@  - - Blazored SessionStorage + Blazored SessionStorage (Wasm) + - + + - - Loading... +
Loading...
An unhandled error has occurred. @@ -75,5 +75,4 @@ }); - - \ No newline at end of file + diff --git a/samples/bUnitExample/Counter.razor b/samples/bUnitExample/Counter.razor new file mode 100644 index 0000000..cb6f9e1 --- /dev/null +++ b/samples/bUnitExample/Counter.razor @@ -0,0 +1,16 @@ +

Counter

+ +

+ Current count: @currentCount +

+ + + +@code { + int currentCount = 0; + + void IncrementCount() + { + currentCount++; + } +} diff --git a/samples/bUnitExample/IndexPageTests.cs b/samples/bUnitExample/IndexPageTests.cs new file mode 100644 index 0000000..31c1492 --- /dev/null +++ b/samples/bUnitExample/IndexPageTests.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Bunit; +using Xunit; + +namespace bUnitExample +{ + public class IndexPageTests : TestContext + { + [Fact] + public async Task SavesNameToSessionStorage() + { + // Arrange + const string inputName = "John Smith"; + var sessionStorage = this.AddBlazoredSessionStorage(); + var cut = RenderComponent(); + + // Act + cut.Find("#Name").Change(inputName); + cut.Find("#NameButton").Click(); + + // Assert + var name = await sessionStorage.GetItemAsync("name"); + + Assert.Equal(inputName, name); + } + } +} diff --git a/samples/bUnitExample/_Imports.razor b/samples/bUnitExample/_Imports.razor new file mode 100644 index 0000000..5703c20 --- /dev/null +++ b/samples/bUnitExample/_Imports.razor @@ -0,0 +1,6 @@ +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using Microsoft.Extensions.DependencyInjection +@using Bunit +@using Bunit.TestDoubles +@using Xunit diff --git a/samples/bUnitExample/bUnitExample.csproj b/samples/bUnitExample/bUnitExample.csproj new file mode 100644 index 0000000..5d0efb7 --- /dev/null +++ b/samples/bUnitExample/bUnitExample.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + \ No newline at end of file diff --git a/src/Blazored.SessionStorage.TestExtensions/BUnitSessionStorageTestExtensions.cs b/src/Blazored.SessionStorage.TestExtensions/BUnitSessionStorageTestExtensions.cs new file mode 100644 index 0000000..4de3433 --- /dev/null +++ b/src/Blazored.SessionStorage.TestExtensions/BUnitSessionStorageTestExtensions.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Blazored.SessionStorage; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Bunit +{ + [ExcludeFromCodeCoverage] + public static class BUnitSessionStorageTestExtensions + + { + public static ISessionStorageService AddBlazoredSessionStorage(this TestContextBase context) + => AddBlazoredSessionStorage(context, null); + + public static ISessionStorageService AddBlazoredSessionStorage(this TestContextBase context, Action configure) + { + if (context is null) + throw new ArgumentNullException(nameof(context)); + + var sessionStorageOptions = new SessionStorageOptions(); + configure?.Invoke(sessionStorageOptions); + sessionStorageOptions.JsonSerializerOptions.Converters.Add(new TimespanJsonConverter()); + + var localStorageService = new SessionStorageService(new InMemoryStorageProvider(), new SystemTextJsonSerializer(sessionStorageOptions)); + context.Services.AddSingleton(localStorageService); + + return localStorageService; + } + } +} diff --git a/src/Blazored.SessionStorage.TestExtensions/Blazored.SessionStorage.TestExtensions.csproj b/src/Blazored.SessionStorage.TestExtensions/Blazored.SessionStorage.TestExtensions.csproj new file mode 100644 index 0000000..40800be --- /dev/null +++ b/src/Blazored.SessionStorage.TestExtensions/Blazored.SessionStorage.TestExtensions.csproj @@ -0,0 +1,57 @@ + + + + net6.0;net7.0 + + Chris Sainty + + Copyright 2020 (c) Chris Sainty. All rights reserved. + A testing library to provide helper extensions for Blazored.SessionStorage + true + snupkg + + + Blazored.SessionStorage.TestExtensions + Blazored;Blazor;Razor;Components;SessionStorage;Session Storage;ASP.NET Core;CSharp;Web + MIT + https://github.com/Blazored/SessionStorage + icon.png + git + https://github.com/Blazored/SessionStorage + + + true + true + true + Blazored.SessionStorage.snk + + + + + + + + + + + + + + + + + + + + + <_Parameter1> + Blazored.SessionStorage.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007527e122cc36dc13c695a4f43b7c2da3f631aed456ed309140c0d52262323e25d84bef7feddc8bd29cfe46ec652186cb10e059eedabf2ff000b977a2376a613dccfb092de6c243e0888db4c66a084124b2c1799bda4bbb2f70fed0382fc1cbdafa6dc0f4baccdc2cee55234f8a5ad76645c315523fee5352d9f01036e48b13e3 + + + + + + + + + diff --git a/src/Blazored.SessionStorage.TestExtensions/Blazored.SessionStorage.snk b/src/Blazored.SessionStorage.TestExtensions/Blazored.SessionStorage.snk new file mode 100644 index 0000000..18e2ce3 Binary files /dev/null and b/src/Blazored.SessionStorage.TestExtensions/Blazored.SessionStorage.snk differ diff --git a/src/Blazored.SessionStorage.TestExtensions/InMemoryStorageProvider.cs b/src/Blazored.SessionStorage.TestExtensions/InMemoryStorageProvider.cs new file mode 100644 index 0000000..36615c0 --- /dev/null +++ b/src/Blazored.SessionStorage.TestExtensions/InMemoryStorageProvider.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Blazored.SessionStorage.TestExtensions +{ + internal class InMemoryStorageProvider : IStorageProvider + { + private readonly Dictionary _dataStore = new(); + + public void Clear() + => _dataStore.Clear(); + + public ValueTask ClearAsync(CancellationToken cancellationToken = default) + { + _dataStore.Clear(); + return new ValueTask(Task.CompletedTask); + } + + public bool ContainKey(string key) + => _dataStore.ContainsKey(key); + + public ValueTask ContainKeyAsync(string key, CancellationToken cancellationToken = default) + => new(ContainKey(key)); + + public string GetItem(string key) + => _dataStore.TryGetValue(key, out var value) ? value : default; + + public ValueTask GetItemAsync(string key, CancellationToken cancellationToken = default) + => new(GetItem(key)); + + public string Key(int index) + => index > _dataStore.Count - 1 ? default : _dataStore.ElementAt(index).Key; + + public IEnumerable Keys() + => _dataStore.Keys.ToList(); + + public ValueTask KeyAsync(int index, CancellationToken cancellationToken = default) + => new(Key(index)); + + public ValueTask> KeysAsync(CancellationToken cancellationToken = default) + => new(_dataStore.Keys.ToList()); + + public int Length() + => _dataStore.Count; + + public ValueTask LengthAsync(CancellationToken cancellationToken = default) + => new(Length()); + + public void RemoveItem(string key) + => _dataStore.Remove(key); + + public ValueTask RemoveItemAsync(string key, CancellationToken cancellationToken = default) + { + RemoveItem(key); + return new ValueTask(Task.CompletedTask); + } + + public void RemoveItems(IEnumerable keys) + { + foreach (var key in keys) + { + _dataStore.Remove(key); + } + } + + public ValueTask RemoveItemsAsync(IEnumerable keys, CancellationToken cancellationToken = default) + { + RemoveItems(keys); + + return new ValueTask(Task.CompletedTask); + } + + public void SetItem(string key, string data) + { + if (_dataStore.ContainsKey(key)) + { + _dataStore[key] = data; + } + else + { + _dataStore.Add(key, data); + } + } + + public ValueTask SetItemAsync(string key, string data, CancellationToken cancellationToken = default) + { + SetItem(key, data); + return new ValueTask(Task.CompletedTask); + } + } +} diff --git a/src/Blazored.SessionStorage/Blazored.SessionStorage.csproj b/src/Blazored.SessionStorage/Blazored.SessionStorage.csproj index 3226b58..07ebd5d 100644 --- a/src/Blazored.SessionStorage/Blazored.SessionStorage.csproj +++ b/src/Blazored.SessionStorage/Blazored.SessionStorage.csproj @@ -1,20 +1,18 @@  - netstandard2.0 - 3.0 + net6.0;net7.0 Chris Sainty - Copyright 2020 (c) Chris Sainty. All rights reserved. - + Copyright 2020 (c) Chris Sainty. All rights reserved. A library to provide access to session storage in Blazor applications true snupkg Blazored.SessionStorage - Blazored;Blazor;Razor;Components;Session Storage;Browser Storage;ASP.NET Core;CSharp;Web + Blazored;Blazor;Razor;Components;SessionStorage;Session Storage;ASP.NET Core;CSharp;Web MIT https://github.com/Blazored/SessionStorage icon.png @@ -24,15 +22,40 @@ true true + true + Blazored.SessionStorage.snk - - + - + + + + + + + + + - + + + <_Parameter1> + Blazored.SessionStorage.TestExtensions, PublicKey=00240000048000009400000006020000002400005253413100040000010001007527e122cc36dc13c695a4f43b7c2da3f631aed456ed309140c0d52262323e25d84bef7feddc8bd29cfe46ec652186cb10e059eedabf2ff000b977a2376a613dccfb092de6c243e0888db4c66a084124b2c1799bda4bbb2f70fed0382fc1cbdafa6dc0f4baccdc2cee55234f8a5ad76645c315523fee5352d9f01036e48b13e3 + + + + + + + <_Parameter1> + Blazored.SessionStorage.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007527e122cc36dc13c695a4f43b7c2da3f631aed456ed309140c0d52262323e25d84bef7feddc8bd29cfe46ec652186cb10e059eedabf2ff000b977a2376a613dccfb092de6c243e0888db4c66a084124b2c1799bda4bbb2f70fed0382fc1cbdafa6dc0f4baccdc2cee55234f8a5ad76645c315523fee5352d9f01036e48b13e3 + + + + + \ No newline at end of file diff --git a/src/Blazored.SessionStorage/Blazored.SessionStorage.snk b/src/Blazored.SessionStorage/Blazored.SessionStorage.snk new file mode 100644 index 0000000..18e2ce3 Binary files /dev/null and b/src/Blazored.SessionStorage/Blazored.SessionStorage.snk differ diff --git a/src/Blazored.SessionStorage/BrowserStorageProvider.cs b/src/Blazored.SessionStorage/BrowserStorageProvider.cs new file mode 100644 index 0000000..883c4bb --- /dev/null +++ b/src/Blazored.SessionStorage/BrowserStorageProvider.cs @@ -0,0 +1,352 @@ +using Microsoft.JSInterop; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Blazored.SessionStorage.Exceptions; + +namespace Blazored.SessionStorage; + +internal class BrowserStorageProvider : IStorageProvider +{ + private const string StorageNotAvailableMessage = "Unable to access the browser storage. This is most likely due to the browser settings."; + + private readonly IJSRuntime _jSRuntime; + private readonly IJSInProcessRuntime _jSInProcessRuntime; + + public BrowserStorageProvider(IJSRuntime jSRuntime) + { + _jSRuntime = jSRuntime; + _jSInProcessRuntime = jSRuntime as IJSInProcessRuntime; + } + + public async ValueTask ClearAsync(CancellationToken cancellationToken = default) + { + try + { + await _jSRuntime.InvokeVoidAsync("sessionStorage.clear", cancellationToken); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public async ValueTask GetItemAsync(string key, CancellationToken cancellationToken = default) + { + try + { + return await _jSRuntime.InvokeAsync("sessionStorage.getItem", cancellationToken, key); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public async ValueTask KeyAsync(int index, CancellationToken cancellationToken = default) + { + try + { + return await _jSRuntime.InvokeAsync("sessionStorage.key", cancellationToken, index); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public async ValueTask> KeysAsync(CancellationToken cancellationToken = default) + { + try + { + return await _jSRuntime.InvokeAsync>("eval", cancellationToken, "Object.keys(sessionStorage)"); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public async ValueTask ContainKeyAsync(string key, CancellationToken cancellationToken = default) + { + try + { + return await _jSRuntime.InvokeAsync("sessionStorage.hasOwnProperty", cancellationToken, key); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public async ValueTask LengthAsync(CancellationToken cancellationToken = default) + { + try + { + return await _jSRuntime.InvokeAsync("eval", cancellationToken, "sessionStorage.length"); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public async ValueTask RemoveItemAsync(string key, CancellationToken cancellationToken = default) + { + try + { + await _jSRuntime.InvokeVoidAsync("sessionStorage.removeItem", cancellationToken, key); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public async ValueTask RemoveItemsAsync(IEnumerable keys, CancellationToken cancellationToken = default) + { + try + { + foreach (var key in keys) + { + await _jSRuntime.InvokeVoidAsync("sessionStorage.removeItem", cancellationToken, key); + } + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public async ValueTask SetItemAsync(string key, string data, CancellationToken cancellationToken = default) + { + try + { + await _jSRuntime.InvokeVoidAsync("sessionStorage.setItem", cancellationToken, key, data); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public void Clear() + { + CheckForInProcessRuntime(); + try + { + _jSInProcessRuntime.InvokeVoid("sessionStorage.clear"); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public string GetItem(string key) + { + CheckForInProcessRuntime(); + try + { + return _jSInProcessRuntime.Invoke("sessionStorage.getItem", key); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public string Key(int index) + { + CheckForInProcessRuntime(); + try + { + return _jSInProcessRuntime.Invoke("sessionStorage.key", index); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public IEnumerable Keys() + { + CheckForInProcessRuntime(); + try + { + return _jSInProcessRuntime.Invoke>("eval", "Object.keys(sessionStorage)"); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public bool ContainKey(string key) + { + CheckForInProcessRuntime(); + try + { + return _jSInProcessRuntime.Invoke("sessionStorage.hasOwnProperty", key); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public int Length() + { + CheckForInProcessRuntime(); + try + { + return _jSInProcessRuntime.Invoke("eval", "sessionStorage.length"); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public void RemoveItem(string key) + { + CheckForInProcessRuntime(); + try + { + _jSInProcessRuntime.InvokeVoid("sessionStorage.removeItem", key); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public void RemoveItems(IEnumerable keys) + { + CheckForInProcessRuntime(); + try + { + foreach (var key in keys) + { + _jSInProcessRuntime.InvokeVoid("sessionStorage.removeItem", key); + } + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + public void SetItem(string key, string data) + { + CheckForInProcessRuntime(); + try + { + _jSInProcessRuntime.InvokeVoid("sessionStorage.setItem", key, data); + } + catch (Exception exception) + { + if (IsStorageDisabledException(exception)) + { + throw new BrowserStorageDisabledException(StorageNotAvailableMessage, exception); + } + + throw; + } + } + + private void CheckForInProcessRuntime() + { + if (_jSInProcessRuntime == null) + throw new InvalidOperationException("IJSInProcessRuntime not available"); + } + + private static bool IsStorageDisabledException(Exception exception) + => exception.Message.Contains("Failed to read the 'sessionStorage' property from 'Window'"); +} diff --git a/src/Blazored.SessionStorage/ChangedEventArgs.cs b/src/Blazored.SessionStorage/ChangedEventArgs.cs index 818909c..f20fba4 100644 --- a/src/Blazored.SessionStorage/ChangedEventArgs.cs +++ b/src/Blazored.SessionStorage/ChangedEventArgs.cs @@ -1,9 +1,11 @@ -namespace Blazored.SessionStorage +using System.Diagnostics.CodeAnalysis; + +namespace Blazored.SessionStorage; + +[ExcludeFromCodeCoverage] +public class ChangedEventArgs { - public class ChangedEventArgs - { - public string Key { get; set; } - public object OldValue { get; set; } - public object NewValue { get; set; } - } + public string Key { get; set; } + public object OldValue { get; set; } + public object NewValue { get; set; } } \ No newline at end of file diff --git a/src/Blazored.SessionStorage/ChangingEventArgs.cs b/src/Blazored.SessionStorage/ChangingEventArgs.cs index 2a38e76..032c9e4 100644 --- a/src/Blazored.SessionStorage/ChangingEventArgs.cs +++ b/src/Blazored.SessionStorage/ChangingEventArgs.cs @@ -1,9 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; -namespace Blazored.SessionStorage +namespace Blazored.SessionStorage; + +[ExcludeFromCodeCoverage] +public class ChangingEventArgs : ChangedEventArgs { - public class ChangingEventArgs : ChangedEventArgs - { - public bool Cancel { get; set; } - } -} + public bool Cancel { get; set; } +} \ No newline at end of file diff --git a/src/Blazored.SessionStorage/Exceptions/BrowserStorageDisabledException.cs b/src/Blazored.SessionStorage/Exceptions/BrowserStorageDisabledException.cs new file mode 100644 index 0000000..212a658 --- /dev/null +++ b/src/Blazored.SessionStorage/Exceptions/BrowserStorageDisabledException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Blazored.SessionStorage.Exceptions; + +public class BrowserStorageDisabledException : Exception +{ + public BrowserStorageDisabledException() + { + } + + public BrowserStorageDisabledException(string message) : base(message) + { + } + + public BrowserStorageDisabledException(string message, Exception inner) : base(message, inner) + { + } +} diff --git a/src/Blazored.SessionStorage/ISessionStorageService.cs b/src/Blazored.SessionStorage/ISessionStorageService.cs index 97724f7..e35d989 100644 --- a/src/Blazored.SessionStorage/ISessionStorageService.cs +++ b/src/Blazored.SessionStorage/ISessionStorageService.cs @@ -1,23 +1,99 @@ -using System; +using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using Microsoft.JSInterop; -namespace Blazored.SessionStorage +namespace Blazored.SessionStorage; + +public interface ISessionStorageService { - public interface ISessionStorageService - { - Task ClearAsync(); + /// + /// Clears all data from session storage. + /// + /// A representing the completion of the operation. + ValueTask ClearAsync(CancellationToken cancellationToken = default); + + /// + /// Retrieve the specified data from session storage and deseralise it to the specfied type. + /// + /// A value specifying the name of the session storage slot to use + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask GetItemAsync(string key, CancellationToken cancellationToken = default); + + /// + /// Retrieve the specified data from session storage as a . + /// + /// A value specifying the name of the storage slot to use + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask GetItemAsStringAsync(string key, CancellationToken cancellationToken = default); + + /// + /// Return the name of the key at the specified . + /// + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask KeyAsync(int index, CancellationToken cancellationToken = default); + + /// + /// Returns a collection of strings representing the names of the keys in the Session storage. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask> KeysAsync(CancellationToken cancellationToken = default); - Task GetItemAsync(string key); + /// + /// Checks if the exists in session storage, but does not check its value. + /// + /// A value specifying the name of the storage slot to use + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask ContainKeyAsync(string key, CancellationToken cancellationToken = default); - Task KeyAsync(int index); + /// + /// The number of items stored in session storage. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask LengthAsync(CancellationToken cancellationToken = default); - Task LengthAsync(); + /// + /// Remove the data with the specified . + /// + /// A value specifying the name of the storage slot to use + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask RemoveItemAsync(string key, CancellationToken cancellationToken = default); + + /// + /// Removes a collection of . + /// + /// A IEnumerable collection of strings specifying the name of the storage slot to remove + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask RemoveItemsAsync(IEnumerable keys, CancellationToken cancellationToken = default); - Task RemoveItemAsync(string key); + /// + /// Sets or updates the in session storage with the specified . + /// + /// A value specifying the name of the storage slot to use + /// The data to be saved + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask SetItemAsync(string key, T data, CancellationToken cancellationToken = default); - Task SetItemAsync(string key, T data); + /// + /// Sets or updates the in session storage with the specified . Does not serialize the value before storing. + /// + /// A value specifying the name of the storage slot to use + /// The string to be saved + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. + /// A representing the completion of the operation. + ValueTask SetItemAsStringAsync(string key, string data, CancellationToken cancellationToken = default); - event EventHandler Changing; - event EventHandler Changed; - } + event EventHandler Changing; + event EventHandler Changed; } diff --git a/src/Blazored.SessionStorage/IStorageProvider.cs b/src/Blazored.SessionStorage/IStorageProvider.cs new file mode 100644 index 0000000..0bd2c7f --- /dev/null +++ b/src/Blazored.SessionStorage/IStorageProvider.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Blazored.SessionStorage; + +internal interface IStorageProvider +{ + void Clear(); + ValueTask ClearAsync(CancellationToken cancellationToken = default); + bool ContainKey(string key); + ValueTask ContainKeyAsync(string key, CancellationToken cancellationToken = default); + string GetItem(string key); + ValueTask GetItemAsync(string key, CancellationToken cancellationToken = default); + string Key(int index); + ValueTask KeyAsync(int index, CancellationToken cancellationToken = default); + IEnumerable Keys(); + ValueTask> KeysAsync(CancellationToken cancellationToken = default); + int Length(); + ValueTask LengthAsync(CancellationToken cancellationToken = default); + void RemoveItem(string key); + ValueTask RemoveItemAsync(string key, CancellationToken cancellationToken = default); + ValueTask RemoveItemsAsync(IEnumerable keys, CancellationToken cancellationToken = default); + void SetItem(string key, string data); + ValueTask SetItemAsync(string key, string data, CancellationToken cancellationToken = default); +} diff --git a/src/Blazored.SessionStorage/ISyncSessionStorageService.cs b/src/Blazored.SessionStorage/ISyncSessionStorageService.cs index a994f6a..d972a1a 100644 --- a/src/Blazored.SessionStorage/ISyncSessionStorageService.cs +++ b/src/Blazored.SessionStorage/ISyncSessionStorageService.cs @@ -1,22 +1,82 @@ -using System; +using System; +using System.Collections.Generic; -namespace Blazored.SessionStorage +namespace Blazored.SessionStorage; + +public interface ISyncSessionStorageService { - public interface ISyncSessionStorageService - { - void Clear(); + /// + /// Clears all data from session storage. + /// + void Clear(); + + /// + /// Retrieve the specified data from session storage as a . + /// + /// A value specifying the name of the session storage slot to use + /// The data from the specified as a + T GetItem(string key); + + /// + /// Retrieve the specified data from session storage as a . + /// + /// A value specifying the name of the storage slot to use + /// The data associated with the specified as a + string GetItemAsString(string key); + + /// + /// Return the name of the key at the specified . + /// + /// + /// The name of the key at the specified + string Key(int index); + + /// + /// Get the keys of all items stored in session storage. + /// + /// The keys of all items stored in session storage + IEnumerable Keys(); - T GetItem(string key); + /// + /// Checks if the exists in session storage, but does not check its value. + /// + /// A value specifying the name of the storage slot to use + /// Boolean indicating if the specified exists + bool ContainKey(string key); - string Key(int index); + /// + /// The number of items stored in session storage. + /// + /// The number of items stored in session storage + int Length(); - int Length(); + /// + /// Remove the data with the specified . + /// + /// A value specifying the name of the storage slot to use + void RemoveItem(string key); + + /// + /// Removes a collection of . + /// + /// A IEnumerable collection of strings specifying the name of the storage slot to remove + void RemoveItems(IEnumerable keys); - void RemoveItem(string key); + /// + /// Sets or updates the in session storage with the specified . + /// + /// A value specifying the name of the storage slot to use + /// The data to be saved + void SetItem(string key, T data); - void SetItem(string key, T data); + /// + /// Sets or updates the in session storage with the specified . Does not serialize the value before storing. + /// + /// A value specifying the name of the storage slot to use + /// The string to be saved + /// + void SetItemAsString(string key, string data); - event EventHandler Changing; - event EventHandler Changed; - } -} \ No newline at end of file + event EventHandler Changing; + event EventHandler Changed; +} diff --git a/src/Blazored.SessionStorage/JsonConverters/TimespanJsonConverter.cs b/src/Blazored.SessionStorage/JsonConverters/TimespanJsonConverter.cs index f2436e3..2a81538 100644 --- a/src/Blazored.SessionStorage/JsonConverters/TimespanJsonConverter.cs +++ b/src/Blazored.SessionStorage/JsonConverters/TimespanJsonConverter.cs @@ -1,46 +1,42 @@ using System; +using System.Collections.Generic; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -namespace Blazored.SessionStorage.JsonConverters +namespace Blazored.SessionStorage.JsonConverters; + +/// +/// The new Json.NET doesn't support Timespan at this time +/// https://github.com/dotnet/corefx/issues/38641 +/// +public class TimespanJsonConverter : JsonConverter { /// - /// The new Json.NET doesn't support Timespan at this time - /// https://github.com/dotnet/corefx/issues/38641 + /// Format: Days.Hours:Minutes:Seconds:Milliseconds /// - public class TimespanJsonConverter : JsonConverter - { - /// - /// Format: Days.Hours:Minutes:Seconds:Milliseconds - /// - public const string TimeSpanFormatString = @"d\.hh\:mm\:ss\:FFF"; + public const string TimeSpanFormatString = @"d\.hh\:mm\:ss\:FFF"; - public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var s = reader.GetString(); + if (string.IsNullOrWhiteSpace(s)) { - var s = reader.GetString(); - if (string.IsNullOrWhiteSpace(s)) - { - return TimeSpan.Zero; - } - else - { - TimeSpan parsedTimeSpan; - if (!TimeSpan.TryParseExact(s, TimeSpanFormatString, null, out parsedTimeSpan)) - { - throw new FormatException($"Input timespan is not in an expected format : expected {Regex.Unescape(TimeSpanFormatString)}. Please retrieve this key as a string and parse manually."); - } - else - { - return parsedTimeSpan; - } - } + return TimeSpan.Zero; } - public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) + if (!TimeSpan.TryParseExact(s, TimeSpanFormatString, null, out var parsedTimeSpan)) { - var timespanFormatted = $"{value.ToString(TimeSpanFormatString)}"; - writer.WriteStringValue(timespanFormatted); + throw new FormatException($"Input timespan is not in an expected format : expected {Regex.Unescape(TimeSpanFormatString)}. Please retrieve this key as a string and parse manually."); } + + return parsedTimeSpan; + } + + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) + { + var timespanFormatted = $"{value.ToString(TimeSpanFormatString)}"; + writer.WriteStringValue(timespanFormatted); } } diff --git a/src/Blazored.SessionStorage/Serialization/IJsonSerializer.cs b/src/Blazored.SessionStorage/Serialization/IJsonSerializer.cs new file mode 100644 index 0000000..e1a99c5 --- /dev/null +++ b/src/Blazored.SessionStorage/Serialization/IJsonSerializer.cs @@ -0,0 +1,7 @@ +namespace Blazored.SessionStorage.Serialization; + +public interface IJsonSerializer +{ + string Serialize(T obj); + T Deserialize(string text); +} diff --git a/src/Blazored.SessionStorage/Serialization/SystemTextJsonSerializer.cs b/src/Blazored.SessionStorage/Serialization/SystemTextJsonSerializer.cs new file mode 100644 index 0000000..a4b893f --- /dev/null +++ b/src/Blazored.SessionStorage/Serialization/SystemTextJsonSerializer.cs @@ -0,0 +1,26 @@ +using Blazored.SessionStorage.StorageOptions; +using Microsoft.Extensions.Options; +using System.Text.Json; + +namespace Blazored.SessionStorage.Serialization; + +internal class SystemTextJsonSerializer : IJsonSerializer +{ + private readonly JsonSerializerOptions _options; + + public SystemTextJsonSerializer(IOptions options) + { + _options = options.Value.JsonSerializerOptions; + } + + public SystemTextJsonSerializer(SessionStorageOptions sessionStorageOptions) + { + _options = sessionStorageOptions.JsonSerializerOptions; + } + + public T Deserialize(string data) + => JsonSerializer.Deserialize(data, _options); + + public string Serialize(T data) + => JsonSerializer.Serialize(data, _options); +} \ No newline at end of file diff --git a/src/Blazored.SessionStorage/ServiceCollectionExtensions.cs b/src/Blazored.SessionStorage/ServiceCollectionExtensions.cs index 4bb05e7..795a92c 100644 --- a/src/Blazored.SessionStorage/ServiceCollectionExtensions.cs +++ b/src/Blazored.SessionStorage/ServiceCollectionExtensions.cs @@ -1,33 +1,58 @@ using System; +using System.Diagnostics.CodeAnalysis; using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; using Blazored.SessionStorage.StorageOptions; -using Microsoft.Extensions.DependencyInjection; - -namespace Blazored.SessionStorage -{ - public static class ServiceCollectionExtensions - { - public static IServiceCollection AddBlazoredSessionStorage(this IServiceCollection services) - { - return services - .AddScoped() - .AddScoped() - .Configure(configureOptions => - { - configureOptions.JsonSerializerOptions.Converters.Add(new TimespanJsonConverter()); - }); - } +using Microsoft.Extensions.DependencyInjection; - public static IServiceCollection AddBlazoredSessionStorage(this IServiceCollection services, Action configure) - { - return services - .AddScoped() - .AddScoped() - .Configure(configureOptions => - { - configure?.Invoke(configureOptions); - configureOptions.JsonSerializerOptions.Converters.Add(new TimespanJsonConverter()); - }); - } - } -} +namespace Blazored.SessionStorage; + +[ExcludeFromCodeCoverage] +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddBlazoredSessionStorage(this IServiceCollection services) + => AddBlazoredSessionStorage(services, null); + + public static IServiceCollection AddBlazoredSessionStorage(this IServiceCollection services, Action configure) + { + return services + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .Configure(configureOptions => + { + configure?.Invoke(configureOptions); + configureOptions.JsonSerializerOptions.Converters.Add(new TimespanJsonConverter()); + }); + } + + /// + /// Registers the Blazored SessionStorage services as singletons. This should only be used in Blazor WebAssembly applications. + /// Using this in Blazor Server applications will cause unexpected and potentially dangerous behaviour. + /// + /// + public static IServiceCollection AddBlazoredSessionStorageAsSingleton(this IServiceCollection services) + => AddBlazoredSessionStorageAsSingleton(services, null); + + /// + /// Registers the Blazored SessionStorage services as singletons. This should only be used in Blazor WebAssembly applications. + /// Using this in Blazor Server applications will cause unexpected and potentially dangerous behaviour. + /// + /// + /// + /// + public static IServiceCollection AddBlazoredSessionStorageAsSingleton(this IServiceCollection services, Action configure) + { + return services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .Configure(configureOptions => + { + configure?.Invoke(configureOptions); + configureOptions.JsonSerializerOptions.Converters.Add(new TimespanJsonConverter()); + }); + } +} diff --git a/src/Blazored.SessionStorage/SessionStorageService.cs b/src/Blazored.SessionStorage/SessionStorageService.cs index efb0922..c9b429d 100644 --- a/src/Blazored.SessionStorage/SessionStorageService.cs +++ b/src/Blazored.SessionStorage/SessionStorageService.cs @@ -1,181 +1,289 @@ -using Blazored.SessionStorage.StorageOptions; -using Microsoft.Extensions.Options; -using Microsoft.JSInterop; -using System; -using System.Text.Json; -using System.Threading.Tasks; - -namespace Blazored.SessionStorage -{ - public class SessionStorageService : ISessionStorageService, ISyncSessionStorageService - { - private readonly IJSRuntime _jSRuntime; - private readonly IJSInProcessRuntime _jSInProcessRuntime; - private readonly JsonSerializerOptions _jsonOptions; - - public event EventHandler Changing; - public event EventHandler Changed; - - public SessionStorageService(IJSRuntime jSRuntime, IOptions options) - { - _jSRuntime = jSRuntime; - _jsonOptions = options.Value.JsonSerializerOptions; - _jSInProcessRuntime = jSRuntime as IJSInProcessRuntime; - } - - public async Task SetItemAsync(string key, T data) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key)); - - var e = await RaiseOnChangingAsync(key, data); - - if (e.Cancel) - return; - - var serialisedData = JsonSerializer.Serialize(data, _jsonOptions); - - await _jSRuntime.InvokeVoidAsync("sessionStorage.setItem", key, serialisedData); - - RaiseOnChanged(key, e.OldValue, data); - } - - public async Task GetItemAsync(string key) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key)); - - var serialisedData = await _jSRuntime.InvokeAsync("sessionStorage.getItem", key); - - if (serialisedData == null) - return default; - - return JsonSerializer.Deserialize(serialisedData, _jsonOptions); - } - - public async Task RemoveItemAsync(string key) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key)); - - await _jSRuntime.InvokeAsync("sessionStorage.removeItem", key); - } - - public async Task ClearAsync() => await _jSRuntime.InvokeAsync("sessionStorage.clear"); - - public async Task LengthAsync() => await _jSRuntime.InvokeAsync("eval", "sessionStorage.length"); - - public async Task KeyAsync(int index) => await _jSRuntime.InvokeAsync("sessionStorage.key", index); - - public void SetItem(string key, T data) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key)); - - if (_jSInProcessRuntime == null) - throw new InvalidOperationException("IJSInProcessRuntime not available"); - - var e = RaiseOnChangingSync(key, data); - - if (e.Cancel) - return; - - var serialisedData = JsonSerializer.Serialize(data, _jsonOptions); - - _jSInProcessRuntime.InvokeVoid("sessionStorage.setItem", key, serialisedData); - - RaiseOnChanged(key, e.OldValue, data); - } - - public T GetItem(string key) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key)); - - if (_jSInProcessRuntime == null) - throw new InvalidOperationException("IJSInProcessRuntime not available"); - - var serialisedData = _jSInProcessRuntime.Invoke("sessionStorage.getItem", key); - - if (serialisedData == null) - return default; - - return JsonSerializer.Deserialize(serialisedData, _jsonOptions); - } - - public void RemoveItem(string key) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key)); - - if (_jSInProcessRuntime == null) - throw new InvalidOperationException("IJSInProcessRuntime not available"); - - _jSInProcessRuntime.InvokeVoid("sessionStorage.removeItem", key); - } - - public void Clear() - { - if (_jSInProcessRuntime == null) - throw new InvalidOperationException("IJSInProcessRuntime not available"); - - _jSInProcessRuntime.InvokeVoid("sessionStorage.clear"); - } - - public int Length() - { - if (_jSInProcessRuntime == null) - throw new InvalidOperationException("IJSInProcessRuntime not available"); - - return _jSInProcessRuntime.Invoke("eval", "sessionStorage.length"); - } - - public string Key(int index) - { - if (_jSInProcessRuntime == null) - throw new InvalidOperationException("IJSInProcessRuntime not available"); - - return _jSInProcessRuntime.Invoke("sessionStorage.key", index); - } - - private async Task RaiseOnChangingAsync(string key, object data) - { - var e = new ChangingEventArgs - { - Key = key, - OldValue = await GetItemAsync(key), - NewValue = data - }; - - Changing?.Invoke(this, e); - - return e; - } - - private ChangingEventArgs RaiseOnChangingSync(string key, object data) - { - var e = new ChangingEventArgs - { - Key = key, - OldValue = ((ISyncSessionStorageService)this).GetItem(key), - NewValue = data - }; - - Changing?.Invoke(this, e); - - return e; - } - - private void RaiseOnChanged(string key, object oldValue, object data) - { - var e = new ChangedEventArgs - { - Key = key, - OldValue = oldValue, - NewValue = data - }; - - Changed?.Invoke(this, e); - } - } -} +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Blazored.SessionStorage.Serialization; + +namespace Blazored.SessionStorage; + +internal class SessionStorageService : ISessionStorageService, ISyncSessionStorageService +{ + private readonly IStorageProvider _storageProvider; + private readonly IJsonSerializer _serializer; + + public SessionStorageService(IStorageProvider storageProvider, IJsonSerializer serializer) + { + _storageProvider = storageProvider; + _serializer = serializer; + } + + public ValueTask RemoveItemsAsync(IEnumerable keys, CancellationToken cancellationToken = default) + => _storageProvider.RemoveItemsAsync(keys, cancellationToken); + + public async ValueTask SetItemAsync(string key, T data, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + var e = await RaiseOnChangingAsync(key, data).ConfigureAwait(false); + + if (e.Cancel) + return; + + var serialisedData = _serializer.Serialize(data); + await _storageProvider.SetItemAsync(key, serialisedData, cancellationToken).ConfigureAwait(false); + + RaiseOnChanged(key, e.OldValue, data); + } + + public async ValueTask SetItemAsStringAsync(string key, string data, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + if (data is null) + throw new ArgumentNullException(nameof(data)); + + var e = await RaiseOnChangingAsync(key, data).ConfigureAwait(false); + + if (e.Cancel) + return; + + await _storageProvider.SetItemAsync(key, data, cancellationToken).ConfigureAwait(false); + + RaiseOnChanged(key, e.OldValue, data); + } + + public async ValueTask GetItemAsync(string key, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + var serialisedData = await _storageProvider.GetItemAsync(key, cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(serialisedData)) + return default; + + try + { + return _serializer.Deserialize(serialisedData); + } + catch (JsonException e) when (e.Path == "$" && typeof(T) == typeof(string)) + { + // For backward compatibility return the plain string. + // On the next save a correct value will be stored and this Exception will not happen again, for this 'key' + return (T)(object)serialisedData; + } + } + + public ValueTask GetItemAsStringAsync(string key, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + return _storageProvider.GetItemAsync(key, cancellationToken); + } + + public ValueTask RemoveItemAsync(string key, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + return _storageProvider.RemoveItemAsync(key, cancellationToken); + } + + public ValueTask ClearAsync(CancellationToken cancellationToken = default) + => _storageProvider.ClearAsync(cancellationToken); + + public ValueTask LengthAsync(CancellationToken cancellationToken = default) + => _storageProvider.LengthAsync(cancellationToken); + + public ValueTask KeyAsync(int index, CancellationToken cancellationToken = default) + => _storageProvider.KeyAsync(index, cancellationToken); + + public ValueTask> KeysAsync(CancellationToken cancellationToken = default) + => _storageProvider.KeysAsync(cancellationToken); + + public ValueTask ContainKeyAsync(string key, CancellationToken cancellationToken = default) + => _storageProvider.ContainKeyAsync(key, cancellationToken); + + public void SetItem(string key, T data) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + var e = RaiseOnChangingSync(key, data); + + if (e.Cancel) + return; + + var serialisedData = _serializer.Serialize(data); + _storageProvider.SetItem(key, serialisedData); + + RaiseOnChanged(key, e.OldValue, data); + } + + public void SetItemAsString(string key, string data) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + if (data is null) + throw new ArgumentNullException(nameof(data)); + + var e = RaiseOnChangingSync(key, data); + + if (e.Cancel) + return; + + _storageProvider.SetItem(key, data); + + RaiseOnChanged(key, e.OldValue, data); + } + + public T GetItem(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + var serialisedData = _storageProvider.GetItem(key); + + if (string.IsNullOrWhiteSpace(serialisedData)) + return default; + + try + { + return _serializer.Deserialize(serialisedData); + } + catch (JsonException e) when (e.Path == "$" && typeof(T) == typeof(string)) + { + // For backward compatibility return the plain string. + // On the next save a correct value will be stored and this Exception will not happen again, for this 'key' + return (T)(object)serialisedData; + } + } + + public string GetItemAsString(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + return _storageProvider.GetItem(key); + } + + public void RemoveItem(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentNullException(nameof(key)); + + _storageProvider.RemoveItem(key); + } + + public void RemoveItems(IEnumerable keys) + { + if (keys == null) + throw new ArgumentNullException(nameof(keys)); + + foreach (var key in keys) + { + _storageProvider.RemoveItem(key); + } + } + + public void Clear() + => _storageProvider.Clear(); + + public int Length() + => _storageProvider.Length(); + + public string Key(int index) + => _storageProvider.Key(index); + + public IEnumerable Keys() + => _storageProvider.Keys(); + + public bool ContainKey(string key) + => _storageProvider.ContainKey(key); + + public event EventHandler Changing; + private async Task RaiseOnChangingAsync(string key, object data) + { + var e = new ChangingEventArgs + { + Key = key, + OldValue = await GetItemInternalAsync(key).ConfigureAwait(false), + NewValue = data + }; + + Changing?.Invoke(this, e); + + return e; + } + + private ChangingEventArgs RaiseOnChangingSync(string key, object data) + { + var e = new ChangingEventArgs + { + Key = key, + OldValue = GetItemInternal(key), + NewValue = data + }; + + Changing?.Invoke(this, e); + + return e; + } + + private async Task GetItemInternalAsync(string key) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + var serialisedData = await _storageProvider.GetItemAsync(key).ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(serialisedData)) + return default; + try + { + return _serializer.Deserialize(serialisedData); + } + catch (JsonException) + { + return (T)(object)serialisedData; + } + } + + private object GetItemInternal(string key) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + var serialisedData = _storageProvider.GetItem(key); + + if (string.IsNullOrWhiteSpace(serialisedData)) + return default; + + try + { + return _serializer.Deserialize(serialisedData); + } + catch (JsonException) + { + return serialisedData; + } + } + + public event EventHandler Changed; + private void RaiseOnChanged(string key, object oldValue, object data) + { + var e = new ChangedEventArgs + { + Key = key, + OldValue = oldValue, + NewValue = data + }; + + Changed?.Invoke(this, e); + } +} diff --git a/src/Blazored.SessionStorage/StorageOptions/SessionStorageOptions.cs b/src/Blazored.SessionStorage/StorageOptions/SessionStorageOptions.cs index b005f1a..727545b 100644 --- a/src/Blazored.SessionStorage/StorageOptions/SessionStorageOptions.cs +++ b/src/Blazored.SessionStorage/StorageOptions/SessionStorageOptions.cs @@ -1,18 +1,8 @@ -using System.Text.Json; - -namespace Blazored.SessionStorage.StorageOptions -{ - public class SessionStorageOptions - { - public JsonSerializerOptions JsonSerializerOptions { get; } = new JsonSerializerOptions - { - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - IgnoreNullValues = true, - IgnoreReadOnlyProperties = true, - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - ReadCommentHandling = JsonCommentHandling.Skip, - WriteIndented = false - }; - } -} +using System.Text.Json; + +namespace Blazored.SessionStorage.StorageOptions; + +public class SessionStorageOptions +{ + public JsonSerializerOptions JsonSerializerOptions { get; set; } = new(); +} diff --git a/tests/Blazored.SessionStorage.Tests/Blazored.SessionStorage.Tests.csproj b/tests/Blazored.SessionStorage.Tests/Blazored.SessionStorage.Tests.csproj new file mode 100644 index 0000000..00c5d20 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/Blazored.SessionStorage.Tests.csproj @@ -0,0 +1,40 @@ + + + + net7.0 + + false + + ..\..\src\Blazored.SessionStorage\Blazored.SessionStorage.snk + + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + Blazored.SessionStorage.snk + + + + diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Clear.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Clear.cs new file mode 100644 index 0000000..4b68577 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Clear.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class Clear +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + + public Clear() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, serializer); + } + + [Fact] + public void ClearsAnyItemsInTheStore() + { + // Arrange + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + + _sut.SetItem("Item1", item1); + _sut.SetItem("Item2", item2); + + // Act + _sut.Clear(); + + // Assert + Assert.Equal(0, _storageProvider.Length()); + } + + [Fact] + public void DoesNothingWhenStoreIsEmpty() + { + // Act + _sut.Clear(); + + // Assert + Assert.Equal(0, _storageProvider.Length()); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/ClearAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/ClearAsync.cs new file mode 100644 index 0000000..3c39ac9 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/ClearAsync.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using System.Threading.Tasks; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class ClearAsync +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + + public ClearAsync() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, serializer); + } + + [Fact] + public async Task ClearsAnyItemsInTheStore() + { + // Arrange + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + + await _sut.SetItemAsync("Item1", item1); + await _sut.SetItemAsync("Item2", item2); + + // Act + await _sut.ClearAsync(); + + // Assert + Assert.Equal(0, await _storageProvider.LengthAsync()); + } + + [Fact] + public async Task DoesNothingWhenItemDoesNotExistInStore() + { + // Act + await _sut.ClearAsync(); + + // Assert + Assert.Equal(0, await _storageProvider.LengthAsync()); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/ContainsKey.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/ContainsKey.cs new file mode 100644 index 0000000..81b67ab --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/ContainsKey.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class ContainsKey +{ + private readonly SessionStorageService _sut; + + public ContainsKey() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + IStorageProvider storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(storageProvider, serializer); + } + + [Fact] + public void ReturnsTrue_When_KeyExistsInStore() + { + // Arrange + const string key = "TestKey"; + var item1 = new TestObject(1, "Jane Smith"); + _sut.SetItem(key, item1); + + // Act + var containsKey = _sut.ContainKey(key); + + // Assert + Assert.True(containsKey); + } + + [Fact] + public void ReturnsFalse_When_KeyDoesNotExistsInStore() + { + // Arrange + const string key = "TestKey"; + + // Act + var containsKey = _sut.ContainKey(key); + + // Assert + Assert.False(containsKey); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/ContainsKeyAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/ContainsKeyAsync.cs new file mode 100644 index 0000000..74c65de --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/ContainsKeyAsync.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using System.Threading.Tasks; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class ContainsKeyAsync +{ + private readonly SessionStorageService _sut; + + public ContainsKeyAsync() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + IStorageProvider storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(storageProvider, serializer); + } + + [Fact] + public async Task ReturnsTrue_When_KeyExistsInStore() + { + // Arrange + const string key = "TestKey"; + var item1 = new TestObject(1, "Jane Smith"); + await _sut.SetItemAsync(key, item1); + + // Act + var containsKey = await _sut.ContainKeyAsync(key); + + // Assert + Assert.True(containsKey); + } + + [Fact] + public async Task ReturnsFalse_When_KeyDoesNotExistsInStore() + { + // Arrange + const string key = "TestKey"; + + // Act + var containsKey = await _sut.ContainKeyAsync(key); + + // Assert + Assert.False(containsKey); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItem.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItem.cs new file mode 100644 index 0000000..ee957a0 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItem.cs @@ -0,0 +1,104 @@ +using System; +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class GetItem +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + private readonly IJsonSerializer _serializer; + + private const string Key = "testKey"; + + public GetItem() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + _serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, _serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + var action = new Action(() => _sut.GetItem(key)); + + // assert + Assert.Throws(action); + } + + [Theory] + [InlineData("Item1", "stringTest")] + [InlineData("Item2", 11)] + [InlineData("Item3", 11.11)] + public void ReturnsDeserializedDataFromStore(string key, T data) + { + // Arrange + _sut.SetItem(key, data); + + // Act + var result = _sut.GetItem(key); + + // Assert + Assert.Equal(data, result); + } + + [Fact] + public void ReturnsComplexObjectFromStore() + { + // Arrange + var objectToSave = new TestObject(2, "Jane Smith"); + _sut.SetItem(Key, objectToSave); + + // Act + var result = _sut.GetItem(Key); + + // Assert + Assert.Equal(objectToSave.Id, result.Id); + Assert.Equal(objectToSave.Name, result.Name); + } + + [Fact] + public void ReturnsNullFromStore_When_NullValueSaved() + { + // Arrange + var valueToSave = (string)null; + _sut.SetItem(Key, valueToSave); + + // Act + var result = _sut.GetItem(Key); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ReturnsStringFromStore_When_JsonExceptionIsThrown() + { + // Arrange + var jsonData = "[{ id: 5, name: \"Jane Smith\"}]"; + _storageProvider.SetItem(Key, jsonData); + + // Act + var result = _sut.GetItem(Key); + + // Assert + Assert.Equal(jsonData, result); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItemAsString.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItemAsString.cs new file mode 100644 index 0000000..92f985a --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItemAsString.cs @@ -0,0 +1,77 @@ +using System; +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class GetItemAsString +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + private readonly IJsonSerializer _serializer; + + private const string Key = "testKey"; + + public GetItemAsString() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + _serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, _serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + var action = new Action(() => _sut.GetItemAsString(key)); + + // assert + Assert.Throws(action); + } + + [Theory] + [InlineData("Item1", "stringTest")] + [InlineData("Item2", 11)] + [InlineData("Item3", 11.11)] + public void ReturnsDeserializedDataFromStore(string key, T data) + { + // Arrange + _sut.SetItem(key, data); + + // Act + var result = _sut.GetItemAsString(key); + + // Assert + var serializedData = _serializer.Serialize(data); + Assert.Equal(serializedData, result); + } + + [Fact] + public void ReturnsComplexObjectFromStore() + { + // Arrange + var objectToSave = new TestObject(2, "Jane Smith"); + _sut.SetItem(Key, objectToSave); + + // Act + var result = _sut.GetItemAsString(Key); + + // Assert + var serializedData = _serializer.Serialize(objectToSave); + Assert.Equal(serializedData, result); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItemAsStringAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItemAsStringAsync.cs new file mode 100644 index 0000000..a3dedb7 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItemAsStringAsync.cs @@ -0,0 +1,78 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class GetItemAsStringAsync +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + private readonly IJsonSerializer _serializer; + + private const string Key = "testKey"; + + public GetItemAsStringAsync() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + _serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, _serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + var action = new Func(async () => await _sut.GetItemAsStringAsync(key)); + + // assert + Assert.ThrowsAsync(action); + } + + [Theory] + [InlineData("Item1", "stringTest")] + [InlineData("Item2", 11)] + [InlineData("Item3", 11.11)] + public async Task ReturnsDeserializedDataFromStore(string key, T data) + { + // Arrange + await _sut.SetItemAsync(key, data); + + // Act + var result = await _sut.GetItemAsStringAsync(key); + + // Assert + var serializedData = _serializer.Serialize(data); + Assert.Equal(serializedData, result); + } + + [Fact] + public async Task ReturnsComplexObjectFromStore() + { + // Arrange + var objectToSave = new TestObject(2, "Jane Smith"); + await _sut.SetItemAsync(Key, objectToSave); + + // Act + var result = await _sut.GetItemAsStringAsync(Key); + + // Assert + var serializedData = _serializer.Serialize(objectToSave); + Assert.Equal(serializedData, result); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItemAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItemAsync.cs new file mode 100644 index 0000000..67de73f --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/GetItemAsync.cs @@ -0,0 +1,105 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class GetItemAsync +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + private readonly IJsonSerializer _serializer; + + private const string Key = "testKey"; + + public GetItemAsync() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + _serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, _serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + var action = new Func(async () => await _sut.GetItemAsync(key)); + + // assert + Assert.ThrowsAsync(action); + } + + [Theory] + [InlineData("Item1", "stringTest")] + [InlineData("Item2", 11)] + [InlineData("Item3", 11.11)] + public async Task ReturnsDeserializedDataFromStore(string key, T data) + { + // Arrange + await _sut.SetItemAsync(key, data); + + // Act + var result = await _sut.GetItemAsync(key); + + // Assert + Assert.Equal(data, result); + } + + [Fact] + public async Task ReturnsComplexObjectFromStore() + { + // Arrange + var objectToSave = new TestObject(2, "Jane Smith"); + await _sut.SetItemAsync(Key, objectToSave); + + // Act + var result = await _sut.GetItemAsync(Key); + + // Assert + Assert.Equal(objectToSave.Id, result.Id); + Assert.Equal(objectToSave.Name, result.Name); + } + + [Fact] + public async Task ReturnsNullFromStore_When_NullValueSaved() + { + // Arrange + var valueToSave = (string)null; + await _sut.SetItemAsync(Key, valueToSave); + + // Act + var result = await _sut.GetItemAsync(Key); + + // Assert + Assert.Null(result); + } + + [Fact] + public async Task ReturnsStringFromStore_When_JsonExceptionIsThrown() + { + // Arrange + var valueToSave = "[{ id: 5, name: \"Jane Smith\"}]"; + await _storageProvider.SetItemAsync(Key, valueToSave); + + // Act + var result = await _sut.GetItemAsync(Key); + + // Assert + Assert.Equal(valueToSave, result); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Key.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Key.cs new file mode 100644 index 0000000..ac8cba3 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Key.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class Key +{ + private readonly SessionStorageService _sut; + + public Key() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + IStorageProvider storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(storageProvider, serializer); + } + + [Fact] + public void ReturnsNameOfKeyAtIndex_When_KeyExistsInStore() + { + // Arrange + const string key1 = "TestKey1"; + const string key2 = "TestKey2"; + + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + + _sut.SetItem(key1, item1); + _sut.SetItem(key2, item2); + + // Act + var keyName = _sut.Key(1); + + // Assert + Assert.Equal(key2, keyName); + } + + [Fact] + public void ReturnsNull_When_KeyDoesNotExistInStore() + { + // Arrange + var item1 = new TestObject(1, "Jane Smith"); + _sut.SetItem("TestKey1", item1); + + // Act + var keyName = _sut.Key(1); + + // Assert + Assert.Null(keyName); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/KeyAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/KeyAsync.cs new file mode 100644 index 0000000..0ec8a92 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/KeyAsync.cs @@ -0,0 +1,62 @@ +using System.Text.Json; +using System.Threading.Tasks; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class KeyAsync +{ + private readonly SessionStorageService _sut; + + public KeyAsync() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + IStorageProvider storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(storageProvider, serializer); + } + + [Fact] + public async Task ReturnsNameOfKeyAtIndex_When_KeyExistsInStore() + { + // Arrange + const string key1 = "TestKey1"; + const string key2 = "TestKey2"; + + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + + await _sut.SetItemAsync(key1, item1); + await _sut.SetItemAsync(key2, item2); + + // Act + var keyName = await _sut.KeyAsync(1); + + // Assert + Assert.Equal(key2, keyName); + } + + [Fact] + public async Task ReturnsNull_When_KeyDoesNotExistInStore() + { + // Arrange + var item1 = new TestObject(1, "Jane Smith"); + await _sut.SetItemAsync("TestKey1", item1); + + // Act + var keyName = await _sut.KeyAsync(1); + + // Assert + Assert.Null(keyName); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Keys.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Keys.cs new file mode 100644 index 0000000..d243c19 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Keys.cs @@ -0,0 +1,153 @@ +using System.Linq; +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class Keys + { + private readonly SessionStorageService _sut; + + public Keys() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + IStorageProvider storageProvider = new InMemoryStorageProvider(); + + _sut = new SessionStorageService(storageProvider, serializer); + } + + [Fact] + public void ReturnsKeysAsync() + { + // Arrange + const string key1 = "TestKey1"; + const string key2 = "TestKey2"; + _sut.Clear(); + + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + + _sut.SetItem(key1, item1); + _sut.SetItem(key2, item2); + + // Act + var keyNames = _sut.KeysAsync().Result.ToList(); + + // Assert + Assert.Collection(keyNames, + item => Assert.Equal(key1, item), + item => Assert.Equal(key2, item)); + } + + [Fact] + public void ReturnsKeys() + { + // Arrange + const string key1 = "TestKey1"; + const string key2 = "TestKey2"; + _sut.Clear(); + + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + + _sut.SetItem(key1, item1); + _sut.SetItem(key2, item2); + + // Act + var keyNames = _sut.Keys().ToList(); + + // Assert + Assert.Collection(keyNames, + item => Assert.Equal(key1, item), + item => Assert.Equal(key2, item)); + } + + [Fact] + public void ReturnsEmptyWhenCollectionEmptyAsync() + { + // Arrange + _sut.Clear(); + + // Act + var keyNames = _sut.KeysAsync().Result; + + // Assert + Assert.Empty(keyNames); + } + + [Fact] + public void ReturnsEmptyWhenCollectionEmpty() + { + // Arrange + _sut.Clear(); + + // Act + var keyNames = _sut.Keys(); + + // Assert + Assert.Empty(keyNames); + } + + [Fact] + public void RemoveKeysAsync() + { + // Arrange + const string key1 = "TestKey1"; + const string key2 = "TestKey2"; + const string key3 = "TestKey3"; + _sut.Clear(); + + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + var item3 = new TestObject(3, "Jade Smith"); + + _sut.SetItem(key1, item1); + _sut.SetItem(key2, item2); + _sut.SetItem(key3, item3); + + // Act + var keyNames = new[] { key1, key2 }; + _sut.RemoveItemsAsync(keyNames).AsTask().Wait(); + + // Assert + Assert.Equal(1, _sut.Length()); + Assert.NotNull(_sut.GetItem(key3)); + } + + [Fact] + public void RemoveKeys() + { + // Arrange + const string key1 = "TestKey1"; + const string key2 = "TestKey2"; + const string key3 = "TestKey3"; + _sut.Clear(); + + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + var item3 = new TestObject(3, "Jade Smith"); + + _sut.SetItem(key1, item1); + _sut.SetItem(key2, item2); + _sut.SetItem(key3, item3); + + // Act + var keyNames = new[] { key1, key2 }; + _sut.RemoveItems(keyNames); + + // Assert + Assert.Equal(1, _sut.Length()); + Assert.NotNull(_sut.GetItem(key3)); + } + } diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Length.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Length.cs new file mode 100644 index 0000000..aaecf4e --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/Length.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class Length +{ + private readonly SessionStorageService _sut; + + public Length() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + IStorageProvider storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(storageProvider, serializer); + } + + [Fact] + public void ReturnsZeroWhenStoreIsEmpty() + { + // Act + var itemCount = _sut.Length(); + + // Assert + Assert.Equal(0, itemCount); + } + + [Fact] + public void ReturnsNumberOfItemsInStore() + { + // Arrange + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + + _sut.SetItem("Item1", item1); + _sut.SetItem("Item2", item2); + + // Act + var itemCount = _sut.Length(); + + // Assert + Assert.Equal(2, itemCount); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/LengthAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/LengthAsync.cs new file mode 100644 index 0000000..700780d --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/LengthAsync.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using System.Threading.Tasks; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class LengthAsync +{ + private readonly SessionStorageService _sut; + + public LengthAsync() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + IStorageProvider storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(storageProvider, serializer); + } + + [Fact] + public async Task ReturnsZeroWhenStoreIsEmpty() + { + // Act + var itemCount = await _sut.LengthAsync(); + + // Assert + Assert.Equal(0, itemCount); + } + + [Fact] + public async Task ReturnsNumberOfItemsInStore() + { + // Arrange + var item1 = new TestObject(1, "Jane Smith"); + var item2 = new TestObject(2, "John Smith"); + + await _sut.SetItemAsync("Item1", item1); + await _sut.SetItemAsync("Item2", item2); + + // Act + var itemCount = await _sut.LengthAsync(); + + // Assert + Assert.Equal(2, itemCount); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/RemoveItem.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/RemoveItem.cs new file mode 100644 index 0000000..1433b44 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/RemoveItem.cs @@ -0,0 +1,68 @@ +using System; +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class RemoveItem +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + + private const string Key = "testKey"; + + public RemoveItem() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + var action = new Action(() => _sut.RemoveItem(key)); + + // assert + Assert.Throws(action); + } + + [Fact] + public void RemovesItemFromStoreIfExists() + { + // Arrange + var data = new TestObject(2, "Jane Smith"); + _sut.SetItem(Key, data); + + // Act + _sut.RemoveItem(Key); + + // Assert + Assert.Equal(0, _storageProvider.Length()); + } + + [Fact] + public void DoesNothingWhenItemDoesNotExistInStore() + { + // Act + _sut.RemoveItem(Key); + + // Assert + Assert.Equal(0, _storageProvider.Length()); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/RemoveItemAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/RemoveItemAsync.cs new file mode 100644 index 0000000..cd138ee --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/RemoveItemAsync.cs @@ -0,0 +1,69 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class RemoveItemAsync +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + + private const string Key = "testKey"; + + public RemoveItemAsync() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + IJsonSerializer serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + var action = new Func(async () => await _sut.RemoveItemAsync(key)); + + // assert + Assert.ThrowsAsync(action); + } + + [Fact] + public async Task RemovesItemFromStoreIfExists() + { + // Arrange + var data = new TestObject(2, "Jane Smith"); + await _sut.SetItemAsync(Key, data); + + // Act + await _sut.RemoveItemAsync(Key); + + // Assert + Assert.Equal(0, await _storageProvider.LengthAsync()); + } + + [Fact] + public async Task DoesNothingWhenItemDoesNotExistInStore() + { + // Act + await _sut.RemoveItemAsync(Key); + + // Assert + Assert.Equal(0, await _storageProvider.LengthAsync()); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItem.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItem.cs new file mode 100644 index 0000000..48c3f92 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItem.cs @@ -0,0 +1,233 @@ +using System; +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class SetItem +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + private readonly IJsonSerializer _serializer; + + private const string Key = "testKey"; + + public SetItem() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + _serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, _serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + const string data = "Data"; + var action = new Action(() => _sut.SetItem(key, data)); + + // assert + Assert.Throws(action); + } + + [Fact] + public void RaisesOnChangingEvent_When_SavingNewData() + { + // arrange + var onChangingCalled = false; + _sut.Changing += (_, _) => onChangingCalled = true; + + // act + _sut.SetItem("Key", "Data"); + + // assert + Assert.True(onChangingCalled); + } + + [Fact] + public void OnChangingEventContainsEmptyOldValue_When_SavingData() + { + // arrange + var oldValue = ""; + _sut.Changing += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + _sut.SetItem("Key", "Data"); + + // assert + Assert.Equal(default, oldValue); + } + + [Fact] + public void OnChangingEventContainsNewValue_When_SavingNewData() + { + // arrange + const string data = "Data"; + var newValue = ""; + _sut.Changing += (_, args) => newValue = args.NewValue.ToString(); + + // act + _sut.SetItemAsync("Key", data); + + // assert + Assert.Equal(data, newValue); + } + + [Fact] + public void OnChangingEventIsCancelled_When_SettingCancelToTrue_When_SavingNewData() + { + // arrange + _sut.Changing += (_, args) => args.Cancel = true; + + // act + _sut.SetItem("Key", "Data"); + + // assert + Assert.Equal(0, _storageProvider.Length()); + } + + [Theory] + [InlineData("stringTest")] + [InlineData(11)] + [InlineData(11.11)] + public void SavesDataAsJsonToStore(T valueToSave) + { + // Act + _sut.SetItem(Key, valueToSave); + + // Assert + var serializedValue = _storageProvider.GetItem(Key); + var valueFromStore = _serializer.Deserialize(serializedValue); + + Assert.Equal(1, _storageProvider.Length()); + Assert.Equal(valueToSave, valueFromStore); + } + + [Fact] + public void SavesComplexObjectAsJsonToStore() + { + // Arrange + var objectToSave = new TestObject(2, "Jane Smith"); + + // Act + _sut.SetItem(Key, objectToSave); + + // Assert + var serializedObject = _storageProvider.GetItem(Key); + var objectFromStore = _serializer.Deserialize(serializedObject); + + Assert.Equal(1, _storageProvider.Length()); + Assert.Equal(objectToSave.Id, objectFromStore.Id); + Assert.Equal(objectToSave.Name, objectFromStore.Name); + } + + [Fact] + public void SavesNullIntoStore_When_NullValueProvided() + { + // Arrange + var valueToSave = (string)null; + + // Act + _sut.SetItem(Key, valueToSave); + + // Assert + var serializedValue = _storageProvider.GetItem(Key); + var valueFromStore = _serializer.Deserialize(serializedValue); + + Assert.Equal(1, _storageProvider.Length()); + Assert.Null(valueFromStore); + } + + [Fact] + public void OverwriteExistingValueInStore_When_UsingTheSameKey() + { + // Arrange + const string existingValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZXhwIjoxNTg1NjYwNzEyLCJpc3MiOiJDb2RlUmVkQm9va2luZy5TZXJ2ZXIiLCJhdWQiOiJDb2RlUmVkQm9va2luZy5DbGllbnRzIn0.JhK1M1H7NLCFexujJYCDjTn9La0HloGYADMHXGCFksU"; + const string newValue = "6QLE0LL7iw7tHPAwold31qUENt3lVTUZxDGqeXQFx38="; + + var serializedValue = _serializer.Serialize(existingValue); + _storageProvider.SetItem(Key, serializedValue); + + // Act + _sut.SetItem(Key, newValue); + + // Assert + var serializedUpdatedValue = _storageProvider.GetItem(Key); + var updatedValue = _serializer.Deserialize(serializedUpdatedValue); + + Assert.Equal(newValue, updatedValue); + } + + [Fact] + public void RaisesOnChangedEvent_When_SavingData() + { + // arrange + var onChangedCalled = false; + _sut.Changed += (_, _) => onChangedCalled = true; + + // act + _sut.SetItem("Key", "Data"); + + // assert + Assert.True(onChangedCalled); + } + + [Fact] + public void OnChangedEventContainsEmptyOldValue_When_SavingNewData() + { + // arrange + var oldValue = ""; + _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + _sut.SetItem("Key", "Data"); + + // assert + Assert.Equal(default, oldValue); + } + + [Fact] + public void OnChangedEventContainsNewValue_When_SavingNewData() + { + // arrange + const string data = "Data"; + var newValue = ""; + _sut.Changed += (_, args) => newValue = args.NewValue.ToString(); + + // act + _sut.SetItem("Key", data); + + // assert + Assert.Equal(data, newValue); + } + + [Fact] + public void OnChangedEventContainsOldValue_When_UpdatingExistingData() + { + // arrange + var existingValue = "Foo"; + _storageProvider.SetItem("Key", existingValue); + var oldValue = ""; + _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + _sut.SetItem("Key", "Data"); + + // assert + Assert.Equal(existingValue, oldValue); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsString.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsString.cs new file mode 100644 index 0000000..913b043 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsString.cs @@ -0,0 +1,203 @@ +using System; +using System.Text.Json; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class SetItemAsString +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + private readonly IJsonSerializer _serializer; + + private const string Key = "testKey"; + + public SetItemAsString() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + _serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, _serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + const string data = "Data"; + var action = new Action(() => _sut.SetItemAsString(key, data)); + + // assert + Assert.Throws(action); + } + + [Fact] + public void ThrowsArgumentNullException_When_DataIsNull() + { + // arrange / act + var data = (string)null; + var action = new Action(() => _sut.SetItemAsString("MyValue", data)); + + // assert + Assert.Throws(action); + } + + [Fact] + public void RaisesOnChangingEvent_When_SavingNewData() + { + // arrange + var onChangingCalled = false; + _sut.Changing += (_, _) => onChangingCalled = true; + + // act + _sut.SetItemAsString("Key", "Data"); + + // assert + Assert.True(onChangingCalled); + } + + [Fact] + public void OnChangingEventContainsEmptyOldValue_When_SavingData() + { + // arrange + var oldValue = ""; + _sut.Changing += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + _sut.SetItemAsString("Key", "Data"); + + // assert + Assert.Equal(default, oldValue); + } + + [Fact] + public void OnChangingEventContainsNewValue_When_SavingNewData() + { + // arrange + const string data = "Data"; + var newValue = ""; + _sut.Changing += (_, args) => newValue = args.NewValue.ToString(); + + // act + _sut.SetItemAsString("Key", data); + + // assert + Assert.Equal(data, newValue); + } + + [Fact] + public void OnChangingEventIsCancelled_When_SettingCancelToTrue_When_SavingNewData() + { + // arrange + _sut.Changing += (_, args) => args.Cancel = true; + + // act + _sut.SetItemAsString("Key", "Data"); + + // assert + Assert.Equal(0, _storageProvider.Length()); + } + + [Fact] + public void SavesDataToStore() + { + // Act + var valueToSave = "StringValue"; + _sut.SetItemAsString(Key, valueToSave); + + // Assert + var valueFromStore = _storageProvider.GetItem(Key); + + Assert.Equal(1, _storageProvider.Length()); + Assert.Equal(valueToSave, valueFromStore); + } + + [Fact] + public void OverwriteExistingValueInStore_When_UsingTheSameKey() + { + // Arrange + const string existingValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZXhwIjoxNTg1NjYwNzEyLCJpc3MiOiJDb2RlUmVkQm9va2luZy5TZXJ2ZXIiLCJhdWQiOiJDb2RlUmVkQm9va2luZy5DbGllbnRzIn0.JhK1M1H7NLCFexujJYCDjTn9La0HloGYADMHXGCFksU"; + const string newValue = "6QLE0LL7iw7tHPAwold31qUENt3lVTUZxDGqeXQFx38="; + + _storageProvider.SetItem(Key, existingValue); + + // Act + _sut.SetItemAsString(Key, newValue); + + // Assert + var updatedValue = _storageProvider.GetItem(Key); + + Assert.Equal(newValue, updatedValue); + } + + [Fact] + public void RaisesOnChangedEvent_When_SavingData() + { + // arrange + var onChangedCalled = false; + _sut.Changed += (_, _) => onChangedCalled = true; + + // act + _sut.SetItemAsString("Key", "Data"); + + // assert + Assert.True(onChangedCalled); + } + + [Fact] + public void OnChangedEventContainsEmptyOldValue_When_SavingNewData() + { + // arrange + var oldValue = ""; + _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + _sut.SetItemAsString("Key", "Data"); + + // assert + Assert.Equal(default, oldValue); + } + + [Fact] + public void OnChangedEventContainsNewValue_When_SavingNewData() + { + // arrange + const string data = "Data"; + var newValue = ""; + _sut.Changed += (_, args) => newValue = args.NewValue.ToString(); + + // act + _sut.SetItemAsString("Key", data); + + // assert + Assert.Equal(data, newValue); + } + + [Fact] + public void OnChangedEventContainsOldValue_When_UpdatingExistingData() + { + // arrange + var existingValue = "Foo"; + _storageProvider.SetItem("Key", existingValue); + var oldValue = ""; + _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + _sut.SetItemAsString("Key", "Data"); + + // assert + Assert.Equal(existingValue, oldValue); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsStringAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsStringAsync.cs new file mode 100644 index 0000000..7857a1b --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsStringAsync.cs @@ -0,0 +1,204 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class SetItemAsStringAsync +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + private readonly IJsonSerializer _serializer; + + private const string Key = "testKey"; + + public SetItemAsStringAsync() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + _serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, _serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + const string data = "Data"; + var action = new Func(async () => await _sut.SetItemAsStringAsync(key, data)); + + // assert + Assert.ThrowsAsync(action); + } + + [Fact] + public void ThrowsArgumentNullException_When_DataIsNull() + { + // arrange / act + var data = (string)null; + var action = new Func(async () => await _sut.SetItemAsStringAsync("MyValue", data)); + + // assert + Assert.ThrowsAsync(action); + } + + [Fact] + public async Task RaisesOnChangingEvent_When_SavingNewData() + { + // arrange + var onChangingCalled = false; + _sut.Changing += (_, _) => onChangingCalled = true; + + // act + await _sut.SetItemAsStringAsync("Key", "Data"); + + // assert + Assert.True(onChangingCalled); + } + + [Fact] + public async Task OnChangingEventContainsEmptyOldValue_When_SavingData() + { + // arrange + var oldValue = ""; + _sut.Changing += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + await _sut.SetItemAsStringAsync("Key", "Data"); + + // assert + Assert.Equal(default, oldValue); + } + + [Fact] + public async Task OnChangingEventContainsNewValue_When_SavingNewData() + { + // arrange + const string data = "Data"; + var newValue = ""; + _sut.Changing += (_, args) => newValue = args.NewValue.ToString(); + + // act + await _sut.SetItemAsStringAsync("Key", data); + + // assert + Assert.Equal(data, newValue); + } + + [Fact] + public async Task OnChangingEventIsCancelled_When_SettingCancelToTrue_When_SavingNewData() + { + // arrange + _sut.Changing += (_, args) => args.Cancel = true; + + // act + await _sut.SetItemAsStringAsync("Key", "Data"); + + // assert + Assert.Equal(0, _storageProvider.Length()); + } + + [Fact] + public async Task SavesDataToStore() + { + // Act + var valueToSave = "StringValue"; + await _sut.SetItemAsStringAsync(Key, valueToSave); + + // Assert + var valueFromStore = _storageProvider.GetItem(Key); + + Assert.Equal(1, _storageProvider.Length()); + Assert.Equal(valueToSave, valueFromStore); + } + + [Fact] + public async Task OverwriteExistingValueInStore_When_UsingTheSameKey() + { + // Arrange + const string existingValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZXhwIjoxNTg1NjYwNzEyLCJpc3MiOiJDb2RlUmVkQm9va2luZy5TZXJ2ZXIiLCJhdWQiOiJDb2RlUmVkQm9va2luZy5DbGllbnRzIn0.JhK1M1H7NLCFexujJYCDjTn9La0HloGYADMHXGCFksU"; + const string newValue = "6QLE0LL7iw7tHPAwold31qUENt3lVTUZxDGqeXQFx38="; + + _storageProvider.SetItem(Key, existingValue); + + // Act + await _sut.SetItemAsStringAsync(Key, newValue); + + // Assert + var updatedValue = _storageProvider.GetItem(Key); + + Assert.Equal(newValue, updatedValue); + } + + [Fact] + public async Task RaisesOnChangedEvent_When_SavingData() + { + // arrange + var onChangedCalled = false; + _sut.Changed += (_, _) => onChangedCalled = true; + + // act + await _sut.SetItemAsStringAsync("Key", "Data"); + + // assert + Assert.True(onChangedCalled); + } + + [Fact] + public async Task OnChangedEventContainsEmptyOldValue_When_SavingNewData() + { + // arrange + var oldValue = ""; + _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + await _sut.SetItemAsStringAsync("Key", "Data"); + + // assert + Assert.Equal(default, oldValue); + } + + [Fact] + public async Task OnChangedEventContainsNewValue_When_SavingNewData() + { + // arrange + const string data = "Data"; + var newValue = ""; + _sut.Changed += (_, args) => newValue = args.NewValue.ToString(); + + // act + await _sut.SetItemAsStringAsync("Key", data); + + // assert + Assert.Equal(data, newValue); + } + + [Fact] + public async Task OnChangedEventContainsOldValue_When_UpdatingExistingData() + { + // arrange + var existingValue = "Foo"; + _storageProvider.SetItem("Key", existingValue); + var oldValue = ""; + _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + await _sut.SetItemAsStringAsync("Key", "Data"); + + // assert + Assert.Equal(existingValue, oldValue); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsync.cs b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsync.cs new file mode 100644 index 0000000..8aa1e49 --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/SessionStorageServiceTests/SetItemAsync.cs @@ -0,0 +1,234 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Blazored.SessionStorage.JsonConverters; +using Blazored.SessionStorage.Serialization; +using Blazored.SessionStorage.StorageOptions; +using Blazored.SessionStorage.TestExtensions; +using Blazored.SessionStorage.Tests.TestAssets; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Blazored.SessionStorage.Tests.SessionStorageServiceTests; + +public class SetItemAsync +{ + private readonly SessionStorageService _sut; + private readonly IStorageProvider _storageProvider; + private readonly IJsonSerializer _serializer; + + private const string Key = "testKey"; + + public SetItemAsync() + { + var mockOptions = new Mock>(); + var jsonOptions = new JsonSerializerOptions(); + jsonOptions.Converters.Add(new TimespanJsonConverter()); + mockOptions.Setup(u => u.Value).Returns(new SessionStorageOptions()); + _serializer = new SystemTextJsonSerializer(mockOptions.Object); + _storageProvider = new InMemoryStorageProvider(); + _sut = new SessionStorageService(_storageProvider, _serializer); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void ThrowsArgumentNullException_When_KeyIsInvalid(string key) + { + // arrange / act + const string data = "Data"; + var action = new Func(async () => await _sut.SetItemAsync(key, data)); + + // assert + Assert.ThrowsAsync(action); + } + + [Fact] + public async Task RaisesOnChangingEvent_When_SavingNewData() + { + // arrange + var onChangingCalled = false; + _sut.Changing += (sender, args) => onChangingCalled = true; + + // act + await _sut.SetItemAsync("Key", "Data"); + + // assert + Assert.True(onChangingCalled); + } + + [Fact] + public async Task OnChangingEventContainsEmptyOldValue_When_SavingData() + { + // arrange + var oldValue = ""; + _sut.Changing += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + await _sut.SetItemAsync("Key", "Data"); + + // assert + Assert.Equal(default, oldValue); + } + + [Fact] + public async Task OnChangingEventContainsNewValue_When_SavingNewData() + { + // arrange + const string data = "Data"; + var newValue = ""; + _sut.Changing += (_, args) => newValue = args.NewValue.ToString(); + + // act + await _sut.SetItemAsync("Key", data); + + // assert + Assert.Equal(data, newValue); + } + + [Fact] + public async Task OnChangingEventIsCancelled_When_SettingCancelToTrue_When_SavingNewData() + { + // arrange + _sut.Changing += (_, args) => args.Cancel = true; + + // act + await _sut.SetItemAsync("Key", "Data"); + + // assert + Assert.Equal(0, await _storageProvider.LengthAsync()); + } + + [Theory] + [InlineData("stringTest")] + [InlineData(11)] + [InlineData(11.11)] + public async Task SavesDataAsJsonToStore(T valueToSave) + { + // Act + await _sut.SetItemAsync(Key, valueToSave); + + // Assert + var serializedValue = await _storageProvider.GetItemAsync(Key); + var valueFromStore = _serializer.Deserialize(serializedValue); + + Assert.Equal(1, await _storageProvider.LengthAsync()); + Assert.Equal(valueToSave, valueFromStore); + } + + [Fact] + public async Task SavesComplexObjectAsJsonToStore() + { + // Arrange + var objectToSave = new TestObject(2, "Jane Smith"); + + // Act + await _sut.SetItemAsync(Key, objectToSave); + + // Assert + var serializedObject = await _storageProvider.GetItemAsync(Key); + var objectFromStore = _serializer.Deserialize(serializedObject); + + Assert.Equal(1, await _storageProvider.LengthAsync()); + Assert.Equal(objectToSave.Id, objectFromStore.Id); + Assert.Equal(objectToSave.Name, objectFromStore.Name); + } + + [Fact] + public async Task SavesNullIntoStore_When_NullValueProvided() + { + // Arrange + var valueToSave = (string)null; + + // Act + await _sut.SetItemAsync(Key, valueToSave); + + // Assert + var serializedValue = await _storageProvider.GetItemAsync(Key); + var valueFromStore = _serializer.Deserialize(serializedValue); + + Assert.Equal(1, await _storageProvider.LengthAsync()); + Assert.Null(valueFromStore); + } + + [Fact] + public async Task OverwriteExistingValueInStore_When_UsingTheSameKey() + { + // Arrange + const string existingValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbmlzdHJhdG9yIiwiZXhwIjoxNTg1NjYwNzEyLCJpc3MiOiJDb2RlUmVkQm9va2luZy5TZXJ2ZXIiLCJhdWQiOiJDb2RlUmVkQm9va2luZy5DbGllbnRzIn0.JhK1M1H7NLCFexujJYCDjTn9La0HloGYADMHXGCFksU"; + const string newValue = "6QLE0LL7iw7tHPAwold31qUENt3lVTUZxDGqeXQFx38="; + + var serializedValue = _serializer.Serialize(existingValue); + await _storageProvider.SetItemAsync(Key, serializedValue); + + // Act + await _sut.SetItemAsync(Key, newValue); + + // Assert + var serializedUpdatedValue = await _storageProvider.GetItemAsync(Key); + var updatedValue = _serializer.Deserialize(serializedUpdatedValue); + + Assert.Equal(newValue, updatedValue); + } + + [Fact] + public async Task RaisesOnChangedEvent_When_SavingData() + { + // arrange + var onChangedCalled = false; + _sut.Changed += (sender, args) => onChangedCalled = true; + + // act + await _sut.SetItemAsync("Key", "Data"); + + // assert + Assert.True(onChangedCalled); + } + + [Fact] + public async Task OnChangedEventContainsEmptyOldValue_When_SavingNewData() + { + // arrange + var oldValue = ""; + _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + await _sut.SetItemAsync("Key", "Data"); + + // assert + Assert.Equal(default, oldValue); + } + + [Fact] + public async Task OnChangedEventContainsNewValue_When_SavingNewData() + { + // arrange + const string data = "Data"; + var newValue = ""; + _sut.Changed += (_, args) => newValue = args.NewValue.ToString(); + + // act + await _sut.SetItemAsync("Key", data); + + // assert + Assert.Equal(data, newValue); + } + + [Fact] + public async Task OnChangedEventContainsOldValue_When_UpdatingExistingData() + { + // arrange + var existingValue = "Foo"; + await _storageProvider.SetItemAsync("Key", existingValue); + var oldValue = ""; + _sut.Changed += (_, args) => oldValue = args.OldValue?.ToString(); + + // act + await _sut.SetItemAsync("Key", "Data"); + + // assert + Assert.Equal(existingValue, oldValue); + } +} \ No newline at end of file diff --git a/tests/Blazored.SessionStorage.Tests/TestAssets/TestObject.cs b/tests/Blazored.SessionStorage.Tests/TestAssets/TestObject.cs new file mode 100644 index 0000000..5ee381d --- /dev/null +++ b/tests/Blazored.SessionStorage.Tests/TestAssets/TestObject.cs @@ -0,0 +1,14 @@ +namespace Blazored.SessionStorage.Tests.TestAssets; + +public class TestObject +{ + public int Id { get; set; } + public string Name { get; set; } + + public TestObject() { } + public TestObject(int id, string name) + { + Id = id; + Name = name; + } +} \ No newline at end of file