├── NOTICE ├── icon.png ├── logo.png ├── public.snk ├── SampleApp ├── appsettings.Development.json ├── appsettings.json ├── SampleApp.csproj ├── WeatherForecast.cs ├── README.md ├── Properties │ └── launchSettings.json └── Program.cs ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── documentation.yml │ ├── feature-request.yml │ └── bug-report.yml ├── dependabot.yml └── workflows │ ├── handle-stale-discussions.yml │ ├── closed-issue-message.yml │ ├── semgrep-analysis.yml │ ├── issue-regression-labeler.yml │ ├── change-file-in-pr.yml │ ├── aws-ci.yml │ ├── stale_issues.yml │ ├── create-release-pr.yml │ └── sync-main-dev.yml ├── .gitattribute ├── CODE_OF_CONDUCT.md ├── .autover └── autover.json ├── src └── AWS.DistributedCacheProvider │ ├── InvalidTableException.cs │ ├── DynamoDBDistributedCacheException.cs │ ├── Internal │ ├── Utilities.cs │ ├── DynamoDBCacheProviderHelper.cs │ └── DynamoDBTableCreator.cs │ ├── ExtensionMethods.cs │ ├── IDynamoDBTableCreator.cs │ ├── AWS.DistributedCacheProvider.csproj │ └── DynamoDBDistributedCacheOptions.cs ├── test ├── AWS.DistributedCacheProviderUnitTests │ ├── UtilitiesTests.cs │ ├── ExtensionTests.cs │ ├── AWS.DistributedCacheProviderUnitTests.csproj │ ├── DynamoDBDistributedCacheHelperTests.cs │ ├── DynamoDBDistributedCacheTableTests.cs │ └── DynamoDBDistributedCacheCRUDTests.cs └── AWS.DistributedCacheProviderIntegrationTests │ ├── AWS.DistributedCacheProviderIntegrationTests.csproj │ ├── IntegrationTestUtils.cs │ ├── DynamoDBDistributedCacheTableTests.cs │ └── DynamoDBDistributedCacheCRUDTests.cs ├── CHANGELOG.md ├── AWS.DistributedCacheProvider.sln ├── CONTRIBUTING.md ├── .gitignore ├── LICENSE ├── .editorconfig ├── README.md └── THIRD-PARTY-LICENSES /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-dotnet-distributed-cache-provider/HEAD/icon.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-dotnet-distributed-cache-provider/HEAD/logo.png -------------------------------------------------------------------------------- /public.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-dotnet-distributed-cache-provider/HEAD/public.snk -------------------------------------------------------------------------------- /SampleApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SampleApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: 💬 General Question 5 | url: https://github.com/aws/aws-dotnet-distributed-cache-provider/discussions/categories/q-a 6 | about: Please ask and answer questions as a discussion thread -------------------------------------------------------------------------------- /.gitattribute: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Declare files that will always have CRLF line endings on checkout. 5 | *.sln text eol=crlf 6 | 7 | # Denote all files that are truly binary and should not be modified. 8 | *.png binary 9 | *.jpg binary -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /.autover/autover.json: -------------------------------------------------------------------------------- 1 | { 2 | "Projects": [ 3 | { 4 | "Name": "AWS.AspNetCore.DistributedCacheProvider", 5 | "Path": "src/AWS.DistributedCacheProvider/AWS.DistributedCacheProvider.csproj" 6 | } 7 | ], 8 | "UseCommitsForChangelog": false, 9 | "UseSameVersionForAllProjects": false, 10 | "DefaultIncrementType": "Patch", 11 | "ChangeFilesDetermineIncrementType": true 12 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every quarter 8 | interval: "quarterly" 9 | labels: 10 | - "Release Not Needed" 11 | target-branch: "dev" 12 | # Group all github-actions updates into a single PR 13 | groups: 14 | all-github-actions: 15 | applies-to: "version-updates" 16 | patterns: 17 | - "*" 18 | -------------------------------------------------------------------------------- /SampleApp/SampleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/handle-stale-discussions.yml: -------------------------------------------------------------------------------- 1 | name: HandleStaleDiscussions 2 | on: 3 | schedule: 4 | - cron: '0 */4 * * *' 5 | discussion_comment: 6 | types: [created] 7 | 8 | jobs: 9 | handle-stale-discussions: 10 | name: Handle stale discussions 11 | runs-on: ubuntu-latest 12 | permissions: 13 | discussions: write 14 | steps: 15 | - name: Stale discussions action 16 | uses: aws-github-ops/handle-stale-discussions@c0beee451a5d33d9c8f048a6d4e7c856b5422544 #v1.6.0 17 | env: 18 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 19 | -------------------------------------------------------------------------------- /src/AWS.DistributedCacheProvider/InvalidTableException.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | namespace AWS.DistributedCacheProvider 5 | { 6 | /// 7 | /// An Exception that is thrown when the Configuration points to a DynamoDB table that is not suitable for a cache 8 | /// 9 | [Serializable] 10 | public class InvalidTableException : Exception 11 | { 12 | public InvalidTableException() { } 13 | 14 | public InvalidTableException(string message) : base(message) { } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "📕 Documentation Issue" 3 | description: Report an issue in the API Reference documentation or Developer Guide 4 | title: "(short issue description)" 5 | labels: [documentation, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the issue 12 | description: A clear and concise description of the issue. 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: links 18 | attributes: 19 | label: Links 20 | description: | 21 | Include links to affected documentation page(s). 22 | validations: 23 | required: true 24 | -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderUnitTests/UtilitiesTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | using AWS.DistributedCacheProvider.Internal; 4 | using Xunit; 5 | 6 | namespace AWS.DistributedCacheProviderUnitTests 7 | { 8 | public class UtilitiesTests 9 | { 10 | [Theory] 11 | [InlineData("foo", null, "dc:foo")] 12 | [InlineData("foo", "bar", "bar:dc:foo")] 13 | public void FormatKey(string key, string? prefix, string expectedValue) 14 | { 15 | var formattedKey = Utilities.FormatPartitionKey(key, prefix); 16 | Assert.Equal(expectedValue, formattedKey); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AWS.DistributedCacheProvider/DynamoDBDistributedCacheException.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | namespace AWS.DistributedCacheProvider 5 | { 6 | /// 7 | /// An Exception that acts as a wrapper for any exception thrown during interactions with DynamoDB 8 | /// 9 | [Serializable] 10 | public class DynamoDBDistributedCacheException : Exception 11 | { 12 | public DynamoDBDistributedCacheException() { } 13 | 14 | public DynamoDBDistributedCacheException(string message) : base(message) { } 15 | 16 | public DynamoDBDistributedCacheException(string message, Exception innerException) : base(message, innerException) { } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SampleApp/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | namespace SampleApp; 5 | 6 | public class WeatherForecast 7 | { 8 | /// 9 | /// The instant the forecast was generated, which may be in the past if it was cached 10 | /// 11 | public DateTimeOffset DateForecastWasGenerated { get; set; } 12 | 13 | /// 14 | /// Forecasted temperature in Celsius 15 | /// 16 | public int TemperatureC { get; set; } 17 | 18 | /// 19 | /// Forecasted temperature in Fahrenheit 20 | /// 21 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 22 | 23 | /// 24 | /// Friendly description of the forecast 25 | /// 26 | public string? Summary { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/closed-issue-message.yml: -------------------------------------------------------------------------------- 1 | name: Closed Issue Message 2 | on: 3 | issues: 4 | types: [closed] 5 | permissions: 6 | issues: write 7 | 8 | jobs: 9 | auto_comment: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: aws-actions/closed-issue-message@10aaf6366131b673a7c8b7742f8b3849f1d44f18 #v2 13 | with: 14 | # These inputs are both required 15 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 16 | message: | 17 | ### ⚠️COMMENT VISIBILITY WARNING⚠️ 18 | Comments on closed issues are hard for our team to see. 19 | If you need more assistance, please either tag a team member or open a new issue that references this one. 20 | If you wish to keep having a conversation with other community members under this issue feel free to do so. 21 | -------------------------------------------------------------------------------- /SampleApp/README.md: -------------------------------------------------------------------------------- 1 | # AWS .NET Distributed Cache Provider Sample App 2 | 3 | This sample is an ASP.NET Core minimal API that returns a weather forecast. 4 | 5 | A GET request to `/weatherforecast` will initially return a random weather forecast. 6 | 7 | The generated forecast will be cached for the current user's session with an `IdleTimeout` of 30 seconds. Subsequent requests should return the cached forecast and reset the timeout. 8 | 9 | If a request is not made for 30 seconds, the cached forecast should expire. A subsequent request should generate a new forecast. 10 | 11 | The cached data is stored in a DynamoDB table named `weather-sessions-cache`. This table will be created if it does not exist. 12 | 13 | **Note:** Running this sample may create a DynamoDB table using the `default` or environmental AWS credentials. Since AWS resources are created and used during the running of this sample, charges may occur. 14 | -------------------------------------------------------------------------------- /SampleApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:5947", 8 | "sslPort": 44379 9 | } 10 | }, 11 | "profiles": { 12 | "SampleApp": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "https://localhost:7082;http://localhost:5062", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "IIS Express": { 23 | "commandName": "IISExpress", 24 | "launchBrowser": true, 25 | "launchUrl": "swagger", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/AWS.DistributedCacheProvider/Internal/Utilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace AWS.DistributedCacheProvider.Internal 7 | { 8 | public static class Utilities 9 | { 10 | /// 11 | /// Format the partition key value applying user specified key prefix. The prefix "dc:" is always 12 | /// added to namespace the cache items in the table allowing the table to be used in a single table 13 | /// pattern by default for most use cases. 14 | /// 15 | /// 16 | /// 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | public static string FormatPartitionKey(string partitionKeyValue, string? partitionKeyPrefix) 19 | { 20 | if (string.IsNullOrEmpty(partitionKeyPrefix)) 21 | { 22 | return $"dc:{partitionKeyValue}"; 23 | } 24 | 25 | return $"{partitionKeyPrefix}:dc:{partitionKeyValue}"; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/semgrep-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | # Scan changed files in PRs, block on new issues only (existing issues ignored) 5 | pull_request: 6 | 7 | push: 8 | branches: ["dev", "main"] 9 | 10 | schedule: 11 | - cron: '23 20 * * 1' 12 | 13 | # Manually trigger the workflow 14 | workflow_dispatch: 15 | 16 | jobs: 17 | semgrep: 18 | name: Scan 19 | permissions: 20 | security-events: write 21 | runs-on: ubuntu-latest 22 | container: 23 | image: returntocorp/semgrep 24 | # Skip any PR created by dependabot to avoid permission issues 25 | if: (github.actor != 'dependabot[bot]') 26 | steps: 27 | # Fetch project source 28 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 29 | 30 | - run: semgrep ci --sarif > semgrep.sarif 31 | env: 32 | SEMGREP_RULES: >- # more at semgrep.dev/explore 33 | p/security-audit 34 | p/secrets 35 | p/owasp-top-ten 36 | 37 | - name: Upload SARIF file for GitHub Advanced Security Dashboard 38 | uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b #v3.29.2 39 | with: 40 | sarif_file: semgrep.sarif 41 | if: always() 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release 2025-11-17 2 | 3 | ### AWS.AspNetCore.DistributedCacheProvider (1.0.1) 4 | * Update AWS SDK Dependencies 5 | 6 | ## Release 2025-07-01 7 | 8 | ### AWS.AspNetCore.DistributedCacheProvider (1.0.0) 9 | * This marks the first stable release of AWS .NET Distributed Cache Provider which is now generally available. 10 | 11 | ## Release 2025-04-28 12 | 13 | ### AWS.AspNetCore.DistributedCacheProvider (0.21.0-preview) 14 | * Updated the .NET SDK dependencies to the latest version GA 4.0.0 15 | 16 | ## Release 2025-04-02 17 | 18 | ### AWS.AspNetCore.DistributedCacheProvider (0.20.0-preview) 19 | * Update AWS SDK to Preview 11 20 | * Remove support for .NET 6 21 | * Marked project as trimmable 22 | * Added SourceLink support 23 | 24 | ## Release 2024-04-20 25 | 26 | ### AWS.AspNetCore.DistributedCacheProvider (0.9.3) 27 | - Update User-Agent string 28 | 29 | ## Release 2023-07-28 30 | 31 | ### AWS.AspNetCore.DistributedCacheProvider (0.9.2) 32 | - Add PackageReadmeFile attribute to show README file in NuGet 33 | 34 | ## Release 2023-07-28 35 | 36 | ### AWS.AspNetCore.DistributedCacheProvider (0.9.1) 37 | - Added the repository README file to the NuGet package 38 | 39 | ## Release 2023-07-06 40 | 41 | ### AWS.AspNetCore.DistributedCacheProvider (0.9.0) 42 | - Initial preview release 43 | -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderUnitTests/ExtensionTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | using Amazon.DynamoDBv2; 4 | using Microsoft.Extensions.Caching.Distributed; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Xunit; 7 | 8 | namespace AWS.DistributedCacheProviderUnitTests 9 | { 10 | public class ExtensionTests 11 | { 12 | /// 13 | /// Tests that our extension method returns a DynamoDBDistributedCache 14 | /// 15 | [Fact] 16 | public void TestExtensionMethodsToReturnValidCache() 17 | { 18 | var serviceContainer = new ServiceCollection(); 19 | var moqClient = new Moq.Mock(); 20 | serviceContainer.AddSingleton(moqClient.Object); 21 | serviceContainer.AddAWSDynamoDBDistributedCache(options => 22 | { 23 | options.TableName = "blah"; 24 | options.CreateTableIfNotExists = false; 25 | }); 26 | var provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(serviceContainer); 27 | var cache = provider.GetService(); 28 | Assert.NotNull(cache); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderUnitTests/AWS.DistributedCacheProviderUnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/issue-regression-labeler.yml: -------------------------------------------------------------------------------- 1 | # Apply potential regression label on issues 2 | name: issue-regression-label 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | jobs: 7 | add-regression-label: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | steps: 12 | - name: Fetch template body 13 | id: check_regression 14 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | TEMPLATE_BODY: ${{ github.event.issue.body }} 18 | with: 19 | script: | 20 | const regressionPattern = /\[x\] Select this option if this issue appears to be a regression\./i; 21 | const template = `${process.env.TEMPLATE_BODY}` 22 | const match = regressionPattern.test(template); 23 | core.setOutput('is_regression', match); 24 | - name: Manage regression label 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | run: | 28 | if [ "${{ steps.check_regression.outputs.is_regression }}" == "true" ]; then 29 | gh issue edit ${{ github.event.issue.number }} --add-label "potential-regression" -R ${{ github.repository }} 30 | else 31 | gh issue edit ${{ github.event.issue.number }} --remove-label "potential-regression" -R ${{ github.repository }} 32 | fi 33 | -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderIntegrationTests/AWS.DistributedCacheProviderIntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/change-file-in-pr.yml: -------------------------------------------------------------------------------- 1 | name: Change File Included in PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, labeled] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | check-files-in-directory: 12 | if: ${{ !contains(github.event.pull_request.labels.*.name, 'Release Not Needed') && !contains(github.event.pull_request.labels.*.name, 'Release PR') }} 13 | name: Change File Included in PR 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout PR code 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 19 | 20 | - name: Get List of Changed Files 21 | id: changed-files 22 | uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c #v45 23 | 24 | - name: Check for Change File(s) in .autover/changes/ 25 | env: 26 | CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} 27 | DIRECTORY: ".autover/changes/" 28 | run: | 29 | if echo "$CHANGED_FILES" | grep -q "$DIRECTORY"; then 30 | echo "✅ One or more change files in '$DIRECTORY' are included in this PR." 31 | else 32 | echo "❌ No change files in '$DIRECTORY' are included in this PR." 33 | echo "Refer to the 'Adding a change file to your contribution branch' section of https://github.com/aws/aws-dotnet-distributed-cache-provider/blob/main/CONTRIBUTING.md" 34 | exit 1 35 | fi 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/aws-ci.yml: -------------------------------------------------------------------------------- 1 | name: AWS CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | - dev 9 | - 'feature/**' 10 | 11 | permissions: 12 | id-token: write 13 | 14 | jobs: 15 | run-ci: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Configure AWS Credentials 19 | uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df #v4 20 | with: 21 | role-to-assume: ${{ secrets.CI_MAIN_TESTING_ACCOUNT_ROLE_ARN }} 22 | role-duration-seconds: 7200 23 | aws-region: us-west-2 24 | - name: Invoke Load Balancer Lambda 25 | id: lambda 26 | shell: pwsh 27 | run: | 28 | aws lambda invoke response.json --function-name "${{ secrets.CI_TESTING_LOAD_BALANCER_LAMBDA_NAME }}" --cli-binary-format raw-in-base64-out --payload '{"Roles": "${{ secrets.CI_TEST_RUNNER_ACCOUNT_ROLES }}", "ProjectName": "${{ secrets.CI_TESTING_CODE_BUILD_PROJECT_NAME }}", "Branch": "${{ github.sha }}"}' 29 | $roleArn=$(cat ./response.json) 30 | "roleArn=$($roleArn -replace '"', '')" >> $env:GITHUB_OUTPUT 31 | - name: Configure Test Runner Credentials 32 | uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df #v4 33 | with: 34 | role-to-assume: ${{ steps.lambda.outputs.roleArn }} 35 | role-duration-seconds: 7200 36 | aws-region: us-west-2 37 | - name: Run Tests on AWS 38 | id: codebuild 39 | uses: aws-actions/aws-codebuild-run-build@4d15a47425739ac2296ba5e7eee3bdd4bfbdd767 #v1.0.18 40 | with: 41 | project-name: ${{ secrets.CI_TESTING_CODE_BUILD_PROJECT_NAME }} 42 | - name: CodeBuild Link 43 | shell: pwsh 44 | run: | 45 | $buildId = "${{ steps.codebuild.outputs.aws-build-id }}" 46 | echo $buildId 47 | -------------------------------------------------------------------------------- /.github/workflows/stale_issues.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | 3 | # Controls when the action will run. 4 | on: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | jobs: 13 | cleanup: 14 | runs-on: ubuntu-latest 15 | name: Stale issue job 16 | steps: 17 | - uses: aws-actions/stale-issue-cleanup@5650b49bcd757a078f6ca06c373d7807b773f9bc #v7.1.0 18 | with: 19 | # Setting messages to an empty string will cause the automation to skip 20 | # that category 21 | ancient-issue-message: We have noticed this issue has not received attention in 1 year. We will close this issue for now. If you think this is in error, please feel free to comment and reopen the issue. 22 | stale-issue-message: This issue has not received a response in 5 days. If you want to keep this issue open, please just leave a comment below and auto-close will be canceled. 23 | 24 | # These labels are required 25 | stale-issue-label: closing-soon 26 | exempt-issue-labels: no-autoclose 27 | stale-pr-label: no-pr-activity 28 | exempt-pr-labels: awaiting-approval 29 | response-requested-label: response-requested 30 | 31 | # Don't set closed-for-staleness label to skip closing very old issues 32 | # regardless of label 33 | closed-for-staleness-label: closed-for-staleness 34 | 35 | # Issue timing 36 | days-before-stale: 5 37 | days-before-close: 2 38 | days-before-ancient: 36500 39 | 40 | # If you don't want to mark a issue as being ancient based on a 41 | # threshold of "upvotes", you can set this here. An "upvote" is 42 | # the total number of +1, heart, hooray, and rocket reactions 43 | # on an issue. 44 | minimum-upvotes-to-exempt: 10 45 | 46 | repo-token: ${{ secrets.GITHUB_TOKEN }} 47 | #loglevel: DEBUG 48 | # Set dry-run to true to not perform label or close actions. 49 | #dry-run: true 50 | 51 | -------------------------------------------------------------------------------- /src/AWS.DistributedCacheProvider/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | using Amazon.DynamoDBv2; 4 | using AWS.DistributedCacheProvider; 5 | using AWS.DistributedCacheProvider.Internal; 6 | using Microsoft.Extensions.Caching.Distributed; 7 | 8 | namespace Microsoft.Extensions.DependencyInjection 9 | { 10 | public static class ExtensionMethods 11 | { 12 | /// 13 | /// Injects as the implementation for . 14 | /// 15 | /// The current ServiceCollection 16 | /// An Action to configure the parameters of for the cache 17 | /// Thrown when one of the required parameters is null 18 | public static IServiceCollection AddAWSDynamoDBDistributedCache(this IServiceCollection services, Action action) 19 | { 20 | if (services == null) 21 | { 22 | throw new ArgumentNullException(nameof(services)); 23 | } 24 | if (action == null) 25 | { 26 | throw new ArgumentNullException(nameof(action)); 27 | } 28 | 29 | // Ensure there is an DynamoDB client added, though using Try in case the user added their own 30 | services.TryAddAWSService(); 31 | 32 | // The TableCreator is an internal dependency 33 | services.AddSingleton(); 34 | 35 | // Configure the Action the user provided 36 | services.AddOptions(); 37 | services.Configure(action); 38 | 39 | // Now that the three required parameters are added, add the cache implementation 40 | services.AddSingleton(); 41 | 42 | return services; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/AWS.DistributedCacheProvider/IDynamoDBTableCreator.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | using Amazon.DynamoDBv2; 4 | 5 | namespace AWS.DistributedCacheProvider 6 | { 7 | public interface IDynamoDBTableCreator 8 | { 9 | /// 10 | /// Tests first to see if Table " exists. 11 | /// If it does exist, check to see if the table is valid to serve as a cache. 12 | /// Requirements are that the table contain a non-composite Hash key of type String 13 | /// If the table does not exist, and is set to true, then create the table 14 | /// When creating a table, TTL is turned on using the name for the TTL column. 15 | /// If is not set, a default value will be used. 16 | /// 17 | /// DynamoDB client. 18 | /// Name of the table. 19 | /// Create the table if it does not exist. 20 | /// When turning on TTL, what column should specifically be used? If left null, a default will be used. 21 | /// When creating a table, what should the partition key attribute name be? If left null, a default will be used. 22 | /// The attribute name of the Table's partition key 23 | public Task CreateTableIfNotExistsAsync(IAmazonDynamoDB client, string tableName, bool create, string TtlAttribute, string partitionKeyAttribute); 24 | 25 | /// 26 | /// Returns the TTL attribute column for this table. 27 | /// 28 | /// DynamoDB client. 29 | /// Name of the table. 30 | /// The name of the TTL column for this table 31 | public Task GetTTLColumnAsync(IAmazonDynamoDB client, string tableName); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderIntegrationTests/IntegrationTestUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | using System.Diagnostics; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace AWS.DistributedCacheProviderIntegrationTests 8 | { 9 | public class IntegrationTestUtils 10 | { 11 | /// 12 | /// Concatenates the calling method's namespace, class name, method name and the UTC timestamp in 13 | /// milliseconds. To be used when generating a DynamoDB Table that needs a name that can be traced to which test 14 | /// generated the table should it not be deleted in the test directly. The UTC timestamp is so that multiple 15 | /// people can run the same test on the same account at the same time and hopefully not have a conflict. 16 | /// 17 | /// The calling methods information in "{namespace}-{class name}-{method name}-{UTC now}" format. 18 | public static string GetFullTestName() 19 | { 20 | //This method is being called from Integration tests. The methods being used here are not returning null. 21 | #pragma warning disable CS8602 // Dereference of a possibly null reference. 22 | var baseMethod = new StackTrace().GetFrame(1).GetMethod().ReflectedType; 23 | var nameSpace = baseMethod.Namespace; 24 | var className = baseMethod.DeclaringType.Name; 25 | var methodName = baseMethod.Name.Split('<', '>')[1]; 26 | #pragma warning restore CS8602 // Dereference of a possibly null reference. 27 | var fullName = $"{nameSpace}-{className}-{methodName}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; 28 | //DynamoDB Table name cannot have special chars 29 | var filteredName = Regex.Replace(fullName, @"[^0-9a-zA-Z]+", ""); 30 | //DynamoDB Table name length must be between 3 and 255 chars 31 | //Check that is does not exceed 255 32 | if(filteredName.Length > 255) 33 | { 34 | return filteredName.Substring(filteredName.Length - 255); 35 | } 36 | //DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() returns a string that is longer than 3 chars. No need to check that case. 37 | else 38 | { 39 | return filteredName; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/AWS.DistributedCacheProvider/AWS.DistributedCacheProvider.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | AWS.DistributedCacheProvider 6 | enable 7 | enable 8 | 1.0.1 9 | AWS.AspNetCore.DistributedCacheProvider 10 | AWS Provider for ASP.NET Core's IDistributedCache 11 | The AWS Distributed Cache provider provides an IDistributedCache implementation backed by DynamoDB. 12 | Amazon Web Services 13 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 14 | AWS;DynamoDB;Caching;IDistributedCache 15 | https://github.com/aws/aws-dotnet-distributed-cache-provider/ 16 | icon.png 17 | https://github.com/aws/aws-dotnet-distributed-cache-provider/ 18 | Amazon Web Services 19 | 20 | true 21 | ..\..\public.snk 22 | 23 | README.md 24 | 25 | true 26 | true 27 | true 28 | snupkg 29 | 30 | true 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | description: Suggest an idea for this project 4 | title: "(short issue description)" 5 | labels: [feature-request, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the feature 12 | description: A clear and concise description of the feature you are proposing. 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: use-case 17 | attributes: 18 | label: Use Case 19 | description: | 20 | Why do you need this feature? For example: "I'm always frustrated when..." 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: solution 25 | attributes: 26 | label: Proposed Solution 27 | description: | 28 | Suggest how to implement the addition or change. Please include prototype/workaround/sketch/reference implementation. 29 | validations: 30 | required: false 31 | - type: textarea 32 | id: other 33 | attributes: 34 | label: Other Information 35 | description: | 36 | Any alternative solutions or features you considered, a more detailed explanation, stack traces, related issues, links for context, etc. 37 | validations: 38 | required: false 39 | - type: checkboxes 40 | id: ack 41 | attributes: 42 | label: Acknowledgements 43 | options: 44 | - label: I may be able to implement this feature request 45 | required: false 46 | - label: This feature might incur a breaking change 47 | required: false 48 | 49 | - type: textarea 50 | id: dotnet-sdk-version 51 | attributes: 52 | label: AWS.AspNetCore.DistributedCacheProvider package version 53 | description: NuGet Package(s) used 54 | placeholder: AWS.AspNetCore.DistributedCacheProvider 0.9.0 55 | validations: 56 | required: true 57 | 58 | - type: input 59 | id: platform-used 60 | attributes: 61 | label: Targeted .NET Platform 62 | description: "Example: .NET Framework 4.7, .NET Core 3.1, .NET 6, etc." 63 | placeholder: .NET Framework 4.7, .NET Core 3.1, .NET 6, etc. 64 | validations: 65 | required: true 66 | 67 | - type: input 68 | id: operating-system 69 | attributes: 70 | label: Operating System and version 71 | description: "Example: Windows 10, OSX Mojave, Ubuntu, AmazonLinux, etc." 72 | placeholder: Windows 10, OSX Mojave, Ubuntu, AmazonLinux, etc. 73 | validations: 74 | required: true 75 | -------------------------------------------------------------------------------- /AWS.DistributedCacheProvider.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32526.322 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.DistributedCacheProvider", "src\AWS.DistributedCacheProvider\AWS.DistributedCacheProvider.csproj", "{BDDB418E-2C8C-40C0-A227-C32E41547798}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.DistributedCacheProviderUnitTests", "test\AWS.DistributedCacheProviderUnitTests\AWS.DistributedCacheProviderUnitTests.csproj", "{CA625C23-4F9F-4FE2-A293-3B8E3B04B5FC}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AWS.DistributedCacheProviderIntegrationTests", "test\AWS.DistributedCacheProviderIntegrationTests\AWS.DistributedCacheProviderIntegrationTests.csproj", "{239349F5-8552-452B-830D-30FE5FBCCABA}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6D3BF1B1-1B57-45B4-91AC-C1993F4B8EDC}" 13 | ProjectSection(SolutionItems) = preProject 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "SampleApp\SampleApp.csproj", "{6ADFDA37-A3FF-40B5-ACC2-34054CFF3962}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {BDDB418E-2C8C-40C0-A227-C32E41547798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {BDDB418E-2C8C-40C0-A227-C32E41547798}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {BDDB418E-2C8C-40C0-A227-C32E41547798}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {BDDB418E-2C8C-40C0-A227-C32E41547798}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {CA625C23-4F9F-4FE2-A293-3B8E3B04B5FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {CA625C23-4F9F-4FE2-A293-3B8E3B04B5FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {CA625C23-4F9F-4FE2-A293-3B8E3B04B5FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {CA625C23-4F9F-4FE2-A293-3B8E3B04B5FC}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {239349F5-8552-452B-830D-30FE5FBCCABA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {239349F5-8552-452B-830D-30FE5FBCCABA}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {239349F5-8552-452B-830D-30FE5FBCCABA}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {239349F5-8552-452B-830D-30FE5FBCCABA}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {6ADFDA37-A3FF-40B5-ACC2-34054CFF3962}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {6ADFDA37-A3FF-40B5-ACC2-34054CFF3962}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {6ADFDA37-A3FF-40B5-ACC2-34054CFF3962}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {6ADFDA37-A3FF-40B5-ACC2-34054CFF3962}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {9A2C41B1-751D-4C28-BB61-97769E6C437C} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | description: Report a bug 4 | title: "(short issue description)" 5 | labels: [bug, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the bug 12 | description: What is the problem? A clear and concise description of the bug. 13 | validations: 14 | required: true 15 | - type: checkboxes 16 | id: regression 17 | attributes: 18 | label: Regression Issue 19 | description: What is a regression? If it worked in a previous version but doesn't in the latest version, it's considered a regression. In this case, please provide specific version number in the report. 20 | options: 21 | - label: Select this option if this issue appears to be a regression. 22 | required: false 23 | - type: textarea 24 | id: expected 25 | attributes: 26 | label: Expected Behavior 27 | description: | 28 | What did you expect to happen? 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: current 33 | attributes: 34 | label: Current Behavior 35 | description: | 36 | What actually happened? 37 | 38 | Please include full errors, uncaught exceptions, stack traces, and relevant logs. 39 | If service responses are relevant, please include wire logs. 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: reproduction 44 | attributes: 45 | label: Reproduction Steps 46 | description: | 47 | Provide a self-contained, concise snippet of code that can be used to reproduce the issue. 48 | For more complex issues provide a repo with the smallest sample that reproduces the bug. 49 | 50 | Avoid including business logic or unrelated code, it makes diagnosis more difficult. 51 | The code sample should be an SSCCE. See http://sscce.org/ for details. In short, please provide a code sample that we can copy/paste, run and reproduce. 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: solution 56 | attributes: 57 | label: Possible Solution 58 | description: | 59 | Suggest a fix/reason for the bug 60 | validations: 61 | required: false 62 | - type: textarea 63 | id: context 64 | attributes: 65 | label: Additional Information/Context 66 | description: | 67 | Anything else that might be relevant for troubleshooting this bug. Providing context helps us come up with a solution that is most useful in the real world. 68 | validations: 69 | required: false 70 | 71 | - type: textarea 72 | id: dotnet-sdk-version 73 | attributes: 74 | label: AWS.AspNetCore.DistributedCacheProvider package version 75 | description: NuGet Package(s) used 76 | placeholder: AWS.AspNetCore.DistributedCacheProvider 0.9.0 77 | validations: 78 | required: true 79 | 80 | - type: input 81 | id: platform-used 82 | attributes: 83 | label: Targeted .NET Platform 84 | description: "Example: .NET Framework 4.7, .NET Core 3.1, .NET 6, etc." 85 | placeholder: .NET Framework 4.7, .NET Core 3.1, .NET 6, etc. 86 | validations: 87 | required: true 88 | 89 | - type: input 90 | id: operating-system 91 | attributes: 92 | label: Operating System and version 93 | description: "Example: Windows 10, OSX Mojave, Ubuntu, AmazonLinux, etc." 94 | placeholder: Windows 10, OSX Mojave, Ubuntu, AmazonLinux, etc. 95 | validations: 96 | required: true 97 | -------------------------------------------------------------------------------- /src/AWS.DistributedCacheProvider/DynamoDBDistributedCacheOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace AWS.DistributedCacheProvider 7 | { 8 | /// 9 | /// Configurable parameters for DynamoDBDistributedCache 10 | /// 11 | public class DynamoDBDistributedCacheOptions : IOptions 12 | { 13 | /// 14 | /// Required parameter. The name of the DynamoDB Table to store cached data. 15 | /// 16 | public string? TableName { get; set; } 17 | 18 | /// 19 | /// If set to true during startup the library will check if the specified table exists. If the 20 | /// table does not exist a new table will be created using on demand provision throughput. 21 | /// This will require extra permissions to call DescribeTable, CreateTable and UpdateTimeToLive 22 | /// service operations. 23 | /// It is recommended to only set to true in development environments. Production environments 24 | /// should create the table before deployment and set this property to false. This will 25 | /// allow production deployments to have less permissions and have faster startup. 26 | /// 27 | /// Default is false 28 | /// 29 | /// 30 | public bool CreateTableIfNotExists { get; set; } 31 | 32 | /// 33 | /// Optional parameter. When true, reads from the underlying DynamoDB table will use consistent reads. 34 | /// Having consistent reads means that any read will be from the latest data in the DynamoDB table. 35 | /// However, using consistent reads requires more read capacity affecting the cost of the DynamoDB table. 36 | /// To reduce cost this property could be set to false but the application must be able to handle 37 | /// a delay after a set operation for the data to come back in a get operation. 38 | /// 39 | /// Default is true. 40 | /// 41 | /// 42 | public bool UseConsistentReads { get; set; } = true; 43 | 44 | /// 45 | /// Name of the TTL column when Table is created here. If this is not set a DescribeTimeToLive service call 46 | /// will be made to determine the partition key's name. To reduce startup time or avoid needing 47 | /// permissions to DescribeTimeToLive this property should be set. 48 | /// 49 | /// When a table is created by this library the TTL attribute is set to "ttl_date". 50 | /// 51 | /// 52 | public string? TTLAttributeName { get; set; } 53 | 54 | /// 55 | /// Name of DynamoDB table's partion key. If this is not set 56 | /// a DescribeTable service call will be made at startup to determine the partition key's name. To 57 | /// reduce startup time or avoid needing permissions to DescribeTable this property should be set. 58 | /// 59 | public string? PartitionKeyName { get; set; } 60 | 61 | /// 62 | /// Optional parameter. Prefix added to value of the partition key stored in DynamoDB. 63 | /// 64 | public string? PartitionKeyPrefix { get; set; } 65 | 66 | DynamoDBDistributedCacheOptions IOptions.Value 67 | { 68 | get { return this; } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /SampleApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | using System.Text.Json; 5 | 6 | namespace SampleApp; 7 | 8 | public class Program 9 | { 10 | /// 11 | /// Session key for the cached forecast data 12 | /// 13 | private const string ForecastCacheKey = "forecast"; 14 | 15 | public static void Main(string[] args) 16 | { 17 | var builder = WebApplication.CreateBuilder(args); 18 | 19 | // Add services to the container. 20 | builder.Services.AddAuthorization(); 21 | 22 | // Add session state support 23 | builder.Services.AddSession(options => 24 | { 25 | // The following option will cache weather forecasts for 30 seconds. 26 | // Reloading a cached forecast will reset this timer. If a cached forecast 27 | // is not requested for 30 seconds it should expire and a new 28 | // forecast will be generated. 29 | options.IdleTimeout = TimeSpan.FromSeconds(30); 30 | options.Cookie.IsEssential = true; 31 | }); 32 | 33 | // Add DynamoDB distributed cache for sessions 34 | builder.Services.AddAWSDynamoDBDistributedCache(options => 35 | { 36 | options.TableName = "weather-sessions-cache"; 37 | options.UseConsistentReads = true; 38 | options.PartitionKeyName = "id"; 39 | options.TTLAttributeName = "cache_ttl"; 40 | 41 | // The following option will create a table with the above name if it doesn't 42 | // already exist in the account corresponding to the detected credentials. 43 | // 44 | // For production we recommend setting this to false and using a preexisting 45 | // table to reduce the startup time and require fewer permissions. 46 | options.CreateTableIfNotExists = true; 47 | }); 48 | 49 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 50 | builder.Services.AddEndpointsApiExplorer(); 51 | builder.Services.AddSwaggerGen(); 52 | 53 | var app = builder.Build(); 54 | 55 | // Configure the HTTP request pipeline. 56 | if (app.Environment.IsDevelopment()) 57 | { 58 | app.UseSwagger(); 59 | app.UseSwaggerUI(); 60 | } 61 | 62 | app.UseHttpsRedirection(); 63 | 64 | app.UseAuthorization(); 65 | 66 | app.UseSession(); 67 | 68 | var summaries = new[] 69 | { 70 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 71 | }; 72 | 73 | app.MapGet("/", () => "Welcome to the Amazon DynamoDB distributed caching sample! GET /weatherforecast to test the caching."); 74 | 75 | app.MapGet("/weatherforecast", (HttpContext httpContext) => 76 | { 77 | // First check if there is a cached forecast stored in the session state 78 | if (httpContext.Session.TryGetValue(ForecastCacheKey, out var serializedForecast)) 79 | { 80 | // If there is, deserialize it and return it 81 | var cachedForecast = JsonSerializer.Deserialize(serializedForecast); 82 | 83 | if (cachedForecast != null) 84 | { 85 | return cachedForecast; 86 | } 87 | } 88 | 89 | // Otherwise if we made it here, generate a new forecast 90 | var forecast = new WeatherForecast 91 | { 92 | DateForecastWasGenerated = DateTimeOffset.UtcNow, 93 | TemperatureC = Random.Shared.Next(-20, 55), 94 | Summary = summaries[Random.Shared.Next(summaries.Length)] 95 | }; 96 | 97 | // Save the new forecast in the session state and then return it 98 | httpContext.Session.Set(ForecastCacheKey, JsonSerializer.SerializeToUtf8Bytes(forecast)); 99 | return forecast; 100 | }) 101 | .WithName("GetWeatherForecast"); 102 | 103 | app.Run(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /.github/workflows/create-release-pr.yml: -------------------------------------------------------------------------------- 1 | # This GitHub Workflow will create a new release branch that contains the updated C# project versions and changelog. 2 | # The workflow will also create a PR that targets `dev` from the release branch. 3 | name: Create Release PR 4 | 5 | # This workflow is manually triggered when in preparation for a release. The workflow should be dispatched from the `dev` branch. 6 | on: 7 | workflow_dispatch: 8 | inputs: 9 | OVERRIDE_VERSION: 10 | description: "Override Version" 11 | type: string 12 | required: false 13 | 14 | permissions: 15 | id-token: write 16 | 17 | jobs: 18 | release-pr: 19 | name: Release PR 20 | runs-on: ubuntu-latest 21 | 22 | env: 23 | INPUT_OVERRIDE_VERSION: ${{ github.event.inputs.OVERRIDE_VERSION }} 24 | 25 | steps: 26 | # Assume an AWS Role that provides access to the Access Token 27 | - name: Configure AWS Credentials 28 | uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df #v4 29 | with: 30 | role-to-assume: ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_ROLE_ARN }} 31 | aws-region: us-west-2 32 | # Retrieve the Access Token from Secrets Manager 33 | - name: Retrieve secret from AWS Secrets Manager 34 | uses: aws-actions/aws-secretsmanager-get-secrets@5e19ff380d035695bdd56bbad320ca535c9063f2 #v2.0.9 35 | with: 36 | secret-ids: | 37 | AWS_SECRET, ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_NAME }} 38 | parse-json-secrets: true 39 | # Checkout a full clone of the repo 40 | - name: Checkout 41 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 42 | with: 43 | fetch-depth: '0' 44 | token: ${{ env.AWS_SECRET_TOKEN }} 45 | # Install .NET8 which is needed for AutoVer 46 | - name: Setup .NET 8.0 47 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 #v4.3.1 48 | with: 49 | dotnet-version: 8.0.x 50 | # Install AutoVer to automate versioning and changelog creation 51 | - name: Install AutoVer 52 | run: dotnet tool install --global AutoVer --version 0.0.25 53 | # Set up a git user to be able to run git commands later on 54 | - name: Setup Git User 55 | run: | 56 | git config --global user.email "github-aws-sdk-dotnet-automation@amazon.com" 57 | git config --global user.name "aws-sdk-dotnet-automation" 58 | # Create the release branch which will contain the version changes and updated changelog 59 | - name: Create Release Branch 60 | id: create-release-branch 61 | run: | 62 | branch=releases/next-release 63 | git checkout -b $branch 64 | echo "BRANCH=$branch" >> $GITHUB_OUTPUT 65 | # Update the version of projects based on the change files 66 | - name: Increment Version 67 | run: autover version 68 | if: env.INPUT_OVERRIDE_VERSION == '' 69 | # Update the version of projects based on the override version 70 | - name: Increment Version 71 | run: autover version --use-version "$INPUT_OVERRIDE_VERSION" 72 | if: env.INPUT_OVERRIDE_VERSION != '' 73 | # Update the changelog based on the change files 74 | - name: Update Changelog 75 | run: autover changelog 76 | # Push the release branch up as well as the created tag 77 | - name: Push Changes 78 | run: | 79 | branch=${{ steps.create-release-branch.outputs.BRANCH }} 80 | git push origin $branch 81 | git push origin $branch --tags 82 | # Get the release name that will be used to create a PR 83 | - name: Read Release Name 84 | id: read-release-name 85 | run: | 86 | version=$(autover changelog --release-name) 87 | echo "VERSION=$version" >> $GITHUB_OUTPUT 88 | # Get the changelog that will be used to create a PR 89 | - name: Read Changelog 90 | id: read-changelog 91 | run: | 92 | changelog=$(autover changelog --output-to-console) 93 | echo "CHANGELOG<> "$GITHUB_OUTPUT" 94 | # Create the Release PR and label it 95 | - name: Create Pull Request 96 | env: 97 | GITHUB_TOKEN: ${{ env.AWS_SECRET_TOKEN }} 98 | run: | 99 | pr_url="$(gh pr create --title "${{ steps.read-release-name.outputs.VERSION }}" --body "${{ steps.read-changelog.outputs.CHANGELOG }}" --base dev --head ${{ steps.create-release-branch.outputs.BRANCH }})" 100 | gh label create "Release PR" --description "A Release PR that includes versioning and changelog changes" -c "#FF0000" -f 101 | gh pr edit $pr_url --add-label "Release PR" 102 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | ## Adding a `change file` to your contribution branch 43 | 44 | Each contribution branch should include a `change file` that contains a changelog message for each project that has been updated, as well as the type of increment to perform for those changes when versioning the project. 45 | 46 | A `change file` looks like the following example: 47 | ```json 48 | { 49 | "Projects": [ 50 | { 51 | "Name": "AWS.AspNetCore.DistributedCacheProvider", 52 | "Type": "Patch", 53 | "ChangelogMessages": [ 54 | "Fixed an issue causing a failure somewhere" 55 | ] 56 | } 57 | ] 58 | } 59 | ``` 60 | The `change file` lists all the modified projects, the changelog message for each project as well as the increment type. 61 | 62 | These files are located in the repo at .autover/changes/ 63 | 64 | You can use the `AutoVer` tool to create the change file. You can install it using the following command: 65 | ``` 66 | dotnet tool install -g AutoVer 67 | ``` 68 | 69 | You can create the `change file` using the following command: 70 | ``` 71 | autover change --project-name "AWS.AspNetCore.DistributedCacheProvider" -m "Fixed an issue causing a failure somewhere 72 | ``` 73 | Note: Make sure to run the command from the root of the repository. 74 | 75 | You can update the command to specify which project you are updating. 76 | The available projects are: 77 | * AWS.AspNetCore.DistributedCacheProvider 78 | 79 | The possible increment types are: 80 | * Patch 81 | * Minor 82 | * Major 83 | 84 | Note: You do not need to create a new `change file` for every changelog message or project within your branch. You can create one `change file` that contains all the modified projects and the changelog messages. 85 | 86 | ## Finding contributions to work on 87 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 88 | 89 | 90 | ## Code of Conduct 91 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 92 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 93 | opensource-codeofconduct@amazon.com with any additional questions or comments. 94 | 95 | 96 | ## Security issue notifications 97 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 98 | 99 | 100 | ## Licensing 101 | 102 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 103 | -------------------------------------------------------------------------------- /src/AWS.DistributedCacheProvider/Internal/DynamoDBCacheProviderHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Amazon.DynamoDBv2.Model; 10 | using Microsoft.Extensions.Caching.Distributed; 11 | 12 | namespace AWS.DistributedCacheProvider.Internal 13 | { 14 | /// 15 | /// A helper class that calculates TTL related information for 16 | /// This class is not meant to be called directly by a client, it is only kept public for testing purposes. 17 | /// If you need to rely on this class, consider opening a 18 | /// feature request 19 | /// 20 | public class DynamoDBCacheProviderHelper 21 | { 22 | /// 23 | /// Calculates the absolute Time To Live (TTL) given the 24 | /// 25 | /// A which is used to calculate the TTL deadline of an Item 26 | /// An which contains either the absolute deadline TTL or nothing. 27 | /// When the calculated absolute deadline is in the past. 28 | public static AttributeValue CalculateTTLDeadline(DistributedCacheEntryOptions options) 29 | { 30 | if (options.AbsoluteExpiration == null && options.AbsoluteExpirationRelativeToNow == null) 31 | { 32 | return new AttributeValue { NULL = true }; 33 | } 34 | else if (options.AbsoluteExpiration != null && options.AbsoluteExpirationRelativeToNow == null) 35 | { 36 | var ttl = (DateTimeOffset)options.AbsoluteExpiration; 37 | var now = DateTimeOffset.UtcNow; 38 | if (now.CompareTo(ttl) > 0)//if ttl is before current time 39 | { 40 | throw new ArgumentOutOfRangeException("AbsoluteExpiration must be in the future."); 41 | } 42 | else 43 | { 44 | return new AttributeValue { N = ttl.ToUnixTimeSeconds().ToString() }; 45 | } 46 | } 47 | else //AbsoluteExpirationRelativeToNow is not null, regardless of what AbsoluteExpiration is set to, we prefer AbsoluteExpirationRelativeToNow 48 | { 49 | var ttl = DateTimeOffset.UtcNow.Add((TimeSpan)options.AbsoluteExpirationRelativeToNow!).ToUnixTimeSeconds(); 50 | return new AttributeValue { N = ttl.ToString() }; 51 | } 52 | } 53 | 54 | /// 55 | /// Calculates the TTL. 56 | /// 57 | /// A which is used to calculate the TTL of an Item 58 | /// An containting the TTL 59 | public static AttributeValue CalculateTTL(DistributedCacheEntryOptions options) 60 | { 61 | //if the sliding window is present, then now + window 62 | if (options.SlidingExpiration != null) 63 | { 64 | var ttl = DateTimeOffset.UtcNow.Add(((TimeSpan)options.SlidingExpiration)); 65 | //Cannot be later than the deadline 66 | var absoluteTTL = CalculateTTLDeadline(options); 67 | if (absoluteTTL.NULL is not null && (bool)absoluteTTL.NULL) 68 | { 69 | return new AttributeValue { N = ttl.ToUnixTimeSeconds().ToString() }; 70 | } 71 | else //return smaller of the two. Either the TTL based on the sliding window or the deadline 72 | { 73 | if (long.Parse(absoluteTTL.N) < ttl.ToUnixTimeSeconds()) 74 | { 75 | return absoluteTTL; 76 | } 77 | else 78 | { 79 | return new AttributeValue { N = ttl.ToUnixTimeSeconds().ToString() }; 80 | } 81 | } 82 | } 83 | else //just return the absolute TTL 84 | { 85 | return CalculateTTLDeadline(options); 86 | } 87 | } 88 | 89 | /// 90 | /// Returns the sliding window of the TTL 91 | /// 92 | /// 93 | /// An which either contains a string version of the sliding window 94 | /// or nothing 95 | public static AttributeValue CalculateSlidingWindow(DistributedCacheEntryOptions options) 96 | { 97 | if (options.SlidingExpiration != null) 98 | { 99 | return new AttributeValue { S = options.SlidingExpiration.ToString() }; 100 | } 101 | else 102 | { 103 | return new AttributeValue { NULL = true }; 104 | } 105 | } 106 | 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /.github/workflows/sync-main-dev.yml: -------------------------------------------------------------------------------- 1 | 2 | # This GitHub Workflow is designed to run automatically after the Release PR, which was created by the `Create Release PR` workflow, is closed. 3 | # This workflow has 2 jobs. One will run if the `Release PR` is successfully merged, indicating that a release should go out. 4 | # The other will run if the `Release PR` was closed and a release is not intended to go out. 5 | name: Sync 'dev' and 'main' 6 | 7 | # The workflow will automatically be triggered when any PR is closed. 8 | on: 9 | pull_request: 10 | types: [closed] 11 | 12 | permissions: 13 | contents: write 14 | id-token: write 15 | 16 | jobs: 17 | # This job will check if the PR was successfully merged, it's source branch is `releases/next-release` and target branch is `dev`. 18 | # This indicates that the merged PR was the `Release PR`. 19 | # This job will synchronize `dev` and `main`, create a GitHub Release and delete the `releases/next-release` branch. 20 | sync-dev-and-main: 21 | name: Sync dev and main 22 | if: | 23 | github.event.pull_request.merged == true && 24 | github.event.pull_request.head.ref == 'releases/next-release' && 25 | github.event.pull_request.base.ref == 'dev' 26 | runs-on: ubuntu-latest 27 | steps: 28 | # Assume an AWS Role that provides access to the Access Token 29 | - name: Configure AWS Credentials 30 | uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df #v4 31 | with: 32 | role-to-assume: ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_ROLE_ARN }} 33 | aws-region: us-west-2 34 | # Retrieve the Access Token from Secrets Manager 35 | - name: Retrieve secret from AWS Secrets Manager 36 | uses: aws-actions/aws-secretsmanager-get-secrets@5e19ff380d035695bdd56bbad320ca535c9063f2 #v2.0.9 37 | with: 38 | secret-ids: | 39 | AWS_SECRET, ${{ secrets.RELEASE_WORKFLOW_ACCESS_TOKEN_NAME }} 40 | parse-json-secrets: true 41 | # Checkout a full clone of the repo 42 | - name: Checkout code 43 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 44 | with: 45 | ref: dev 46 | fetch-depth: 0 47 | token: ${{ env.AWS_SECRET_TOKEN }} 48 | # Install .NET8 which is needed for AutoVer 49 | - name: Setup .NET 8.0 50 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 #v4.3.1 51 | with: 52 | dotnet-version: 8.0.x 53 | # Install AutoVer which is needed to retrieve information about the current release. 54 | - name: Install AutoVer 55 | run: dotnet tool install --global AutoVer --version 0.0.25 56 | # Set up a git user to be able to run git commands later on 57 | - name: Setup Git User 58 | run: | 59 | git config --global user.email "github-aws-sdk-dotnet-automation@amazon.com" 60 | git config --global user.name "aws-sdk-dotnet-automation" 61 | # Retrieve the release name which is needed for the GitHub Release 62 | - name: Read Release Name 63 | id: read-release-name 64 | run: | 65 | version=$(autover changelog --release-name) 66 | echo "VERSION=$version" >> $GITHUB_OUTPUT 67 | # Retrieve the tag name which is needed for the GitHub Release 68 | - name: Read Tag Name 69 | id: read-tag-name 70 | run: | 71 | tag=$(autover changelog --tag-name) 72 | echo "TAG=$tag" >> $GITHUB_OUTPUT 73 | # Retrieve the changelog which is needed for the GitHub Release 74 | - name: Read Changelog 75 | id: read-changelog 76 | run: | 77 | changelog=$(autover changelog --output-to-console) 78 | echo "CHANGELOG<> "$GITHUB_OUTPUT" 79 | # Merge dev into main in order to synchronize the 2 branches 80 | - name: Merge dev to main 81 | run: | 82 | git fetch origin 83 | git checkout main 84 | git merge dev 85 | git push origin main 86 | # Create the GitHub Release 87 | - name: Create GitHub Release 88 | env: 89 | GITHUB_TOKEN: ${{ env.AWS_SECRET_TOKEN }} 90 | run: | 91 | gh release create "${{ steps.read-tag-name.outputs.TAG }}" --title "${{ steps.read-release-name.outputs.VERSION }}" --notes "${{ steps.read-changelog.outputs.CHANGELOG }}" 92 | # Delete the `releases/next-release` branch 93 | - name: Clean up 94 | run: | 95 | git fetch origin 96 | git push origin --delete releases/next-release 97 | # This job will check if the PR was closed, it's source branch is `releases/next-release` and target branch is `dev`. 98 | # This indicates that the closed PR was the `Release PR`. 99 | # This job will delete the tag created by AutoVer and the release branch. 100 | clean-up-closed-release: 101 | name: Clean up closed release 102 | if: | 103 | github.event.pull_request.merged == false && 104 | github.event.pull_request.head.ref == 'releases/next-release' && 105 | github.event.pull_request.base.ref == 'dev' 106 | runs-on: ubuntu-latest 107 | steps: 108 | # Checkout a full clone of the repo 109 | - name: Checkout code 110 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 111 | with: 112 | ref: releases/next-release 113 | fetch-depth: 0 114 | # Install .NET8 which is needed for AutoVer 115 | - name: Setup .NET 8.0 116 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 #v4.3.1 117 | with: 118 | dotnet-version: 8.0.x 119 | # Install AutoVer which is needed to retrieve information about the current release. 120 | - name: Install AutoVer 121 | run: dotnet tool install --global AutoVer --version 0.0.25 122 | # Set up a git user to be able to run git commands later on 123 | - name: Setup Git User 124 | run: | 125 | git config --global user.email "github-aws-sdk-dotnet-automation@amazon.com" 126 | git config --global user.name "aws-sdk-dotnet-automation" 127 | # Retrieve the tag name to be deleted 128 | - name: Read Tag Name 129 | id: read-tag-name 130 | run: | 131 | tag=$(autover changelog --tag-name) 132 | echo "TAG=$tag" >> $GITHUB_OUTPUT 133 | # Delete the tag created by AutoVer and the release branch 134 | - name: Clean up 135 | run: | 136 | git fetch origin 137 | git push --delete origin ${{ steps.read-tag-name.outputs.TAG }} 138 | git push origin --delete releases/next-release 139 | -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderUnitTests/DynamoDBDistributedCacheHelperTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | using AWS.DistributedCacheProvider.Internal; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | using Xunit; 7 | 8 | namespace AWS.DistributedCacheProviderUnitTests 9 | { 10 | public class DynamoDBDistributedCacheHelperTests 11 | { 12 | /*CalculateSlidingWindow Tests*/ 13 | [Fact] 14 | public void CalculateSlidingWindow_NoWindow_NullAttributeReturn() 15 | { 16 | var ret = DynamoDBCacheProviderHelper.CalculateSlidingWindow(new DistributedCacheEntryOptions()); 17 | Assert.True(ret.NULL); 18 | } 19 | 20 | [Fact] 21 | public void CalculateSlidingWindow_YesWindow_ReturnSerializedSlidingWindow() 22 | { 23 | var window = new TimeSpan(12, 30, 21); 24 | var ret = DynamoDBCacheProviderHelper.CalculateSlidingWindow(new DistributedCacheEntryOptions 25 | { 26 | SlidingExpiration = window 27 | }); 28 | Assert.Equal(window, TimeSpan.Parse(ret.S)); 29 | } 30 | 31 | /*CalculateTTL Tests*/ 32 | [Fact] 33 | public void CalculateTTL_NoSlidingWindow_ReturnDeadline() 34 | { 35 | var options = new DistributedCacheEntryOptions 36 | { 37 | AbsoluteExpirationRelativeToNow = new TimeSpan(12, 31, 2) 38 | }; 39 | var deadline = DynamoDBCacheProviderHelper.CalculateTTLDeadline(options); 40 | var ttl = DynamoDBCacheProviderHelper.CalculateTTL(options); 41 | Assert.Null(deadline.NULL); 42 | Assert.Null(ttl.NULL); 43 | Assert.Equal(ttl.N, deadline.N); 44 | } 45 | 46 | [Fact] 47 | public void CalculateTTL_NoDeadlineYesWindow_ReturnNowPlusWindow() 48 | { 49 | var window = new TimeSpan(12, 31, 2); 50 | var options = new DistributedCacheEntryOptions 51 | { 52 | SlidingExpiration = window 53 | }; 54 | var ret = DynamoDBCacheProviderHelper.CalculateTTL(options); 55 | Assert.True( 56 | Math.Abs(DateTimeOffset.UtcNow.Add(window).ToUnixTimeSeconds() - double.Parse(ret.N)) 57 | < 100); 58 | } 59 | 60 | [Fact] 61 | public void CalculateTTL_YesDeadlineYesWindow_ReturnAtDeadline() 62 | { 63 | var hoursToDeadline = 9; 64 | var window = new TimeSpan(12, 0, 0); 65 | var deadline = new TimeSpan(hoursToDeadline, 0, 0); 66 | var options = new DistributedCacheEntryOptions 67 | { 68 | SlidingExpiration = window, 69 | AbsoluteExpirationRelativeToNow = deadline 70 | }; 71 | var ret = DynamoDBCacheProviderHelper.CalculateTTL(options); 72 | //ttl should be only 9 hours from now, not 12 73 | Assert.True( 74 | Math.Abs(DateTimeOffset.UtcNow.AddHours(hoursToDeadline).ToUnixTimeSeconds() - double.Parse(ret.N)) 75 | < 100); 76 | } 77 | 78 | [Fact] 79 | public void CalculateTTL_YesDeadlineYesWindow_ReturnWithinDeadline() 80 | { 81 | var hoursToDeadline = 24; 82 | var hoursToWindow = 12; 83 | var window = new TimeSpan(hoursToWindow, 0, 0); 84 | var deadline = new TimeSpan(hoursToDeadline, 0, 0); 85 | var options = new DistributedCacheEntryOptions 86 | { 87 | SlidingExpiration = window, 88 | AbsoluteExpirationRelativeToNow = deadline 89 | }; 90 | var ret = DynamoDBCacheProviderHelper.CalculateTTL(options); 91 | //ttl should be only 12 hours from now, not 24 92 | Assert.True( 93 | Math.Abs(DateTimeOffset.UtcNow.AddHours(hoursToWindow).ToUnixTimeSeconds() - double.Parse(ret.N)) 94 | < 100); 95 | } 96 | 97 | /*CalculateTTLDeadline Tests*/ 98 | [Fact] 99 | public void CalculateTTLDeadline_NullOptions_ReturnNullAttribute() 100 | { 101 | Assert.True(DynamoDBCacheProviderHelper.CalculateTTLDeadline(new DistributedCacheEntryOptions()).NULL); 102 | } 103 | 104 | [Fact] 105 | public void CalculateTTLDeadline_BothOptions_PreferRelativeOption() 106 | { 107 | var relative = new TimeSpan(12, 0, 0); 108 | var Absolute = DateTimeOffset.UtcNow.AddHours(24); 109 | var options = new DistributedCacheEntryOptions 110 | { 111 | AbsoluteExpiration = Absolute, 112 | AbsoluteExpirationRelativeToNow = relative 113 | }; 114 | var ret = DynamoDBCacheProviderHelper.CalculateTTLDeadline(options); 115 | //assert the deadline is approx 12 hours from now, not 24 116 | Assert.True( 117 | Math.Abs(DateTimeOffset.UtcNow.AddHours(12).ToUnixTimeSeconds() - double.Parse(ret.N)) 118 | < 100); 119 | } 120 | 121 | [Fact] 122 | public void CalculateTTLDeadline_RelativeOptionOnly_returnNowPlusRelative() 123 | { 124 | var relative = new TimeSpan(12, 0, 0); 125 | var options = new DistributedCacheEntryOptions 126 | { 127 | AbsoluteExpirationRelativeToNow = relative 128 | }; 129 | var ret = DynamoDBCacheProviderHelper.CalculateTTLDeadline(options); 130 | //assert the deadline is approx 12 hours from now. 131 | //Effectively the same logic as CalculateTTLDeadline_BothOptions_PreferRelativeOption 132 | Assert.True( 133 | Math.Abs(DateTimeOffset.UtcNow.AddHours(12).ToUnixTimeSeconds() - double.Parse(ret.N)) 134 | < 100); 135 | } 136 | 137 | [Fact] 138 | public void CalculateTTLDeadline_ObsoluteOptionOnly_OptionIsInPast_Exception() 139 | { 140 | var absolute = DateTimeOffset.UtcNow.AddHours(-24); 141 | var options = new DistributedCacheEntryOptions 142 | { 143 | AbsoluteExpiration = absolute 144 | }; 145 | Assert.Throws(() => DynamoDBCacheProviderHelper.CalculateTTLDeadline(options)); 146 | } 147 | 148 | [Fact] 149 | public void CalculateTTLDeadline_ObsoluteOptionOnly_OptionIsInFuture_ReturnAbsoluteDeadline() 150 | { 151 | var absolute = DateTimeOffset.UtcNow.AddHours(24); 152 | var options = new DistributedCacheEntryOptions 153 | { 154 | AbsoluteExpiration = absolute 155 | }; 156 | var ret = DynamoDBCacheProviderHelper.CalculateTTLDeadline(options); 157 | //assert the deadline is approx 24 hours from now. 158 | Assert.True( 159 | Math.Abs(DateTimeOffset.UtcNow.AddHours(24).ToUnixTimeSeconds() - double.Parse(ret.N)) 160 | < 100); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /src/AWS.DistributedCacheProvider/Internal/DynamoDBTableCreator.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | using Amazon.DynamoDBv2; 5 | using Amazon.DynamoDBv2.Model; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Logging.Abstractions; 8 | 9 | namespace AWS.DistributedCacheProvider.Internal 10 | { 11 | /// 12 | /// A helper class that manages DynamoDB interactions related to Table creation, loading, and validation. 13 | /// This class is not meant to be called directly by a client, it is only kept public for testing purposes 14 | /// If you need to rely on this class, consider opening a 15 | /// feature request 16 | /// 17 | public class DynamoDBTableCreator : IDynamoDBTableCreator 18 | { 19 | public const string DEFAULT_PARTITION_KEY = "id"; 20 | 21 | private readonly ILogger _logger; 22 | 23 | public DynamoDBTableCreator(ILoggerFactory? loggerFactory = null) 24 | { 25 | if (loggerFactory != null) 26 | { 27 | _logger = loggerFactory.CreateLogger(); 28 | } 29 | else 30 | { 31 | _logger = NullLoggerFactory.Instance.CreateLogger(); 32 | } 33 | } 34 | 35 | /// 36 | public async Task CreateTableIfNotExistsAsync(IAmazonDynamoDB client, string tableName, bool create, string? ttlAttribute, string? partitionKeyAttribute) 37 | { 38 | _logger.LogDebug("Create If Not Exists called. Table name: {tableName}, Create If Not Exists: {create}.", tableName, create); 39 | try 40 | { 41 | //test if table already exists 42 | var resp = await client.DescribeTableAsync(new DescribeTableRequest 43 | { 44 | TableName = tableName 45 | }); 46 | _logger.LogDebug("Table does exist. Validating"); 47 | var partitionKey = ValidateTable(resp.Table); 48 | _logger.LogInformation("DynamoDB distributed cache provider configured to use table {tableName}. Partition key is {partitionKey}", tableName, partitionKey); 49 | return partitionKey; 50 | } 51 | catch (ResourceNotFoundException) //thrown when table does not already exist 52 | { 53 | _logger.LogDebug("Table does not exist"); 54 | if (create) 55 | { 56 | var partitionKey = await CreateTableAsync(client, tableName, ttlAttribute, partitionKeyAttribute); 57 | _logger.LogInformation("DynamoDB distributed cache provider created table {tableName}. Partition key is {partitionKey}", tableName, partitionKey); 58 | return partitionKey; 59 | } 60 | else 61 | { 62 | throw new InvalidTableException($"Table {tableName} was not found to be used as cache and autocreate is turned off."); 63 | } 64 | } 65 | } 66 | 67 | /// 68 | /// Verifies that the key schema for this table is a non-composite, Hash key of type String. 69 | /// 70 | /// A table description 71 | /// Thrown when key Schema is invalid 72 | private string ValidateTable(TableDescription description) 73 | { 74 | var partitionKeyName = ""; 75 | var keySchema = description.KeySchema ?? new List(); 76 | foreach (var key in keySchema) 77 | { 78 | if (key.KeyType.Equals(KeyType.RANGE)) 79 | { 80 | throw new InvalidTableException($"Table {description.TableName} cannot be used as a cache because it contains" + 81 | " a range key in its schema. Cache requires a non-composite Hash key of type String."); 82 | } 83 | else //We know the key is of type Hash 84 | { 85 | var attributeDefinitions = description.AttributeDefinitions ?? new List(); 86 | foreach (var attributeDef in attributeDefinitions) 87 | { 88 | if (attributeDef.AttributeName.Equals(key.AttributeName) && attributeDef.AttributeType != ScalarAttributeType.S) 89 | { 90 | throw new InvalidTableException($"Table {description.TableName} cannot be used as a cache because hash key " + 91 | "is not a string. Cache requires a non-composite Hash key of type String."); 92 | } 93 | } 94 | } 95 | //If there is an element in the key schema that is of type Hash and is a string, it must be the partition key 96 | partitionKeyName = key.AttributeName; 97 | } 98 | return partitionKeyName; 99 | } 100 | 101 | /// 102 | /// Creates a table that is usable as a cache. 103 | /// 104 | /// DynamoDB client 105 | /// Table name 106 | /// TTL attribute name 107 | private async Task CreateTableAsync(IAmazonDynamoDB client, string tableName, string? ttlAttribute, string? partitionKeyAttribute) 108 | { 109 | var partitionKey = partitionKeyAttribute ?? DEFAULT_PARTITION_KEY; 110 | var createRequest = new CreateTableRequest 111 | { 112 | TableName = tableName, 113 | KeySchema = new List 114 | { 115 | new KeySchemaElement 116 | { 117 | AttributeName = partitionKey, 118 | KeyType = KeyType.HASH 119 | } 120 | }, 121 | AttributeDefinitions = new List 122 | { 123 | new AttributeDefinition 124 | { 125 | AttributeName = partitionKey, 126 | AttributeType = ScalarAttributeType.S 127 | } 128 | }, 129 | BillingMode = BillingMode.PAY_PER_REQUEST 130 | }; 131 | 132 | await client.CreateTableAsync(createRequest); 133 | 134 | // Wait untill table is active 135 | var isActive = false; 136 | while (!isActive) 137 | { 138 | var tableStatus = (await (client.DescribeTableAsync(new DescribeTableRequest 139 | { 140 | TableName = tableName 141 | }))).Table.TableStatus; 142 | 143 | if (tableStatus == TableStatus.ACTIVE) 144 | { 145 | isActive = true; 146 | } 147 | else 148 | { 149 | await Task.Delay(5000); 150 | } 151 | } 152 | 153 | await client.UpdateTimeToLiveAsync(new UpdateTimeToLiveRequest 154 | { 155 | TableName = tableName, 156 | TimeToLiveSpecification = new TimeToLiveSpecification 157 | { 158 | AttributeName = ttlAttribute ?? DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME, 159 | Enabled = true 160 | } 161 | }); 162 | 163 | return partitionKey; 164 | } 165 | 166 | /// 167 | public async Task GetTTLColumnAsync(IAmazonDynamoDB client, string tableName) 168 | { 169 | var ttlDesc = (await client.DescribeTimeToLiveAsync(new DescribeTimeToLiveRequest { TableName = tableName })).TimeToLiveDescription; 170 | if (ttlDesc.TimeToLiveStatus == TimeToLiveStatus.DISABLED || ttlDesc.TimeToLiveStatus == TimeToLiveStatus.DISABLING) 171 | { 172 | _logger.LogWarning("Distributed cache table {tableName} has Time to Live (TTL) disabled. Items will never be deleted " + 173 | "automatically. It is recommended to enable TTL for the table to remove stale cached data.", tableName); 174 | } 175 | 176 | return ttlDesc.AttributeName ?? DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderUnitTests/DynamoDBDistributedCacheTableTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.DynamoDBv2.Model; 3 | using AWS.DistributedCacheProvider; 4 | using AWS.DistributedCacheProvider.Internal; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace AWS.DistributedCacheProviderUnitTests 9 | { 10 | /// 11 | /// The purpose of this test class is to test the DynamoDBTableCreator class. 12 | /// 13 | public class DynamoDBDistributedCacheTableTests 14 | { 15 | /// 16 | /// Describe table throws a ResourceNotFoundException, and the create table boolean is turned off. Expect Exception 17 | /// 18 | [Fact] 19 | public async Task CreateIfNotExists_TableDoesNotExist_DoNotCreate_ExpectException() 20 | { 21 | var moqClient = new Moq.Mock(); 22 | //mock describe table that table does not exist 23 | moqClient.Setup(x => x.DescribeTableAsync(It.IsAny(), It.IsAny())) 24 | .Throws(new ResourceNotFoundException("")); 25 | var creator = new DynamoDBTableCreator(); 26 | //create table, set create boolean to false. 27 | await Assert.ThrowsAsync(() => creator.CreateTableIfNotExistsAsync(moqClient.Object, "", false, "", "")); 28 | } 29 | 30 | /// 31 | /// Describe table throws a ResourceNotFoundException, and the create table is turned on. Do not expect Exception 32 | /// 33 | [Fact] 34 | public async Task CreateIfNotExists_TableDoesNotExist_Create_NoException() 35 | { 36 | var moqClient = new Moq.Mock(); 37 | //mock describe table that table does not exist. Then the next time return an active table 38 | moqClient.SetupSequence(x => x.DescribeTableAsync(It.IsAny(), It.IsAny())) 39 | .Throws(new ResourceNotFoundException("")) 40 | .Returns(Task.FromResult(new DescribeTableResponse 41 | { 42 | Table = new TableDescription 43 | { 44 | TableStatus = TableStatus.ACTIVE 45 | } 46 | })); 47 | //mock create table that it returns immediately 48 | moqClient.Setup(x => x.CreateTableAsync(It.IsAny(), It.IsAny())); 49 | var creator = new DynamoDBTableCreator(); 50 | //create table, set create boolean to true. 51 | await creator.CreateTableIfNotExistsAsync(moqClient.Object, "", true, "", ""); 52 | } 53 | 54 | /// 55 | /// Describe table returns an existing table. Table is valid to be used for a cache. No exception 56 | /// 57 | [Fact] 58 | public async Task TableExists_Valid() 59 | { 60 | var keyName = "key"; 61 | var moqClient = new Moq.Mock(); 62 | moqClient.Setup(x => x.DescribeTableAsync(It.IsAny(), It.IsAny())) 63 | .Returns(Task.FromResult(new DescribeTableResponse 64 | { 65 | Table = new TableDescription 66 | { 67 | //Key is a non-composite Hash key 68 | KeySchema = new List 69 | { 70 | new KeySchemaElement 71 | { 72 | AttributeName = keyName, 73 | KeyType = KeyType.HASH 74 | } 75 | }, 76 | //And is of type String 77 | AttributeDefinitions = new List 78 | { 79 | new AttributeDefinition 80 | { 81 | AttributeName = keyName, 82 | AttributeType = ScalarAttributeType.S 83 | } 84 | } 85 | } 86 | })); 87 | var creator = new DynamoDBTableCreator(); 88 | await creator.CreateTableIfNotExistsAsync(moqClient.Object, "", false, "", ""); 89 | } 90 | 91 | /// 92 | /// Describe table returns an existing table. Table is invalid to be used for a cache becuase it has a composite key. Exception 93 | /// 94 | [Fact] 95 | public async Task TableExists_TooManyKeys_Invalid() 96 | { 97 | var key1 = "key"; 98 | var key2 = "key2"; 99 | var moqClient = new Moq.Mock(); 100 | moqClient.Setup(x => x.DescribeTableAsync(It.IsAny(), It.IsAny())) 101 | .Returns(Task.FromResult(new DescribeTableResponse 102 | { 103 | Table = new TableDescription 104 | { 105 | //Key is not a non-compisite Key 106 | KeySchema = new List 107 | { 108 | new KeySchemaElement 109 | { 110 | AttributeName = key1, 111 | KeyType = KeyType.HASH 112 | }, 113 | new KeySchemaElement 114 | { 115 | AttributeName = key2, 116 | KeyType = KeyType.RANGE 117 | } 118 | }, 119 | AttributeDefinitions = new List 120 | { 121 | new AttributeDefinition 122 | { 123 | AttributeName = key1, 124 | AttributeType = ScalarAttributeType.S 125 | }, 126 | new AttributeDefinition 127 | { 128 | AttributeName = key2, 129 | AttributeType = ScalarAttributeType.S 130 | } 131 | } 132 | } 133 | })); 134 | var creator = new DynamoDBTableCreator(); 135 | await Assert.ThrowsAsync(() => creator.CreateTableIfNotExistsAsync(moqClient.Object, "", false, "", "")); 136 | } 137 | 138 | /// 139 | /// Describe table returns an existing table. Table is invalid to be used for a cache becuase it has a bad key attribute type. Exception 140 | /// 141 | [Fact] 142 | public async Task TableExists_BadKeyAttributeType_Invalid() 143 | { 144 | var key = "key"; 145 | var moqClient = new Moq.Mock(); 146 | moqClient.Setup(x => x.DescribeTableAsync(It.IsAny(), It.IsAny())) 147 | .Returns(Task.FromResult(new DescribeTableResponse 148 | { 149 | Table = new TableDescription 150 | { 151 | KeySchema = new List 152 | { 153 | //Key is a non-composite Hash key 154 | new KeySchemaElement 155 | { 156 | AttributeName = key, 157 | KeyType = KeyType.HASH 158 | } 159 | }, 160 | AttributeDefinitions = new List 161 | { 162 | //But is of type Number 163 | new AttributeDefinition 164 | { 165 | AttributeName = key, 166 | AttributeType = ScalarAttributeType.N 167 | } 168 | } 169 | } 170 | })); 171 | var creator = new DynamoDBTableCreator(); 172 | await Assert.ThrowsAsync(() => creator.CreateTableIfNotExistsAsync(moqClient.Object, "", false, "", "")); 173 | } 174 | 175 | /// 176 | /// Describe table returns an existing table. Table is invalid to be used for a cache becuase it has a bad key type. Exception 177 | /// 178 | [Fact] 179 | public async Task TableExists_BadKeyType_Invalid() 180 | { 181 | var key = "key"; 182 | var moqClient = new Moq.Mock(); 183 | moqClient.Setup(x => x.DescribeTableAsync(It.IsAny(), It.IsAny())) 184 | .Returns(Task.FromResult(new DescribeTableResponse 185 | { 186 | Table = new TableDescription 187 | { 188 | //Key is non-composite. But is a Range key 189 | KeySchema = new List 190 | { 191 | new KeySchemaElement 192 | { 193 | AttributeName = key, 194 | KeyType = KeyType.RANGE 195 | } 196 | }, 197 | AttributeDefinitions = new List 198 | { 199 | new AttributeDefinition 200 | { 201 | AttributeName = key, 202 | AttributeType = ScalarAttributeType.S 203 | } 204 | } 205 | } 206 | })); 207 | var creator = new DynamoDBTableCreator(); 208 | await Assert.ThrowsAsync(() => creator.CreateTableIfNotExistsAsync(moqClient.Object, "", false, "", "")); 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | # top-most EditorConfig file 3 | 4 | # Default settings: 5 | # A newline ending every file 6 | # Use 4 spaces as indentation 7 | [*] 8 | insert_final_newline=true 9 | indent_style=space 10 | indent_size=4 11 | trim_trailing_whitespace=true 12 | end_of_line=crlf 13 | charset=utf-8 14 | 15 | [project.json] 16 | indent_size=2 17 | 18 | # Generated code 19 | [*{_AssemblyInfo.cs,.notsupported.cs}] 20 | generated_code=true 21 | 22 | # Xml project files 23 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] 24 | indent_size=2 25 | 26 | [*.{csproj,vbproj,proj,nativeproj,locproj}] 27 | charset=utf-8 28 | 29 | # Xml build files 30 | [*.builds] 31 | indent_size=2 32 | 33 | # Xml files 34 | [*.{xml,stylecop,resx,ruleset}] 35 | indent_size=2 36 | 37 | # Xml config files 38 | [*.{props,targets,config,nuspec}] 39 | indent_size=2 40 | 41 | # YAML config files 42 | [*.{yml,yaml}] 43 | indent_size=2 44 | 45 | # Shell scripts 46 | [*.sh] 47 | end_of_line=lf 48 | [*.{cmd,bat}] 49 | end_of_line=crlf 50 | 51 | [*.lock] 52 | end_of_line=lf 53 | 54 | # C# files 55 | [*.cs] 56 | max_line_length=140 57 | 58 | # New line preferences 59 | csharp_new_line_before_open_brace=all 60 | csharp_new_line_before_else=true 61 | csharp_new_line_before_catch=true 62 | csharp_new_line_before_finally=true 63 | csharp_new_line_before_members_in_object_initializers=true 64 | csharp_new_line_before_members_in_anonymous_types=true 65 | csharp_new_line_between_query_expression_clauses=true 66 | 67 | # Indentation preferences 68 | csharp_indent_block_contents=true 69 | csharp_indent_braces=false 70 | csharp_indent_case_contents=true 71 | csharp_indent_case_contents_when_block=true 72 | csharp_indent_switch_labels=true 73 | csharp_indent_labels=one_less_than_current 74 | 75 | # Modifier preferences 76 | csharp_preferred_modifier_order=public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 77 | 78 | # avoid this. unless absolutely necessary 79 | dotnet_style_qualification_for_field=false:warning 80 | dotnet_style_qualification_for_property=false:error 81 | dotnet_style_qualification_for_method=false:warning 82 | dotnet_style_qualification_for_event=false:warning 83 | 84 | # Types: use keywords instead of BCL types, and prefer var 85 | csharp_style_var_elsewhere=true:suggestion 86 | csharp_style_var_for_built_in_types=true:suggestion 87 | csharp_style_var_when_type_is_apparent=true:suggestion 88 | dotnet_style_predefined_type_for_locals_parameters_members=true:suggestion 89 | dotnet_style_predefined_type_for_member_access=true:suggestion 90 | 91 | # name all constant fields using ALL_UPPER 92 | dotnet_naming_rule.constant_fields_should_be_all_upper.severity=suggestion 93 | dotnet_naming_rule.constant_fields_should_be_all_upper.symbols=constant_fields 94 | dotnet_naming_rule.constant_fields_should_be_all_upper.style=consts_style 95 | dotnet_naming_symbols.constant_fields.applicable_kinds=field 96 | dotnet_naming_symbols.constant_fields.required_modifiers=const 97 | dotnet_naming_symbols.constant_fields.applicable_accessibilities=private, internal, private_protected 98 | dotnet_naming_style.consts_style.capitalization=all_upper 99 | dotnet_naming_style.consts_style.word_separator=_ 100 | 101 | # static fields should have s_ prefix 102 | dotnet_naming_rule.static_fields_should_have_prefix.severity=suggestion 103 | dotnet_naming_rule.static_fields_should_have_prefix.symbols=static_fields 104 | dotnet_naming_rule.static_fields_should_have_prefix.style=static_prefix_style 105 | dotnet_naming_symbols.static_fields.applicable_kinds=field 106 | dotnet_naming_symbols.static_fields.required_modifiers=static 107 | dotnet_naming_symbols.static_fields.applicable_accessibilities=private, internal, private_protected 108 | dotnet_naming_style.static_prefix_style.required_prefix=s_ 109 | dotnet_naming_style.static_prefix_style.capitalization=camel_case 110 | 111 | # internal and private fields should be _camelCase 112 | dotnet_naming_rule.camel_case_for_private_internal_fields.severity=suggestion 113 | dotnet_naming_rule.camel_case_for_private_internal_fields.symbols=private_internal_fields 114 | dotnet_naming_rule.camel_case_for_private_internal_fields.style=camel_case_underscore_style 115 | dotnet_naming_symbols.private_internal_fields.applicable_kinds=field 116 | dotnet_naming_symbols.private_internal_fields.applicable_accessibilities=private, internal 117 | dotnet_naming_style.camel_case_underscore_style.required_prefix=_ 118 | dotnet_naming_style.camel_case_underscore_style.capitalization=camel_case 119 | 120 | # Code style defaults 121 | csharp_using_directive_placement=outside_namespace:suggestion 122 | dotnet_sort_system_directives_first=true 123 | csharp_prefer_braces=true:silent 124 | csharp_preserve_single_line_blocks=true:none 125 | csharp_preserve_single_line_statements=false:none 126 | csharp_prefer_static_local_function=true:suggestion 127 | csharp_prefer_simple_using_statement=false:none 128 | csharp_style_prefer_switch_expression=true:suggestion 129 | 130 | # Code quality 131 | dotnet_style_readonly_field=true:warning 132 | dotnet_code_quality_unused_parameters=non_public:warning 133 | 134 | # Expression-level preferences 135 | dotnet_style_object_initializer=true:suggestion 136 | dotnet_style_collection_initializer=true:suggestion 137 | dotnet_style_explicit_tuple_names=true:suggestion 138 | dotnet_style_coalesce_expression=true:suggestion 139 | dotnet_style_null_propagation=true:suggestion 140 | dotnet_style_prefer_is_null_check_over_reference_equality_method=true:suggestion 141 | dotnet_style_prefer_inferred_tuple_names=true:suggestion 142 | dotnet_style_prefer_inferred_anonymous_type_member_names=true:suggestion 143 | dotnet_style_prefer_auto_properties=true:suggestion 144 | dotnet_style_prefer_conditional_expression_over_assignment=true:silent 145 | dotnet_style_prefer_conditional_expression_over_return=true:silent 146 | csharp_prefer_simple_default_expression=true:suggestion 147 | 148 | # Expression-bodied members 149 | csharp_style_expression_bodied_methods=true:silent 150 | csharp_style_expression_bodied_constructors=true:silent 151 | csharp_style_expression_bodied_operators=true:silent 152 | csharp_style_expression_bodied_properties=true:silent 153 | csharp_style_expression_bodied_indexers=true:silent 154 | csharp_style_expression_bodied_accessors=true:silent 155 | csharp_style_expression_bodied_lambdas=true:silent 156 | csharp_style_expression_bodied_local_functions=true:silent 157 | 158 | # Pattern matching 159 | csharp_style_pattern_matching_over_is_with_cast_check=true:suggestion 160 | csharp_style_pattern_matching_over_as_with_null_check=true:suggestion 161 | csharp_style_inlined_variable_declaration=true:suggestion 162 | 163 | # Null checking preferences 164 | csharp_style_throw_expression=true:suggestion 165 | csharp_style_conditional_delegate_call=true:suggestion 166 | 167 | # Other features 168 | csharp_style_prefer_index_operator=false:none 169 | csharp_style_prefer_range_operator=false:none 170 | csharp_style_pattern_local_over_anonymous_function=false:none 171 | 172 | # Space preferences 173 | csharp_space_after_cast=false 174 | csharp_space_after_colon_in_inheritance_clause=true 175 | csharp_space_after_comma=true 176 | csharp_space_after_dot=false 177 | csharp_space_after_keywords_in_control_flow_statements=true 178 | csharp_space_after_semicolon_in_for_statement=true 179 | csharp_space_around_binary_operators=before_and_after 180 | csharp_space_around_declaration_statements=do_not_ignore 181 | csharp_space_before_colon_in_inheritance_clause=true 182 | csharp_space_before_comma=false 183 | csharp_space_before_dot=false 184 | csharp_space_before_open_square_brackets=false 185 | csharp_space_before_semicolon_in_for_statement=false 186 | csharp_space_between_empty_square_brackets=false 187 | csharp_space_between_method_call_empty_parameter_list_parentheses=false 188 | csharp_space_between_method_call_name_and_opening_parenthesis=false 189 | csharp_space_between_method_call_parameter_list_parentheses=false 190 | csharp_space_between_method_declaration_empty_parameter_list_parentheses=false 191 | csharp_space_between_method_declaration_name_and_open_parenthesis=false 192 | csharp_space_between_method_declaration_parameter_list_parentheses=false 193 | csharp_space_between_parentheses=false 194 | csharp_space_between_square_brackets=false 195 | 196 | # Analyzers 197 | dotnet_code_quality.ca1802.api_surface=private, internal 198 | dotnet_code_quality.ca1822.api_surface=private, internal 199 | dotnet_code_quality.ca2208.api_surface=public 200 | 201 | # CA1822: Mark members as static 202 | dotnet_diagnostic.ca1822.severity=none 203 | 204 | # License header 205 | file_header_template= Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\r\nSPDX-License-Identifier: Apache-2.0 206 | 207 | # ReSharper properties 208 | resharper_braces_for_for=required 209 | resharper_braces_for_foreach=required 210 | resharper_braces_for_ifelse=required_for_multiline 211 | resharper_braces_for_while=required 212 | resharper_braces_redundant=false 213 | resharper_csharp_wrap_arguments_style=chop_if_long 214 | resharper_csharp_wrap_lines=false 215 | resharper_csharp_wrap_parameters_style=chop_if_long 216 | resharper_enforce_line_ending_style=true 217 | resharper_max_initializer_elements_on_line=1 218 | resharper_space_before_foreach_parentheses=true 219 | resharper_space_before_if_parentheses=true 220 | resharper_space_within_single_line_array_initializer_braces=true 221 | resharper_use_indent_from_vs=false 222 | resharper_wrap_after_declaration_lpar=true 223 | resharper_wrap_object_and_collection_initializer_style=chop_always 224 | 225 | 226 | # ReSharper inspection severities 227 | # https://www.jetbrains.com/help/resharper/EditorConfig_Index.html 228 | resharper_arrange_redundant_parentheses_highlighting=hint 229 | resharper_arrange_this_qualifier_highlighting=error 230 | resharper_arrange_type_member_modifiers_highlighting=hint 231 | resharper_arrange_type_modifiers_highlighting=hint 232 | resharper_built_in_type_reference_style_for_member_access_highlighting=hint 233 | resharper_built_in_type_reference_style_highlighting=hint 234 | resharper_ca1031_highlighting=suggestion 235 | resharper_ca1308_highlighting=none 236 | resharper_ca2007_highlighting=none 237 | resharper_convert_closure_to_method_group_highlighting=hint 238 | resharper_convert_to_static_class_highlighting=none 239 | resharper_member_can_be_made_static_global_highlighting=none 240 | resharper_member_can_be_made_static_local_highlighting=none 241 | resharper_redundant_base_qualifier_highlighting=warning 242 | resharper_suggest_var_or_type_built_in_types_highlighting=hint 243 | resharper_suggest_var_or_type_elsewhere_highlighting=hint 244 | resharper_suggest_var_or_type_simple_types_highlighting=hint 245 | resharper_web_config_module_not_resolved_highlighting=warning 246 | resharper_web_config_type_not_resolved_highlighting=warning 247 | resharper_web_config_wrong_module_highlighting=warning 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![.NET on AWS Banner](./logo.png ".NET on AWS") 2 | 3 | # AWS .NET Distributed Cache Provider [![nuget](https://img.shields.io/nuget/v/AWS.AspNetCore.DistributedCacheProvider.svg) ![downloads](https://img.shields.io/nuget/dt/AWS.AspNetCore.DistributedCacheProvider.svg)](https://www.nuget.org/packages/AWS.AspNetCore.DistributedCacheProvider/) 4 | The AWS .NET Distributed Cache Provider provides an implementation of the ASP.NET Core interface [IDistributedCache](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) backed by Amazon DynamoDB. A common use of an `IDistributedCache` implementation is to store ephemeral, non-critical [session state](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?#session-state) data in ASP.NET Core applications. 5 | 6 | # Getting Started 7 | Install the [AWS.AspNetCore.DistributedCacheProvider](https://www.nuget.org/packages/AWS.AspNetCore.DistributedCacheProvider/) package from NuGet. 8 | 9 | .NET uses [dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) to provide services to different objects that rely on them. This library provides extensions to assist the user in injecting this implementation of `IDistributedCache` as a service for other objects to consume. 10 | 11 | ## Sample 12 | For example, if you are building an application that requires the use of sessions in a distributed webapp, .NET's session state middleware looks for an implementation of `IDistributedCache` to store the session data. You can direct the session service to use the DynamoDB distributed cache implementation using dependency injection: 13 | 14 | ```csharp 15 | var builder = WebApplication.CreateBuilder(args); 16 | builder.Services.AddAWSDynamoDBDistributedCache(options => 17 | { 18 | options.TableName = "session_cache_table"; 19 | options.PartitionKeyName = "id"; 20 | options.TTLAttributeName = "cache_ttl"; 21 | 22 | }); 23 | builder.Services.AddSession(options => 24 | { 25 | options.IdleTimeout = TimeSpan.FromSeconds(90); 26 | options.Cookie.IsEssential = true; 27 | }); 28 | var app = builder.Build(); 29 | ... 30 | ``` 31 | 32 | For more information about .NET's session state middleware, see [this article](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state) and specifically [this section](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state#configure-session-state) regarding dependency injection. 33 | 34 | ## Configuration 35 | Here are the available options to configure the `DynamoDBDistributedCache`: 36 | * **TableName** (required) - string - The name of an existing table that will be used to store the cache data. 37 | * **CreateTableIfNotExists** (optional) - boolean - If set to true during startup the library will check if the specified table exists. If the table does not exist a new table will be created using on demand provision throughput. This will require extra permissions to call the `CreateTable` and `UpdateTimeToLive` service operations. It is recommended to only set this to `true` in development environments. Production environments should create the table before deployment. This will allow production deployments to require fewer permissions and have a faster startup. **Default value is `false`**. 38 | * **UseConsistentReads** (optional) - boolean - When `true`, reads from the underlying DynamoDB table will use consistent reads. Having consistent reads means that any read will be from the latest data in the DynamoDB table. However, using consistent reads requires more read capacity affecting the cost of the DynamoDB table. To reduce cost this property could be set to `false` but the application must be able to handle a delay after a set operation for the data to come back in a get operation. **Default value is true**. See more [here](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html). 39 | * **PartitionKeyName** (optional) - string - Name of DynamoDB table's partition key. If this is not set a `DescribeTable` service call will be made at startup to determine the partition key's name. To reduce startup time and avoid needing permissions to `DescribeTable` this property should be set. 40 | * **PartitionKeyPrefix** (optional) - string - Prefix added to value of the partition key stored in DynamoDB. 41 | * **TTLAttributeName** (optional) - string - DynamoDB's [Time To Live (TTL) feature](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) is used for removing expired cache items from the table. This option specifies the attribute name that will be used to store the TTL timestamp. If this is not set, a `DescribeTimeToLive` service call will be made to determine the TTL attribute's name. To reduce startup time and avoid needing permissions to `DescribeTimeToLive` this property should be set. 42 | 43 | 44 | The options can be used in the following way: 45 | ```csharp 46 | var builder = WebApplication.CreateBuilder(args); 47 | builder.Services.AddAWSDynamoDBDistributedCache(options => 48 | { 49 | options.TableName = "session_cache_table"; 50 | options.CreateTableIfNotExists = true; 51 | options.UseConsistentReads = true; 52 | options.PartitionKeyName = "id"; 53 | options.TTLAttributeName = "cache_ttl" 54 | 55 | }); 56 | builder.Services.AddSession(options => 57 | { 58 | options.IdleTimeout = TimeSpan.FromSeconds(90); 59 | options.Cookie.IsEssential = true; 60 | }); 61 | var app = builder.Build(); 62 | ... 63 | ``` 64 | 65 | ## Table Considerations 66 | The cache provider will use the table specified by `TableName` if it already exists. The table must use a non-composite partition key of type string, otherwise an exception will be thrown during the first cache operation. No settings will be modified on existing tables. 67 | 68 | For production environments, we recommend setting `CreateTableIfNotExists` to `false` and providing both a `PartitionKeyName` and `TTLAttributeName`. This minimizes the required permissions and additional control plane calls during the first cache operation. It is recommended to only set `CreateTableIfNotExists` to `true` in development environments. 69 | 70 | When `CreateTableIfNotExists` is set to `true` and if the table specified by `TableName` does not already exist, the cache provider will create the table during the first cache operation with the following default settings: 71 | * The [on-demand](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html#HowItWorks.OnDemand) capacity mode 72 | * [Time to Live](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) enabled 73 | * Encryption using an [AWS KMS key owned and managed by DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/encryption.howitworks.html#ddb-owned). If you would like to use an AWS managed or customer managed key, you can update the table after it is created or provide your own table. See [Managing encrypted tables](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/encryption.tutorial.html) in the Amazon DynamoDB Developer Guide. 74 | 75 | # Permissions 76 | To use the cache provider with an _existing_ DynamoDB table and a provided `PartitionKeyName` and `TTLAttributeName`, the following permissions are required: 77 | ``` 78 | { 79 | "Version": "2012-10-17", 80 | "Statement": [ 81 | { 82 | "Effect": "Allow", 83 | "Action": [ 84 | "dynamodb:PutItem", 85 | "dynamodb:DeleteItem", 86 | "dynamodb:GetItem", 87 | "dynamodb:UpdateItem" 88 | ], 89 | "Resource": "arn:aws:dynamodb:*::table/" 90 | } 91 | ] 92 | } 93 | ``` 94 | 95 | If configured to use an existing table but `PartitionKeyName` and `TTLAttributeName` are not specified, then the following permissions are required _in addition to_ those specified above: 96 | ``` 97 | { 98 | "Version": "2012-10-17", 99 | "Statement": [ 100 | { 101 | "Effect": "Allow", 102 | "Action": [ 103 | "dynamodb:DescribeTable", 104 | "dynamodb:DescribeTimeToLive" 105 | ], 106 | "Resource": "arn:aws:dynamodb:*::table/
" 107 | } 108 | ] 109 | } 110 | ``` 111 | 112 | If the cache provider is configured to create a table via the `CreateTableIfNotExists` option described above, then the following permissions are required _in addition to_ the minimal permissions specified above: 113 | ``` 114 | { 115 | "Version": "2012-10-17", 116 | "Statement": [ 117 | { 118 | "Effect": "Allow", 119 | "Action": [ 120 | "dynamodb:CreateTable", 121 | "dynamodb:UpdateTimeToLive" 122 | ], 123 | "Resource": "arn:aws:dynamodb:*::table/
" 124 | } 125 | ] 126 | } 127 | ``` 128 | 129 | The `LeadingKeys` condition can optionally be used to restrict access to only cache entry items in the DynamoDB table. The prefix can be configured via the `PartitionKeyPrefix` option described above, with the default value `"dc"`. See [Using IAM policy conditions for fine-grained access control](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/specifying-conditions.html) for more information. 130 | ``` 131 | "Resource": "arn:aws:dynamodb:*::table/
", 132 | "Condition": { 133 | "ForAllValues:StringLike": { 134 | "dynamodb:LeadingKeys": "dc:*" 135 | } 136 | } 137 | ``` 138 | # Time to Live 139 | `IDistributedCache` exposes several options for for expiring entries from the cache on its [`DistributedCacheEntryOptions`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.distributed.distributedcacheentryoptions). If using the `IDistributedCache` to store session state for an ASP.NET Core application, [`SessionOptions.IdleTimeout`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.sessionoptions.idletimeout) controls the expiration for cache entries. 140 | 141 | The AWS .NET Distributed Cache Provider relies on DynamoDB's [Time to Live (TTL) feature](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) to delete expired items from the table. TTL will be enabled automatically when tables are created via `CreateTableIfNotExists`. If you provide an existing table and TTL is **not enabled, expired entries will not be deleted** and a warning will be logged during the first cache operation. 142 | 143 | Because DynamoDB's TTL feature can take up to a few days to delete expired items, the cache provider filters out items that have expired but not been deleted yet on the client side. 144 | 145 | # Auditing 146 | By default CloudTrail is enabled on AWS accounts when they are created and will capture control plane events for DynamoDB. 147 | 148 | To capture data plane events, refer to [Logging data events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html) in the AWS CloudTrail User Guide. 149 | 150 | # Dependencies 151 | 152 | The library has the following dependencies 153 | * [AWSSDK.DynamoDBv2](https://www.nuget.org/packages/AWSSDK.DynamoDBv2) 154 | * [AWSSDK.Extensions.NETCore.Setup](https://www.nuget.org/packages/AWSSDK.Extensions.NETCore.Setup/) 155 | * [Microsoft.Extensions.Caching.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Caching.Abstractions) 156 | * [Microsoft.Extensions.DependencyInjection.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection.Abstractions) 157 | * [Microsoft.Extensions.Options](https://www.nuget.org/packages/Microsoft.Extensions.Options) 158 | 159 | 160 | # Getting Help 161 | 162 | We use the [GitHub issues](https://github.com/aws/aws-dotnet-distributed-cache-provider/issues) for tracking bugs and feature requests and have limited bandwidth to address them. 163 | 164 | If you think you may have found a bug, please open an [issue](https://github.com/aws/aws-dotnet-distributed-cache-provider/issues/new). 165 | 166 | # Contributing 167 | 168 | We welcome community contributions and pull requests. See 169 | [CONTRIBUTING.md](./CONTRIBUTING.md) for information on how to set up a development environment and submit code. 170 | 171 | # Additional Resources 172 | 173 | [AWS .NET GitHub Home Page](https://github.com/aws/dotnet) 174 | GitHub home for .NET development on AWS. You'll find libraries, tools, and resources to help you build .NET applications and services on AWS. 175 | 176 | [AWS Developer Center - Explore .NET on AWS](https://aws.amazon.com/developer/language/net/) 177 | Find all the .NET code samples, step-by-step guides, videos, blog content, tools, and information about live events that you need in one place. 178 | 179 | [AWS Developer Blog - .NET](https://aws.amazon.com/blogs/developer/category/programing-language/dot-net/) 180 | Come see what .NET developers at AWS are up to! Learn about new .NET software announcements, guides, and how-to's. 181 | 182 | [@dotnetonaws](https://twitter.com/dotnetonaws) 183 | Follow us on twitter! 184 | 185 | # Security 186 | 187 | The AWS .NET Distributed Cache Provider relies on the [AWS SDK for .NET](https://github.com/aws/aws-sdk-net) for communicating with AWS. Refer to the [security section](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/security.html) in the [AWS SDK for .NET Developer Guide](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/welcome.html) for more information. 188 | 189 | If you discover a potential security issue, refer to the [security policy](https://github.com/aws/aws-dotnet-distributed-cache-provider/security/policy) for reporting information. 190 | 191 | # License 192 | 193 | Libraries in this repository are licensed under the Apache 2.0 License. 194 | 195 | See [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for more information. 196 | -------------------------------------------------------------------------------- /THIRD-PARTY-LICENSES: -------------------------------------------------------------------------------- 1 | ** AWSSDK.DynamoDBv2; version 3.7.302.15 -- https://github.com/aws/aws-sdk-net/ 2 | ** AWSSDK.Extensions.NETCore.Setup; version 3.7.300 -- https://www.nuget.org/packages/AWSSDK.Extensions.NETCore.Setup 3 | 4 | Apache License 5 | 6 | Version 2.0, January 2004 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | “License” shall mean the terms and conditions for use, reproduction, and 13 | distribution as defined by Sections 1 through 9 of this document. 14 | 15 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 16 | owner that is granting the License. 17 | 18 | “Legal Entity” shall mean the union of the acting entity and all other entities 19 | that control, are controlled by, or are under common control with that entity. 20 | For the purposes of this definition, “control” means (i) the power, direct or 21 | indirect, to cause the direction or management of such entity, whether by 22 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 26 | permissions granted by this License. 27 | 28 | “Source” form shall mean the preferred form for making modifications, including 29 | but not limited to software source code, documentation source, and configuration 30 | files. 31 | 32 | “Object” form shall mean any form resulting from mechanical transformation or 33 | translation of a Source form, including but not limited to compiled object code, 34 | generated documentation, and conversions to other media types. 35 | 36 | “Work” shall mean the work of authorship, whether in Source or Object form, made 37 | available under the License, as indicated by a copyright notice that is included 38 | in or attached to the work (an example is provided in the Appendix below). 39 | 40 | “Derivative Works” shall mean any work, whether in Source or Object form, that 41 | is based on (or derived from) the Work and for which the editorial revisions, 42 | annotations, elaborations, or other modifications represent, as a whole, an 43 | original work of authorship. For the purposes of this License, Derivative Works 44 | shall not include works that remain separable from, or merely link (or bind by 45 | name) to the interfaces of, the Work and Derivative Works thereof. 46 | 47 | “Contribution” shall mean any work of authorship, including the original version 48 | of the Work and any modifications or additions to that Work or Derivative Works 49 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 50 | by the copyright owner or by an individual or Legal Entity authorized to submit 51 | on behalf of the copyright owner. For the purposes of this definition, 52 | “submitted” means any form of electronic, verbal, or written communication sent 53 | to the Licensor or its representatives, including but not limited to 54 | communication on electronic mailing lists, source code control systems, and 55 | issue tracking systems that are managed by, or on behalf of, the Licensor for 56 | the purpose of discussing and improving the Work, but excluding communication 57 | that is conspicuously marked or otherwise designated in writing by the copyright 58 | owner as “Not a Contribution.” 59 | 60 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 61 | of whom a Contribution has been received by Licensor and subsequently 62 | incorporated within the Work. 63 | 64 | 2. Grant of Copyright License. Subject to the terms and conditions of this 65 | License, each Contributor hereby grants to You a perpetual, worldwide, non- 66 | exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, 67 | prepare Derivative Works of, publicly display, publicly perform, sublicense, and 68 | distribute the Work and such Derivative Works in Source or Object form. 69 | 70 | 3. Grant of Patent License. Subject to the terms and conditions of this License, 71 | each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no- 72 | charge, royalty-free, irrevocable (except as stated in this section) patent 73 | license to make, have made, use, offer to sell, sell, import, and otherwise 74 | transfer the Work, where such license applies only to those patent claims 75 | licensable by such Contributor that are necessarily infringed by their 76 | Contribution(s) alone or by combination of their Contribution(s) with the Work 77 | to which such Contribution(s) was submitted. If You institute patent litigation 78 | against any entity (including a cross-claim or counterclaim in a lawsuit) 79 | alleging that the Work or a Contribution incorporated within the Work 80 | constitutes direct or contributory patent infringement, then any patent licenses 81 | granted to You under this License for that Work shall terminate as of the date 82 | such litigation is filed. 83 | 84 | 4. Redistribution. You may reproduce and distribute copies of the Work or 85 | Derivative Works thereof in any medium, with or without modifications, and in 86 | Source or Object form, provided that You meet the following conditions: 87 | 88 | You must give any other recipients of the Work or Derivative Works a copy of 89 | this License; and 90 | 91 | You must cause any modified files to carry prominent notices stating that You 92 | changed the files; and 93 | 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | 99 | If the Work includes a “NOTICE” text file as part of its distribution, then any 100 | Derivative Works that You distribute must include a readable copy of the 101 | attribution notices contained within such NOTICE file, excluding those notices 102 | that do not pertain to any part of the Derivative Works, in at least one of the 103 | following places: within a NOTICE text file distributed as part of the 104 | Derivative Works; within the Source form or documentation, if provided along 105 | with the Derivative Works; or, within a display generated by the Derivative 106 | Works, if and wherever such third-party notices normally appear. The contents of 107 | the NOTICE file are for informational purposes only and do not modify the 108 | License. You may add Your own attribution notices within Derivative Works that 109 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 110 | provided that such additional attribution notices cannot be construed as 111 | modifying the License. 112 | You may add Your own copyright statement to Your modifications and may provide 113 | additional or different license terms and conditions for use, reproduction, or 114 | distribution of Your modifications, or for any such Derivative Works as a whole, 115 | provided Your use, reproduction, and distribution of the Work otherwise complies 116 | with the conditions stated in this License. 117 | 118 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 119 | Contribution intentionally submitted for inclusion in the Work by You to the 120 | Licensor shall be under the terms and conditions of this License, without any 121 | additional terms or conditions. Notwithstanding the above, nothing herein shall 122 | supersede or modify the terms of any separate license agreement you may have 123 | executed with Licensor regarding such Contributions. 124 | 125 | 6. Trademarks. This License does not grant permission to use the trade names, 126 | trademarks, service marks, or product names of the Licensor, except as required 127 | for reasonable and customary use in describing the origin of the Work and 128 | reproducing the content of the NOTICE file. 129 | 130 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 131 | writing, Licensor provides the Work (and each Contributor provides its 132 | Contributions) on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 133 | KIND, either express or implied, including, without limitation, any warranties 134 | or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 135 | PARTICULAR PURPOSE. You are solely responsible for determining the 136 | appropriateness of using or redistributing the Work and assume any risks 137 | associated with Your exercise of permissions under this License. 138 | 139 | 8. Limitation of Liability. In no event and under no legal theory, whether in 140 | tort (including negligence), contract, or otherwise, unless required by 141 | applicable law (such as deliberate and grossly negligent acts) or agreed to in 142 | writing, shall any Contributor be liable to You for damages, including any 143 | direct, indirect, special, incidental, or consequential damages of any character 144 | arising as a result of this License or out of the use or inability to use the 145 | Work (including but not limited to damages for loss of goodwill, work stoppage, 146 | computer failure or malfunction, or any and all other commercial damages or 147 | losses), even if such Contributor has been advised of the possibility of such 148 | damages. 149 | 150 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or 151 | Derivative Works thereof, You may choose to offer, and charge a fee for, 152 | acceptance of support, warranty, indemnity, or other liability obligations 153 | and/or rights consistent with this License. However, in accepting such 154 | obligations, You may act only on Your own behalf and on Your sole 155 | responsibility, not on behalf of any other Contributor, and only if You agree to 156 | indemnify, defend, and hold each Contributor harmless for any liability incurred 157 | by, or claims asserted against, such Contributor by reason of your accepting any 158 | such warranty or additional liability. 159 | 160 | END OF TERMS AND CONDITIONS 161 | * For AWSSDK.DynamoDBv2 see also this required NOTICE: 162 | The AWS SDK for .NET includes the following third-party software/licensing: 163 | 164 | ** Json processing from LitJson 165 | All the source code and related files distributed with this software have 166 | been dedicated to the public domain by the authors. 167 | 168 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute 169 | the software, either in source code form or as a compiled binary, for any 170 | purpose, commercial or non-commercial, and by any means. 171 | 172 | ---------------- 173 | 174 | ** Parsing PEM files from Bouncy Castle 175 | Copyright (c) 2000 - 2017 The Legion of the Bouncy Castle Inc. 176 | (http://www.bouncycastle.org) 177 | 178 | Permission is hereby granted, free of charge, to any person obtaining a copy 179 | of this software and associated documentation files (the "Software"), to 180 | deal 181 | in the Software without restriction, including without limitation the rights 182 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 183 | copies of the Software, and to permit persons to whom the Software is 184 | furnished to do so, subject to the following conditions: 185 | 186 | The above copyright notice and this permission notice shall be included in 187 | all 188 | copies or substantial portions of the Software. 189 | 190 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 191 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 192 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 193 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 194 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 195 | FROM, 196 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 197 | THE 198 | SOFTWARE. 199 | 200 | ---------------- 201 | 202 | ** Performing CRC32 checks from vbAccelerator.com 203 | vbAccelerator Software License 204 | 205 | Version 1.0 206 | 207 | Copyright (c) 2002 vbAccelerator.com 208 | 209 | Redistribution and use in source and binary forms, with or without 210 | modification, are permitted provided that the following conditions are met: 211 | 212 | Redistributions of source code must retain the above copyright notice, this 213 | list of conditions and the following disclaimer 214 | Redistributions in binary form must reproduce the above copyright notice, 215 | this list of conditions and the following disclaimer in the documentation and/or 216 | other materials provided with the distribution. 217 | The end-user documentation included with the redistribution, if any, must 218 | include the following acknowledgment: 219 | 220 | "This product includes software developed by vbAccelerator ( 221 | http://vbaccelerator.com/)." 222 | 223 | Alternately, this acknowledgment may appear in the software itself, if and 224 | wherever such third-party acknowledgments normally appear. 225 | The names "vbAccelerator" and "vbAccelerator.com" must not be used to 226 | endorse or promote products derived from this software without prior written 227 | permission. For written permission, please contact vbAccelerator through 228 | steve@vbaccelerator.com. 229 | Products derived from this software may not be called "vbAccelerator", nor 230 | may "vbAccelerator" appear in their name, without prior written permission of 231 | vbAccelerator. 232 | 233 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, 234 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 235 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 236 | VBACCELERATOR OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 237 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 238 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 239 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 240 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 241 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 242 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 243 | 244 | This software consists of voluntary contributions made by many individuals 245 | on behalf of the vbAccelerator. For more information, please see 246 | http://vbaccelerator.com/ . 247 | 248 | The vbAccelerator licence is based on the Apache Software Foundation 249 | Software Licence, Copyright (c) 2000 The Apache Software Foundation. All rights 250 | reserved. 251 | * For AWSSDK.Extensions.NETCore.Setup see also this required NOTICE: 252 | The AWS SDK for .NET includes the following third-party software/licensing: 253 | 254 | ** Json processing from LitJson 255 | All the source code and related files distributed with this software have 256 | been dedicated to the public domain by the authors. 257 | 258 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute 259 | the software, either in source code form or as a compiled binary, for any 260 | purpose, commercial or non-commercial, and by any means. 261 | 262 | ---------------- 263 | 264 | ** Parsing PEM files from Bouncy Castle 265 | Copyright (c) 2000 - 2017 The Legion of the Bouncy Castle Inc. 266 | (http://www.bouncycastle.org) 267 | 268 | Permission is hereby granted, free of charge, to any person obtaining a copy 269 | of this software and associated documentation files (the "Software"), to 270 | deal 271 | in the Software without restriction, including without limitation the rights 272 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 273 | copies of the Software, and to permit persons to whom the Software is 274 | furnished to do so, subject to the following conditions: 275 | 276 | The above copyright notice and this permission notice shall be included in 277 | all 278 | copies or substantial portions of the Software. 279 | 280 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 281 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 282 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 283 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 284 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 285 | FROM, 286 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 287 | THE 288 | SOFTWARE. 289 | 290 | ---------------- 291 | 292 | ** Performing CRC32 checks from vbAccelerator.com 293 | vbAccelerator Software License 294 | 295 | Version 1.0 296 | 297 | Copyright (c) 2002 vbAccelerator.com 298 | 299 | Redistribution and use in source and binary forms, with or without 300 | modification, are permitted provided that the following conditions are met: 301 | 302 | Redistributions of source code must retain the above copyright notice, this 303 | list of conditions and the following disclaimer 304 | Redistributions in binary form must reproduce the above copyright notice, 305 | this list of conditions and the following disclaimer in the documentation and/or 306 | other materials provided with the distribution. 307 | The end-user documentation included with the redistribution, if any, must 308 | include the following acknowledgment: 309 | 310 | "This product includes software developed by vbAccelerator ( 311 | http://vbaccelerator.com/)." 312 | 313 | Alternately, this acknowledgment may appear in the software itself, if and 314 | wherever such third-party acknowledgments normally appear. 315 | The names "vbAccelerator" and "vbAccelerator.com" must not be used to 316 | endorse or promote products derived from this software without prior written 317 | permission. For written permission, please contact vbAccelerator through 318 | steve@vbaccelerator.com. 319 | Products derived from this software may not be called "vbAccelerator", nor 320 | may "vbAccelerator" appear in their name, without prior written permission of 321 | vbAccelerator. 322 | 323 | THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, 324 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 325 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 326 | VBACCELERATOR OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 327 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 328 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 329 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 330 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 331 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 332 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 333 | 334 | This software consists of voluntary contributions made by many individuals 335 | on behalf of the vbAccelerator. For more information, please see 336 | http://vbaccelerator.com/ . 337 | 338 | The vbAccelerator licence is based on the Apache Software Foundation 339 | Software Licence, Copyright (c) 2000 The Apache Software Foundation. All rights 340 | reserved. 341 | 342 | ------ 343 | 344 | ** Microsoft.Extensions.DependencyInjection.Abstractions; version 6.0.0 -- https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection.Abstractions 345 | Copyright (c) .NET Foundation and Contributors 346 | ** Microsoft.Extensions.Caching.Abstractions; version 6.0.0 -- https://www.nuget.org/packages/Microsoft.Extensions.Caching.Abstractions/ 347 | Copyright (c) .NET Foundation and Contributors 348 | ** Microsoft.Extensions.Options; version 6.0.0 -- https://www.nuget.org/packages/Microsoft.Extensions.Options/ 349 | Copyright (c) .NET Foundation and Contributors 350 | 351 | The MIT License (MIT) 352 | 353 | Copyright (c) .NET Foundation and Contributors 354 | 355 | All rights reserved. 356 | 357 | Permission is hereby granted, free of charge, to any person obtaining a copy 358 | of this software and associated documentation files (the "Software"), to deal 359 | in the Software without restriction, including without limitation the rights 360 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 361 | copies of the Software, and to permit persons to whom the Software is 362 | furnished to do so, subject to the following conditions: 363 | 364 | The above copyright notice and this permission notice shall be included in all 365 | copies or substantial portions of the Software. 366 | 367 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 368 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 369 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 370 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 371 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 372 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 373 | SOFTWARE. -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderUnitTests/DynamoDBDistributedCacheCRUDTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | using Amazon.DynamoDBv2; 4 | using Amazon.DynamoDBv2.Model; 5 | using AWS.DistributedCacheProvider; 6 | using AWS.DistributedCacheProvider.Internal; 7 | using Microsoft.Extensions.Caching.Distributed; 8 | using Moq; 9 | using Xunit; 10 | 11 | namespace AWS.DistributedCacheProviderUnitTests 12 | { 13 | public class DynamoDBDistributedCacheCRUDTests 14 | { 15 | [Fact] 16 | public void NullParameterExceptions() 17 | { 18 | var moqClient = new Moq.Mock(); 19 | var moqCreator = new Moq.Mock(); 20 | //Mock method calls to make sure DynamoDBDistributedCache.Startup() returns immediately. 21 | moqCreator.Setup(x => x.CreateTableIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) 22 | .Returns(Task.FromResult("foobar")); 23 | moqCreator.Setup(x => x.GetTTLColumnAsync(It.IsAny(), It.IsAny())) 24 | .Returns(Task.FromResult("blah")); 25 | var cache = new DynamoDBDistributedCache(moqClient.Object, moqCreator.Object, new DynamoDBDistributedCacheOptions 26 | { 27 | TableName = "MyTableName" 28 | }); 29 | #pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. 30 | Assert.Throws(() => cache.Get(null)); 31 | Assert.Throws(() => cache.Remove(null)); 32 | Assert.Throws(() => cache.Set(null, Array.Empty(), new DistributedCacheEntryOptions())); 33 | Assert.Throws(() => cache.Set(" ", null, new DistributedCacheEntryOptions())); 34 | Assert.Throws(() => cache.Set(" ", Array.Empty(), null)); 35 | Assert.Throws(() => cache.Refresh(null)); 36 | #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. 37 | } 38 | 39 | [Fact] 40 | public void GetReturnsNullWhenKeyIsNotFound() 41 | { 42 | var moqClient = new Moq.Mock(); 43 | moqClient.Setup(x => x.GetItemAsync(It.IsAny(), CancellationToken.None)) 44 | .Throws(new ResourceNotFoundException("")); 45 | var moqCreator = new Moq.Mock(); 46 | //Mock method calls to make sure DynamoDBDistributedCache.Startup() returns immediately. 47 | moqCreator.Setup(x => x.CreateTableIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) 48 | .Returns(Task.FromResult("foobar")); 49 | moqCreator.Setup(x => x.GetTTLColumnAsync(It.IsAny(), It.IsAny())) 50 | .Returns(Task.FromResult("blah")); 51 | var cache = new DynamoDBDistributedCache(moqClient.Object, moqCreator.Object, new DynamoDBDistributedCacheOptions 52 | { 53 | TableName = "MyTableName" 54 | }); 55 | Assert.Null(cache.Get("foo")); 56 | } 57 | 58 | [Fact] 59 | public void Get_ReturnsNull_WhenKeyIsExpired_ButStillExistsinTheTable() 60 | { 61 | // Create mock DDB client that returns an expired key. 62 | var moqClient = new Mock(); 63 | moqClient.Setup(x => x.GetItemAsync(It.IsAny(), CancellationToken.None)) 64 | .Returns(Task.FromResult(new GetItemResponse 65 | { 66 | Item = new Dictionary 67 | { 68 | { 69 | DynamoDBDistributedCache.VALUE_KEY, new AttributeValue {S = "someValue"} 70 | }, 71 | { 72 | DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME, new AttributeValue{N = DateTimeOffset.Now.AddHours(-5).ToUnixTimeSeconds().ToString()} 73 | }, 74 | } 75 | })); 76 | 77 | var moqCreator = new Mock(); 78 | //Mock method calls to make sure DynamoDBDistributedCache.Startup() returns immediately. 79 | moqCreator.Setup(x => x.CreateTableIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) 80 | .Returns(Task.FromResult("foobar")); 81 | moqCreator.Setup(x => x.GetTTLColumnAsync(It.IsAny(), It.IsAny())) 82 | .Returns(Task.FromResult(DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME)); 83 | 84 | var cache = new DynamoDBDistributedCache(moqClient.Object, moqCreator.Object, new DynamoDBDistributedCacheOptions 85 | { 86 | TableName = "MyTableName", 87 | }); 88 | 89 | Assert.Null(cache.Get("foo")); 90 | } 91 | 92 | [Fact] 93 | public void DeleteDoesNotThrowExceptionWhenKeyIsNotFound() 94 | { 95 | var moqClient = new Moq.Mock(); 96 | moqClient.Setup(x => x.DeleteItemAsync(It.IsAny(), CancellationToken.None)) 97 | .Throws(new ResourceNotFoundException("")); 98 | var moqCreator = new Moq.Mock(); 99 | //Mock method calls to make sure DynamoDBDistributedCache.Startup() returns immediately. 100 | moqCreator.Setup(x => x.CreateTableIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) 101 | .Returns(Task.FromResult("foobar")); 102 | moqCreator.Setup(x => x.GetTTLColumnAsync(It.IsAny(), It.IsAny())) 103 | .Returns(Task.FromResult("blah")); 104 | var cache = new DynamoDBDistributedCache(moqClient.Object, moqCreator.Object, new DynamoDBDistributedCacheOptions 105 | { 106 | TableName = "MyTableName" 107 | }); 108 | //If this throws an exception, then the test fails 109 | cache.Remove("foo"); 110 | } 111 | 112 | [Fact] 113 | public void RefreshDoesNotThrowExceptionWhenKeyIsNotFoundOnFirstGet() 114 | { 115 | var moqClient = new Moq.Mock(); 116 | moqClient.Setup(x => x.GetItemAsync(It.IsAny(), CancellationToken.None)) 117 | .Returns(Task.FromResult(new GetItemResponse 118 | { 119 | Item = new Dictionary 120 | { 121 | { 122 | DynamoDBDistributedCache.TTL_WINDOW, new AttributeValue{S = new TimeSpan(1, 0, 0).ToString()} 123 | }, 124 | { 125 | DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME, new AttributeValue{N = DateTimeOffset.Now.AddHours(1).ToUnixTimeSeconds().ToString()} 126 | }, 127 | { 128 | DynamoDBDistributedCache.TTL_DEADLINE, new AttributeValue{N = DateTimeOffset.UtcNow.AddHours(3).ToUnixTimeSeconds().ToString()} 129 | } 130 | } 131 | })); 132 | //Client return empty UpdateItemResponse to show that DynamoDB "updated" the item. This allows the test to pass 133 | moqClient.Setup(x => x.UpdateItemAsync(It.IsAny(), CancellationToken.None)) 134 | .Returns(Task.FromResult(new UpdateItemResponse())); 135 | var moqCreator = new Moq.Mock(); 136 | //Mock method calls to make sure DynamoDBDistributedCache.Startup() returns immediately. 137 | moqCreator.Setup(x => x.CreateTableIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) 138 | .Returns(Task.FromResult("foobar")); 139 | moqCreator.Setup(x => x.GetTTLColumnAsync(It.IsAny(), It.IsAny())) 140 | .Returns(Task.FromResult("blah")); 141 | var cache = new DynamoDBDistributedCache(moqClient.Object, moqCreator.Object, new DynamoDBDistributedCacheOptions 142 | { 143 | TableName = "MyTableName" 144 | }); 145 | //Test passes if this does not throw exception 146 | cache.Refresh("foo"); 147 | } 148 | 149 | [Fact] 150 | public void RefreshDoesNotThrowExceptionWhenKeyIsNotFoundOnUpdate() 151 | { 152 | var moqClient = new Moq.Mock(); 153 | moqClient.Setup(x => x.GetItemAsync(It.IsAny(), CancellationToken.None)) 154 | .Returns(Task.FromResult(new GetItemResponse 155 | { 156 | Item = new Dictionary 157 | { 158 | { 159 | DynamoDBDistributedCache.TTL_WINDOW, new AttributeValue{S = new TimeSpan(1, 0, 0).ToString()} 160 | }, 161 | { 162 | DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME, new AttributeValue{N = DateTimeOffset.Now.AddHours(1).ToUnixTimeSeconds().ToString()} 163 | }, 164 | { 165 | DynamoDBDistributedCache.TTL_DEADLINE, new AttributeValue{N = DateTimeOffset.UtcNow.AddHours(3).ToUnixTimeSeconds().ToString()} 166 | } 167 | } 168 | })); 169 | //Client throws exception that the key trying to be updated was not found. Library should still return and not throw an excpetion 170 | moqClient.Setup(x => x.UpdateItemAsync(It.IsAny(), CancellationToken.None)) 171 | .Throws(new ResourceNotFoundException("")); 172 | var moqCreator = new Moq.Mock(); 173 | //Mock method calls to make sure DynamoDBDistributedCache.Startup() returns immediately. 174 | moqCreator.Setup(x => x.CreateTableIfNotExistsAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) 175 | .Returns(Task.FromResult("foobar")); 176 | moqCreator.Setup(x => x.GetTTLColumnAsync(It.IsAny(), It.IsAny())) 177 | .Returns(Task.FromResult("blah")); 178 | var cache = new DynamoDBDistributedCache(moqClient.Object, moqCreator.Object, new DynamoDBDistributedCacheOptions 179 | { 180 | TableName = "MyTableName" 181 | }); 182 | //Test passes if this does not throw exception 183 | cache.Refresh("foo"); 184 | } 185 | 186 | [Fact] 187 | public void WorkInLeastPrivilegeMode() 188 | { 189 | var moqClient = new Moq.Mock(); 190 | moqClient.Setup(x => x.GetItemAsync(It.IsAny(), CancellationToken.None)) 191 | .Returns(Task.FromResult(new GetItemResponse 192 | { 193 | Item = new Dictionary 194 | { 195 | { 196 | DynamoDBDistributedCache.TTL_WINDOW, new AttributeValue{S = new TimeSpan(1, 0, 0).ToString()} 197 | }, 198 | { 199 | DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME, new AttributeValue{N = DateTimeOffset.Now.AddHours(1).ToUnixTimeSeconds().ToString()} 200 | }, 201 | { 202 | DynamoDBDistributedCache.TTL_DEADLINE, new AttributeValue{N = DateTimeOffset.UtcNow.AddHours(3).ToUnixTimeSeconds().ToString()} 203 | } 204 | } 205 | })); 206 | 207 | moqClient.Setup(x => x.DescribeTableAsync(It.IsAny(), CancellationToken.None)) 208 | .Returns((DescribeTableRequest r, CancellationToken token) => 209 | { 210 | throw new Exception("DescribeTableAsync should not be called"); 211 | }); 212 | 213 | moqClient.Setup(x => x.DescribeTimeToLiveAsync(It.IsAny(), CancellationToken.None)) 214 | .Returns((DescribeTableRequest r, CancellationToken token) => 215 | { 216 | throw new Exception("DescribeTimeToLiveAsync should not be called"); 217 | }); 218 | 219 | moqClient.Setup(x => x.CreateTableAsync(It.IsAny(), CancellationToken.None)) 220 | .Returns((CreateTableRequest r, CancellationToken token) => 221 | { 222 | throw new Exception("CreateTableAsync should not be called"); 223 | }); 224 | 225 | moqClient.Setup(x => x.UpdateTimeToLiveAsync(It.IsAny(), CancellationToken.None)) 226 | .Returns((UpdateTimeToLiveRequest r, CancellationToken token) => 227 | { 228 | throw new Exception("DescribeTimeToLiveAsync should not be called"); 229 | }); 230 | 231 | var options = new DynamoDBDistributedCacheOptions 232 | { 233 | TableName = "MyTableName", 234 | PartitionKeyName = "foo_id", 235 | TTLAttributeName = "bar_date" 236 | }; 237 | 238 | var cache = new DynamoDBDistributedCache(moqClient.Object, new DynamoDBTableCreator(), options); 239 | 240 | cache.Get("foo"); 241 | } 242 | 243 | [Fact] 244 | public void CheckExistingTableAttributesWithTTL() 245 | { 246 | const string partitionKeyName = "myId"; 247 | const string ttlName = "myTtl"; 248 | 249 | var moqClient = new Moq.Mock(); 250 | 251 | moqClient.Setup(x => x.DescribeTableAsync(It.IsAny(), CancellationToken.None)) 252 | .Returns(Task.FromResult(new DescribeTableResponse 253 | { 254 | Table = new TableDescription 255 | { 256 | KeySchema = new List 257 | { 258 | new KeySchemaElement() 259 | { 260 | AttributeName = partitionKeyName, 261 | KeyType = KeyType.HASH 262 | } 263 | } 264 | } 265 | })); 266 | 267 | moqClient.Setup(x => x.DescribeTimeToLiveAsync(It.IsAny(), CancellationToken.None)) 268 | .Returns(Task.FromResult(new DescribeTimeToLiveResponse 269 | { 270 | TimeToLiveDescription = new TimeToLiveDescription 271 | { 272 | AttributeName = ttlName, 273 | TimeToLiveStatus = TimeToLiveStatus.ENABLED 274 | } 275 | })); 276 | 277 | moqClient.Setup(x => x.GetItemAsync(It.IsAny(), CancellationToken.None)) 278 | .Callback((request, token) => 279 | { 280 | Assert.True(request.Key.ContainsKey(partitionKeyName)); 281 | }) 282 | .Returns(Task.FromResult(new GetItemResponse 283 | { 284 | Item = new Dictionary 285 | { 286 | { 287 | DynamoDBDistributedCache.TTL_WINDOW, new AttributeValue{S = new TimeSpan(1, 0, 0).ToString()} 288 | }, 289 | { 290 | ttlName, new AttributeValue{N = DateTimeOffset.Now.AddHours(1).ToUnixTimeSeconds().ToString()} 291 | }, 292 | { 293 | DynamoDBDistributedCache.TTL_DEADLINE, new AttributeValue{N = DateTimeOffset.UtcNow.AddHours(3).ToUnixTimeSeconds().ToString()} 294 | } 295 | } 296 | })); 297 | 298 | moqClient.Setup(x => x.PutItemAsync(It.IsAny(), CancellationToken.None)) 299 | .Callback((request, token) => 300 | { 301 | Assert.True(request.Item.ContainsKey(partitionKeyName)); 302 | Assert.True(request.Item.ContainsKey(ttlName)); 303 | }) 304 | .Returns(Task.FromResult(new PutItemResponse 305 | { 306 | 307 | })); 308 | 309 | 310 | var options = new DynamoDBDistributedCacheOptions 311 | { 312 | TableName = "MyTableName" 313 | }; 314 | 315 | var cache = new DynamoDBDistributedCache(moqClient.Object, new DynamoDBTableCreator(), options); 316 | 317 | cache.Get("foo"); 318 | 319 | cache.SetString("foo", "bar", new DistributedCacheEntryOptions 320 | { 321 | AbsoluteExpiration = DateTimeOffset.UtcNow.AddHours(1) 322 | }); 323 | } 324 | 325 | [Fact] 326 | public void CheckExistingTableAttributesWithoutTTL() 327 | { 328 | const string partitionKeyName = "myId"; 329 | 330 | var moqClient = new Moq.Mock(); 331 | 332 | moqClient.Setup(x => x.DescribeTableAsync(It.IsAny(), CancellationToken.None)) 333 | .Returns(Task.FromResult(new DescribeTableResponse 334 | { 335 | Table = new TableDescription 336 | { 337 | KeySchema = new List 338 | { 339 | new KeySchemaElement() 340 | { 341 | AttributeName = partitionKeyName, 342 | KeyType = KeyType.HASH 343 | } 344 | } 345 | } 346 | })); 347 | 348 | moqClient.Setup(x => x.DescribeTimeToLiveAsync(It.IsAny(), CancellationToken.None)) 349 | .Returns(Task.FromResult(new DescribeTimeToLiveResponse 350 | { 351 | TimeToLiveDescription = new TimeToLiveDescription 352 | { 353 | TimeToLiveStatus = TimeToLiveStatus.DISABLED 354 | } 355 | })); 356 | 357 | moqClient.Setup(x => x.GetItemAsync(It.IsAny(), CancellationToken.None)) 358 | .Callback((request, token) => 359 | { 360 | Assert.True(request.Key.ContainsKey(partitionKeyName)); 361 | }) 362 | .Returns(Task.FromResult(new GetItemResponse 363 | { 364 | Item = new Dictionary 365 | { 366 | { 367 | DynamoDBDistributedCache.TTL_WINDOW, new AttributeValue{S = new TimeSpan(1, 0, 0).ToString()} 368 | }, 369 | { 370 | DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME, new AttributeValue{N = DateTimeOffset.Now.AddHours(1).ToUnixTimeSeconds().ToString()} 371 | }, 372 | { 373 | DynamoDBDistributedCache.TTL_DEADLINE, new AttributeValue{N = DateTimeOffset.UtcNow.AddHours(3).ToUnixTimeSeconds().ToString()} 374 | } 375 | } 376 | })); 377 | 378 | moqClient.Setup(x => x.PutItemAsync(It.IsAny(), CancellationToken.None)) 379 | .Callback((request, token) => 380 | { 381 | Assert.True(request.Item.ContainsKey(partitionKeyName)); 382 | Assert.True(request.Item.ContainsKey(DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME)); 383 | }) 384 | .Returns(Task.FromResult(new PutItemResponse 385 | { 386 | 387 | })); 388 | 389 | 390 | var options = new DynamoDBDistributedCacheOptions 391 | { 392 | TableName = "MyTableName" 393 | }; 394 | 395 | var cache = new DynamoDBDistributedCache(moqClient.Object, new DynamoDBTableCreator(), options); 396 | 397 | cache.Get("foo"); 398 | 399 | cache.SetString("foo", "bar", new DistributedCacheEntryOptions 400 | { 401 | AbsoluteExpiration = DateTimeOffset.UtcNow.AddHours(1) 402 | }); 403 | } 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderIntegrationTests/DynamoDBDistributedCacheTableTests.cs: -------------------------------------------------------------------------------- 1 | using Amazon.DynamoDBv2; 2 | using Amazon.DynamoDBv2.Model; 3 | using AWS.DistributedCacheProvider; 4 | using AWS.DistributedCacheProvider.Internal; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | using Xunit.Sdk; 10 | 11 | namespace AWS.DistributedCacheProviderIntegrationTests 12 | { 13 | public class DynamoDBDistributedCacheTableTests(ITestOutputHelper output) 14 | { 15 | /// 16 | /// Tests that our factory will create a Table if it does not already exist. 17 | /// 18 | [Fact] 19 | public async Task CreateTable() 20 | { 21 | var tableName = IntegrationTestUtils.GetFullTestName(); 22 | var client = new AmazonDynamoDBClient(); 23 | try 24 | { 25 | //First verify that a table with this name does not already exist. 26 | try 27 | { 28 | _ = await client.DescribeTableAsync(tableName); 29 | //If no exception was thrown, then the table already exists, bad state for test. 30 | throw new XunitException("Table already exists, cannot create table"); 31 | } 32 | catch (ResourceNotFoundException) { } 33 | var cache = GetCache(options => 34 | { 35 | options.TableName = tableName; 36 | options.CreateTableIfNotExists = true; 37 | }); 38 | //With lazy implementation, table creation is delayed until the client actually needs it. 39 | //resolving the table should pass. 40 | //Key cannot be empty, otherwise the client will throw an exception 41 | cache.Get("randomCacheKey"); 42 | 43 | var table = (await client.DescribeTableAsync(tableName)).Table; 44 | 45 | // Null value indicates that data in the table is encrypted at rest via an AWS owned KMS key. 46 | // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/EncryptionAtRest.html 47 | Assert.Null(table.SSEDescription); 48 | } 49 | finally 50 | { 51 | //Delete DynamoDB table 52 | await CleanupTable(client, tableName); 53 | } 54 | } 55 | 56 | /// 57 | /// Tests that our factory will create a Table with a custom primary key if the table does not already exist. 58 | /// 59 | [Fact] 60 | public async Task CreateTableWithCustomPrimaryKey() 61 | { 62 | var tableName = IntegrationTestUtils.GetFullTestName(); 63 | var primaryKeyName = "MyKeyName"; 64 | var client = new AmazonDynamoDBClient(); 65 | try 66 | { 67 | //First verify that a table with this name does not already exist. 68 | try 69 | { 70 | _ = await client.DescribeTableAsync(tableName); 71 | //If no exception was thrown, then the table already exists, bad state for test. 72 | throw new XunitException("Table already exists, cannot create table"); 73 | } 74 | catch (ResourceNotFoundException) { } 75 | var cache = GetCache(options => 76 | { 77 | options.TableName = tableName; 78 | options.CreateTableIfNotExists = true; 79 | options.PartitionKeyName = primaryKeyName; 80 | }); 81 | //With lazy implementation, table creation is delayed until the client actually needs it. 82 | //resolving the table should pass. 83 | //Key cannot be empty, otherwise the client will throw an exception 84 | cache.Get("randomCacheKey"); 85 | //Now describe the table to see the primary key name 86 | var keySchema = (await client.DescribeTableAsync(tableName)).Table.KeySchema; 87 | Assert.Single(keySchema); 88 | var keyElement = keySchema[0]; 89 | Assert.Equal(primaryKeyName, keyElement.AttributeName); 90 | } 91 | finally 92 | { 93 | //Delete DynamoDB table 94 | await CleanupTable(client, tableName); 95 | } 96 | } 97 | 98 | /// 99 | /// Test that our Cache can load a table that already exists 100 | /// 101 | [Fact] 102 | public async Task LoadValidTableTest() 103 | { 104 | //key must match what the cache expects the key to be. Otherwise an error will be thrown when 105 | //we validate that the table is valid when we make a CRUD call. 106 | var key = DynamoDBTableCreator.DEFAULT_PARTITION_KEY; 107 | var tableName = IntegrationTestUtils.GetFullTestName(); 108 | var client = new AmazonDynamoDBClient(); 109 | //Valid table - Non-composite Hash key of type String. 110 | var request = new CreateTableRequest 111 | { 112 | TableName = tableName, 113 | KeySchema = new List 114 | { 115 | new KeySchemaElement 116 | { 117 | AttributeName = key, 118 | KeyType = KeyType.HASH 119 | } 120 | }, 121 | AttributeDefinitions = new List 122 | { 123 | new AttributeDefinition 124 | { 125 | AttributeName = key, 126 | AttributeType = ScalarAttributeType.S 127 | } 128 | }, 129 | BillingMode = BillingMode.PAY_PER_REQUEST 130 | }; 131 | try { 132 | //create the table here. 133 | await CreateAndWaitUntilActive(client, request); 134 | var cache = GetCache(options => 135 | { 136 | options.TableName = tableName; 137 | options.CreateTableIfNotExists = false; 138 | }); 139 | //With lazy implementation, table creation is delayed until the client actually needs it. 140 | //resolving the table should pass. 141 | //Key cannot be empty, otherwise the client will throw an exception 142 | cache.Get("randomCacheKey"); 143 | } 144 | finally 145 | { 146 | await CleanupTable(client, tableName); 147 | } 148 | } 149 | 150 | /// 151 | /// Tests that our cache can reject a table if it is invalid. Invalid becuase Key is non-composite 152 | /// 153 | [Fact] 154 | public async Task LoadInvalidTable_TooManyKeysTest() 155 | { 156 | var key1 = "key"; 157 | var key2 = "key2"; 158 | var tableName = IntegrationTestUtils.GetFullTestName(); 159 | var client = new AmazonDynamoDBClient(); 160 | var request = new CreateTableRequest 161 | //Invalid becuase key is non-composite 162 | { 163 | TableName = tableName, 164 | KeySchema = new List 165 | { 166 | new KeySchemaElement 167 | { 168 | AttributeName = key1, 169 | KeyType = KeyType.HASH 170 | }, 171 | new KeySchemaElement 172 | { 173 | AttributeName = key2, 174 | KeyType = KeyType.RANGE 175 | } 176 | }, 177 | AttributeDefinitions = new List 178 | { 179 | new AttributeDefinition 180 | { 181 | AttributeName = key1, 182 | AttributeType = ScalarAttributeType.S 183 | }, 184 | new AttributeDefinition 185 | { 186 | AttributeName = key2, 187 | AttributeType = ScalarAttributeType.N 188 | } 189 | }, 190 | BillingMode = BillingMode.PAY_PER_REQUEST 191 | }; 192 | try 193 | { 194 | //Create table here 195 | await CreateAndWaitUntilActive(client, request); 196 | var cache = GetCache(options => 197 | { 198 | options.TableName = tableName; 199 | options.CreateTableIfNotExists = true; 200 | }); 201 | //With lazy implementation, table creation is delayed until the client actually needs it. 202 | //resolving the table should not pass as the key is invalid. 203 | Assert.Throws(() => cache.Get("")); 204 | } 205 | finally 206 | { 207 | await CleanupTable(client, tableName); 208 | } 209 | } 210 | 211 | /// 212 | /// Tests that our cache can reject a table if it is invalid. Invalid becuase Key is not a String 213 | /// 214 | [Fact] 215 | public async Task LoadInvalidTable_BadKeyTypeTest() 216 | { 217 | var key = "key"; 218 | var tableName = IntegrationTestUtils.GetFullTestName(); 219 | var client = new AmazonDynamoDBClient(); 220 | var request = new CreateTableRequest 221 | { 222 | TableName = tableName, 223 | KeySchema = new List 224 | { 225 | new KeySchemaElement 226 | { 227 | AttributeName = key, 228 | KeyType = KeyType.HASH 229 | }, 230 | }, 231 | AttributeDefinitions = new List 232 | { 233 | new AttributeDefinition 234 | { 235 | AttributeName = key, 236 | AttributeType = ScalarAttributeType.N 237 | } 238 | }, 239 | BillingMode = BillingMode.PAY_PER_REQUEST 240 | }; 241 | try 242 | { 243 | await CreateAndWaitUntilActive(client, request); 244 | var cache = GetCache(options => 245 | { 246 | options.TableName = tableName; 247 | options.CreateTableIfNotExists = true; 248 | }); 249 | //With lazy implementation, table creation is delayed until the client actually needs it. 250 | //resolving the table should not pass as the key is invalid. 251 | Assert.Throws(() => cache.Get("")); 252 | } 253 | finally 254 | { 255 | await CleanupTable(client, tableName); 256 | } 257 | } 258 | 259 | /// 260 | /// Test that our Cache can load a table that already exists where the primary key 261 | /// of the table is different than that of the default primary key name for this 262 | /// library 263 | /// 264 | [Fact] 265 | public async Task LoadTableWithDifferentPrimaryKeyThanDefault() 266 | { 267 | //key must match what the cache expects the key to be. Otherwise an error will be thrown when 268 | //we validate that the table is valid when we make a CRUD call. 269 | var key = "foobar"; 270 | var tableName = IntegrationTestUtils.GetFullTestName(); 271 | var client = new AmazonDynamoDBClient(); 272 | var request = new CreateTableRequest 273 | { 274 | TableName = tableName, 275 | KeySchema = new List 276 | { 277 | new KeySchemaElement 278 | { 279 | AttributeName = key, 280 | KeyType = KeyType.HASH 281 | } 282 | }, 283 | AttributeDefinitions = new List 284 | { 285 | new AttributeDefinition 286 | { 287 | AttributeName = key, 288 | AttributeType = ScalarAttributeType.S 289 | } 290 | }, 291 | BillingMode = BillingMode.PAY_PER_REQUEST 292 | }; 293 | try 294 | { 295 | //create the table here. 296 | await CreateAndWaitUntilActive(client, request); 297 | var cache = GetCache(options => 298 | { 299 | options.TableName = tableName; 300 | options.CreateTableIfNotExists = true; 301 | }); 302 | //If the library recognizes the new primary key, this test passes by this 303 | //not throwing an error. Otherwise DynamoDB with throw an error 304 | //saying we have an invalid schema in our GetItem call. 305 | cache.Get("randomCacheKey"); 306 | } 307 | finally 308 | { 309 | await CleanupTable(client, tableName); 310 | } 311 | } 312 | 313 | /// 314 | /// Tests that DynamoDBTableCreator sets the DynamoDB TTL feature to the attribute the user specifies 315 | /// 316 | [Fact] 317 | public async Task CreateTableWithCustomTTLKey() 318 | { 319 | var ttl_attribute_name = "MyTTLAttributeName"; 320 | var tableName = IntegrationTestUtils.GetFullTestName(); 321 | var client = new AmazonDynamoDBClient(); 322 | try 323 | { 324 | //First verify that a table with this name does not already exist. 325 | try 326 | { 327 | _ = await client.DescribeTableAsync(tableName); 328 | //If no exception was thrown, then the table already exists, bad state for test. 329 | throw new XunitException("Table already exists, cannot create table"); 330 | } 331 | catch (ResourceNotFoundException) { } 332 | var cache = GetCache(options => 333 | { 334 | options.TableName = tableName; 335 | options.CreateTableIfNotExists = true; 336 | options.TTLAttributeName = ttl_attribute_name; 337 | }); 338 | cache.Get("randomCacheKey"); 339 | //The cache uses a DescribeTimeToLiveAsync to find the TTL Attribute name. If we verify here that it has the right name, 340 | //the cache should have the right name also. 341 | var ttlDescription = await client.DescribeTimeToLiveAsync(tableName); 342 | Assert.Equal(ttl_attribute_name, ttlDescription.TimeToLiveDescription.AttributeName); 343 | //It can take time for the status to be fully enabled. So we check for both enabled and enabling. 344 | //Both states are acceptable 345 | Assert.True(ttlDescription.TimeToLiveDescription.TimeToLiveStatus.Equals(TimeToLiveStatus.ENABLING) || 346 | ttlDescription.TimeToLiveDescription.TimeToLiveStatus.Equals(TimeToLiveStatus.ENABLED)); 347 | } 348 | finally 349 | { 350 | //Delete DynamoDB table 351 | await CleanupTable(client, tableName); 352 | } 353 | } 354 | 355 | /// 356 | /// Verifies that if the table was already created by the user in a different context with TTL already enabled on an 357 | /// attribute that is different from the library default attribute name, the cache still recognizes the correct TTL 358 | /// attribute. 359 | /// 360 | [Fact] 361 | public async Task LoadTableWithCustomTTLKey() 362 | { 363 | var ttl_attribute_name = "MyTTLAttributeName"; 364 | var key = DynamoDBTableCreator.DEFAULT_PARTITION_KEY; 365 | var tableName = IntegrationTestUtils.GetFullTestName(); 366 | var client = new AmazonDynamoDBClient(); 367 | //Valid table - Non-composite Hash key of type String. 368 | var request = new CreateTableRequest 369 | { 370 | TableName = tableName, 371 | KeySchema = new List 372 | { 373 | new KeySchemaElement 374 | { 375 | AttributeName = key, 376 | KeyType = KeyType.HASH 377 | } 378 | }, 379 | AttributeDefinitions = new List 380 | { 381 | new AttributeDefinition 382 | { 383 | AttributeName = key, 384 | AttributeType = ScalarAttributeType.S 385 | } 386 | }, 387 | BillingMode = BillingMode.PAY_PER_REQUEST 388 | }; 389 | try 390 | { 391 | //create the table here. 392 | await CreateAndWaitUntilActive(client, request); 393 | //change TTL information on table to use an attribute that is NOT the default value for this library. 394 | await client.UpdateTimeToLiveAsync(new UpdateTimeToLiveRequest 395 | { 396 | TableName = tableName, 397 | TimeToLiveSpecification = new TimeToLiveSpecification 398 | { 399 | AttributeName = ttl_attribute_name, 400 | Enabled = true 401 | } 402 | }); 403 | var cache = GetCache(options => 404 | { 405 | options.TableName = tableName; 406 | options.CreateTableIfNotExists = false; 407 | }); 408 | cache.Get("randomCacheKey"); 409 | //The cache uses a DescribeTimeToLiveAsync to find the TTL Attribute name. If we verify here that it has the right name, 410 | //the cache should have the right name also. 411 | var ttlDescription = await client.DescribeTimeToLiveAsync(tableName); 412 | Assert.Equal(ttl_attribute_name, ttlDescription.TimeToLiveDescription.AttributeName); 413 | //It can take time for the status to be fully enabled. So we check for both enabled and enabling. 414 | //Both states are acceptable 415 | Assert.True(ttlDescription.TimeToLiveDescription.TimeToLiveStatus.Equals(TimeToLiveStatus.ENABLING) || 416 | ttlDescription.TimeToLiveDescription.TimeToLiveStatus.Equals(TimeToLiveStatus.ENABLED)); 417 | } 418 | finally 419 | { 420 | await CleanupTable(client, tableName); 421 | } 422 | } 423 | 424 | private async Task CreateAndWaitUntilActive(AmazonDynamoDBClient client, CreateTableRequest request) 425 | { 426 | await client.CreateTableAsync(request); 427 | await WaitUntilActive(client, request.TableName); 428 | 429 | } 430 | 431 | private async Task WaitUntilActive(AmazonDynamoDBClient client, string tableName) 432 | { 433 | var isActive = false; 434 | var descRequest = new DescribeTableRequest 435 | { 436 | TableName = tableName 437 | }; 438 | while (!isActive) 439 | { 440 | try 441 | { 442 | var descResponse = await client.DescribeTableAsync(descRequest); 443 | var tableStatus = descResponse.Table.TableStatus; 444 | 445 | if (tableStatus == TableStatus.ACTIVE) 446 | isActive = true; 447 | } 448 | catch (Exception ex) 449 | { 450 | output.WriteLine(ex.ToString()); 451 | } 452 | finally 453 | { 454 | await Task.Delay(TimeSpan.FromSeconds(1)); 455 | } 456 | } 457 | } 458 | 459 | private async Task CleanupTable(AmazonDynamoDBClient client, string tableName) 460 | { 461 | await WaitUntilActive(client, tableName); 462 | await client.DeleteTableAsync(tableName); 463 | var exists = true; 464 | while (exists) 465 | { 466 | var resp = await client.ListTablesAsync(); 467 | if (!resp.TableNames.Contains(tableName)) 468 | { 469 | exists = false; 470 | } 471 | } 472 | } 473 | 474 | private DynamoDBDistributedCache GetCache(Action options) 475 | { 476 | var serviceContainer = new ServiceCollection(); 477 | serviceContainer.AddAWSDynamoDBDistributedCache(options); 478 | var provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(serviceContainer); 479 | return (DynamoDBDistributedCache)provider.GetService()!; 480 | } 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /test/AWS.DistributedCacheProviderIntegrationTests/DynamoDBDistributedCacheCRUDTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | using System.Text; 5 | using Amazon.DynamoDBv2; 6 | using Amazon.DynamoDBv2.Model; 7 | using AWS.DistributedCacheProvider; 8 | using AWS.DistributedCacheProvider.Internal; 9 | using Microsoft.Extensions.Caching.Distributed; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Xunit; 12 | 13 | namespace AWS.DistributedCacheProviderIntegrationTests 14 | { 15 | public class DynamoDBDistributedCacheCRUDTests : IClassFixture 16 | { 17 | public static readonly string _tableName = "AWS.DistributedCacheProviderIntegrationTests_CRUD_Tests"; 18 | private readonly AmazonDynamoDBClient _client = new(); 19 | private readonly IDistributedCache _cache; 20 | 21 | public DynamoDBDistributedCacheCRUDTests(CacheFixture fixture) 22 | { 23 | _cache = fixture._cache; 24 | } 25 | 26 | [Fact] 27 | public void Get_KeyReturnsValue_NoTTL() 28 | { 29 | var key = RandomString(); 30 | var value = Encoding.ASCII.GetBytes(RandomString()); 31 | ManualPut(key, value, new AttributeValue { NULL = true }, 32 | new AttributeValue { NULL = true }, new AttributeValue { NULL = true }); 33 | var response = _cache.Get(key); 34 | Assert.Equal(response, value); 35 | } 36 | 37 | [Fact] 38 | public void Get_ExiredItemShouldReturnNull() 39 | { 40 | var key = RandomString(); 41 | var value = Encoding.ASCII.GetBytes(RandomString()); 42 | var expiredTTL = DateTimeOffset.UtcNow.AddHours(-5).ToUnixTimeSeconds(); 43 | ManualPut(key, value, new AttributeValue { N = expiredTTL.ToString() }, 44 | new AttributeValue { NULL = true }, new AttributeValue { NULL = true }); 45 | Assert.Null(_cache.Get(key)); 46 | } 47 | 48 | [Fact] 49 | public void Remove_KeyReturnsNull() 50 | { 51 | var key = RandomString(); 52 | var value = Encoding.ASCII.GetBytes(RandomString()); 53 | ManualPut(key, value, new AttributeValue { NULL = true }, 54 | new AttributeValue { NULL = true }, new AttributeValue { NULL = true }); 55 | _cache.Remove(key); 56 | var response = _cache.Get(key); 57 | Assert.Null(response); 58 | } 59 | 60 | [Fact] 61 | public void SetAndGet() 62 | { 63 | var key = RandomString(); 64 | var value = Encoding.ASCII.GetBytes(RandomString()); 65 | _cache.Set(key, value, new DistributedCacheEntryOptions()); 66 | var resp = _cache.Get(key); 67 | Assert.Equal(value, resp); 68 | } 69 | 70 | /*Tests that relate to calculating different TTL attributes*/ 71 | [Fact] 72 | public async Task Set_NullTTLOptions() 73 | { 74 | var key = RandomString(); 75 | var value = Encoding.ASCII.GetBytes(RandomString()); 76 | _cache.Set(key, value, new DistributedCacheEntryOptions()); 77 | var resp = await GetItemAsync(key); 78 | Assert.True(resp.Item[DynamoDBDistributedCache.TTL_WINDOW].NULL); 79 | Assert.True(resp.Item[DynamoDBDistributedCache.TTL_DEADLINE].NULL); 80 | Assert.True(resp.Item[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].NULL); 81 | } 82 | 83 | [Fact] 84 | public async Task Set_OnlyWindowOptionSet_TTLWithNoDeadline() 85 | { 86 | var key = RandomString(); 87 | var value = Encoding.ASCII.GetBytes(RandomString()); 88 | var window = new TimeSpan(12, 0, 0); 89 | _cache.Set(key, value, new DistributedCacheEntryOptions 90 | { 91 | SlidingExpiration = window 92 | }); 93 | var resp = (await GetItemAsync(key)).Item; 94 | //window is 12 hours 95 | Assert.Equal(TimeSpan.Parse(resp[DynamoDBDistributedCache.TTL_WINDOW].S), window); 96 | Assert.True(resp[DynamoDBDistributedCache.TTL_DEADLINE].NULL); 97 | //ttl date is approx 12 hours from now 98 | Assert.True( 99 | Math.Abs( 100 | double.Parse(resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N) - DateTimeOffset.UtcNow.AddHours(12).ToUnixTimeSeconds()) 101 | < 100); 102 | } 103 | 104 | [Fact] 105 | public async Task Set_OnlyRelativeOptionSet_DeadlineAndTTLSet() 106 | { 107 | var key = RandomString(); 108 | var value = Encoding.ASCII.GetBytes(RandomString()); 109 | var ttl = new TimeSpan(12, 0, 0); 110 | var ttlInUnix = DateTimeOffset.UtcNow.Add(ttl).ToUnixTimeSeconds(); 111 | _cache.Set(key, value, new DistributedCacheEntryOptions 112 | { 113 | AbsoluteExpirationRelativeToNow = ttl 114 | }); 115 | var resp = (await GetItemAsync(key)).Item; 116 | Assert.True(resp[DynamoDBDistributedCache.TTL_WINDOW].NULL); 117 | Assert.Equal(resp[DynamoDBDistributedCache.TTL_DEADLINE].N, resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N); 118 | //Can't guarantee how close they will be, but within 100 seconds seems more than generous. 119 | Assert.True(Math.Abs(double.Parse(resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N) - ttlInUnix) < 100); 120 | } 121 | 122 | [Fact] 123 | public async Task Set_RelativeAndWindow_AllSet() 124 | { 125 | var key = RandomString(); 126 | var value = Encoding.ASCII.GetBytes(RandomString()); 127 | var deadline = new TimeSpan(24, 0, 0); 128 | var window = new TimeSpan(12, 0, 0); 129 | _cache.Set(key, value, new DistributedCacheEntryOptions 130 | { 131 | AbsoluteExpirationRelativeToNow = deadline, 132 | SlidingExpiration = window 133 | }); 134 | var resp = (await GetItemAsync(key)).Item; 135 | //window is 12 hours 136 | Assert.Equal(TimeSpan.Parse(resp[DynamoDBDistributedCache.TTL_WINDOW].S), window); 137 | //ttl date is approx 12 hours from now 138 | Assert.True( 139 | Math.Abs( 140 | double.Parse(resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N) - DateTimeOffset.UtcNow.AddHours(12).ToUnixTimeSeconds()) 141 | < 100); 142 | //ttl deadline is approx 24 hours away 143 | Assert.True( 144 | Math.Abs( 145 | double.Parse(resp[DynamoDBDistributedCache.TTL_DEADLINE].N) - DateTimeOffset.UtcNow.AddHours(24).ToUnixTimeSeconds()) 146 | < 100); 147 | } 148 | 149 | [Fact] 150 | public async Task Set_OnlyAbsoluteOptionSet_DeadlineAndTTLSet() 151 | { 152 | var key = RandomString(); 153 | var value = Encoding.ASCII.GetBytes(RandomString()); 154 | var deadline = DateTimeOffset.UtcNow.AddHours(12); 155 | var deadlineInUnix = deadline.ToUnixTimeSeconds(); 156 | _cache.Set(key, value, new DistributedCacheEntryOptions 157 | { 158 | AbsoluteExpiration = deadline 159 | }); 160 | var resp = (await GetItemAsync(key)).Item; 161 | Assert.True(resp[DynamoDBDistributedCache.TTL_WINDOW].NULL); 162 | Assert.Equal(resp[DynamoDBDistributedCache.TTL_DEADLINE].N, resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N); 163 | //Can't guarantee how close they will be, but within 100 seconds seems more than generous. 164 | Assert.True(Math.Abs(double.Parse(resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N) - deadlineInUnix) < 100); 165 | } 166 | 167 | [Fact] 168 | public async Task Set_AbsoluteAndWindow_AllSet() 169 | { 170 | var key = RandomString(); 171 | var value = Encoding.ASCII.GetBytes(RandomString()); 172 | var deadline = DateTimeOffset.UtcNow.AddHours(24); 173 | var deadlineInUnix = deadline.ToUnixTimeSeconds(); 174 | var window = new TimeSpan(12, 0, 0); 175 | _cache.Set(key, value, new DistributedCacheEntryOptions 176 | { 177 | AbsoluteExpiration = deadline, 178 | SlidingExpiration = window 179 | }); 180 | var resp = (await GetItemAsync(key)).Item; 181 | //window is 12 hours 182 | Assert.Equal(TimeSpan.Parse(resp[DynamoDBDistributedCache.TTL_WINDOW].S), window); 183 | //ttl date is approx 12 hours from now 184 | Assert.True( 185 | Math.Abs( 186 | double.Parse(resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N) - DateTimeOffset.UtcNow.AddHours(12).ToUnixTimeSeconds()) 187 | < 100); 188 | //ttl deadline is approx 24 hours away 189 | Assert.True( 190 | Math.Abs( 191 | double.Parse(resp[DynamoDBDistributedCache.TTL_DEADLINE].N) - DateTimeOffset.UtcNow.AddHours(24).ToUnixTimeSeconds()) 192 | < 100); 193 | } 194 | 195 | [Fact] 196 | public async Task Set_AbsoluteAndRelativeSet_RelativeTakesPrecedence() 197 | { 198 | var key = RandomString(); 199 | var value = Encoding.ASCII.GetBytes(RandomString()); 200 | var deadline = DateTimeOffset.UtcNow.AddHours(24); 201 | _cache.Set(key, value, new DistributedCacheEntryOptions 202 | { 203 | AbsoluteExpiration = deadline, 204 | AbsoluteExpirationRelativeToNow = new TimeSpan(12, 0, 0) 205 | }); 206 | var resp = (await GetItemAsync(key)).Item; 207 | //Window is null 208 | Assert.True(resp[DynamoDBDistributedCache.TTL_WINDOW].NULL); 209 | //ttl date and deadline are equal 210 | Assert.Equal(resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N, resp[DynamoDBDistributedCache.TTL_DEADLINE].N); 211 | //ttl date is approx 12 hours from now 212 | Assert.True( 213 | Math.Abs( 214 | double.Parse(resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N) - DateTimeOffset.UtcNow.AddHours(12).ToUnixTimeSeconds()) 215 | < 100); 216 | } 217 | 218 | [Fact] 219 | public async Task Set_AllOptionsUsed() 220 | { 221 | var key = RandomString(); 222 | var value = Encoding.ASCII.GetBytes(RandomString()); 223 | var deadline_abs = DateTimeOffset.UtcNow.AddHours(48); 224 | var deadline_rel = new TimeSpan(24, 0, 0); 225 | var window = new TimeSpan(12, 0, 0); 226 | _cache.Set(key, value, new DistributedCacheEntryOptions 227 | { 228 | AbsoluteExpiration = deadline_abs, 229 | AbsoluteExpirationRelativeToNow = deadline_rel, 230 | SlidingExpiration = window 231 | }); 232 | var resp = (await GetItemAsync(key)).Item; 233 | //Window is 12 hours 234 | Assert.Equal(TimeSpan.Parse(resp[DynamoDBDistributedCache.TTL_WINDOW].S), window); 235 | //ttl date is approx 12 hours from now 236 | Assert.True( 237 | Math.Abs( 238 | double.Parse(resp[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N) - DateTimeOffset.UtcNow.AddHours(12).ToUnixTimeSeconds()) 239 | < 100); 240 | //ttl deadline is approx 24 hours from now 241 | Assert.True( 242 | Math.Abs( 243 | double.Parse(resp[DynamoDBDistributedCache.TTL_DEADLINE].N) - DateTimeOffset.UtcNow.AddHours(24).ToUnixTimeSeconds()) 244 | < 100); 245 | } 246 | 247 | //There are three columns related to TTL: TTL_DATE, TTL_DEADLINE, TTL_WINDOW. We need to test combinations of all 3 248 | [Fact] 249 | public async Task Refresh_AllNullColumns() 250 | { 251 | var key = RandomString(); 252 | var value = Encoding.ASCII.GetBytes(RandomString()); 253 | ManualPut(key, value, new AttributeValue { NULL = true }, 254 | new AttributeValue { NULL = true }, new AttributeValue { NULL = true }); 255 | _cache.Refresh(key); 256 | var resp = await GetItemAsync(key); 257 | Assert.True(resp.Item[DynamoDBDistributedCache.TTL_WINDOW].NULL); 258 | Assert.True(resp.Item[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].NULL); 259 | Assert.True(resp.Item[DynamoDBDistributedCache.TTL_DEADLINE].NULL); 260 | } 261 | 262 | //If TTL_DATE is null, then the item never expires. 263 | //If TTL_DATE is not null, but TTL_WINDOW is, then TTL_DATE and TTL_DEADLINE should be equal. 264 | //Even if they are not, Refresh() is meaningless 265 | [Fact] 266 | public async Task Refresh_TTLWindowIsNull_RefreshChangesNothing() 267 | { 268 | var key = RandomString(); 269 | var value = Encoding.ASCII.GetBytes(RandomString()); 270 | //Set TTL_DATE and TTL_DEADLINE to 12 hours from now 271 | var ttl = DateTimeOffset.UtcNow.AddHours(12).ToUnixTimeSeconds().ToString(); 272 | 273 | ManualPut(key, value, new AttributeValue { N = ttl }, new AttributeValue { N = ttl }, new AttributeValue { NULL = true }); 274 | _cache.Refresh(key); 275 | var resp = await GetItemAsync(key); 276 | Assert.True(resp.Item[DynamoDBDistributedCache.TTL_WINDOW].NULL); 277 | Assert.Equal(resp.Item[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N, ttl); 278 | Assert.Equal(resp.Item[DynamoDBDistributedCache.TTL_DEADLINE].N, ttl); 279 | } 280 | 281 | [Fact] 282 | public async Task Refresh_MoveTTLWithinDeadline() 283 | { 284 | var key = RandomString(); 285 | var value = Encoding.ASCII.GetBytes(RandomString()); 286 | //Set TTL_DATE to 12 hours from now 287 | //Set TTL_DEADLINE to 48 hours from now 288 | //Set TTL_WINDOW to 24 hours 289 | var ttl_date = DateTimeOffset.UtcNow.AddHours(12).ToUnixTimeSeconds().ToString(); 290 | var ttl_deadline = DateTimeOffset.UtcNow.AddHours(48).ToUnixTimeSeconds().ToString(); 291 | var ttl_window = new TimeSpan(24, 0, 0).ToString(); 292 | ManualPut(key, value, new AttributeValue { N = ttl_date }, 293 | new AttributeValue { N = ttl_deadline }, new AttributeValue { S = ttl_window }); 294 | _cache.Refresh(key); 295 | var resp = await GetItemAsync(key); 296 | //window stays the same 297 | Assert.Equal(resp.Item[DynamoDBDistributedCache.TTL_WINDOW].S, ttl_window); 298 | //deadline stays the same 299 | Assert.Equal(resp.Item[DynamoDBDistributedCache.TTL_DEADLINE].N, ttl_deadline); 300 | //refresh moved TTL_DATE to approx 24 hours from now 301 | Assert.True( 302 | Math.Abs( 303 | double.Parse(resp.Item[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N) - DateTimeOffset.UtcNow.AddHours(24).ToUnixTimeSeconds() 304 | ) < 100); 305 | } 306 | 307 | [Fact] 308 | public async Task Refresh_MoveTTLToDeadline() 309 | { 310 | var key = RandomString(); 311 | var value = Encoding.ASCII.GetBytes(RandomString()); 312 | //Set TTL_DATE to 6 hours from now 313 | //Set TTL_DEADLINE to 18 hours from now 314 | //Set TTL_WINDOW to 24 hours 315 | var ttl_date = DateTimeOffset.UtcNow.AddHours(6).ToUnixTimeSeconds().ToString(); 316 | var ttl_deadline = DateTimeOffset.UtcNow.AddHours(18).ToUnixTimeSeconds().ToString(); 317 | var ttl_window = new TimeSpan(24, 0, 0).ToString(); 318 | ManualPut(key, value, new AttributeValue { N = ttl_date }, 319 | new AttributeValue { N = ttl_deadline }, new AttributeValue { S = ttl_window }); 320 | _cache.Refresh(key); 321 | var resp = await GetItemAsync(key); 322 | //window stays the same 323 | Assert.Equal(resp.Item[DynamoDBDistributedCache.TTL_WINDOW].S, ttl_window); 324 | //refresh moves TTL_DATE to be equal to TTL_DEADLINE 325 | Assert.Equal(resp.Item[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N, resp.Item[DynamoDBDistributedCache.TTL_DEADLINE].N); 326 | //deadline stays the same 327 | Assert.Equal(resp.Item[DynamoDBDistributedCache.TTL_DEADLINE].N, ttl_deadline); 328 | } 329 | 330 | [Fact] 331 | public async Task Get_Also_Refreshes() 332 | { 333 | var key = RandomString(); 334 | var value = Encoding.ASCII.GetBytes(RandomString()); 335 | //Set TTL_DATE to 6 hours from now 336 | //Set TTL_DEADLINE to 18 hours from now 337 | //Set TTL_WINDOW to 24 hours 338 | var ttl_date = DateTimeOffset.UtcNow.AddHours(6).ToUnixTimeSeconds().ToString(); 339 | var ttl_deadline = DateTimeOffset.UtcNow.AddHours(18).ToUnixTimeSeconds().ToString(); 340 | var ttl_window = new TimeSpan(24, 0, 0).ToString(); 341 | ManualPut(key, value, new AttributeValue { N = ttl_date }, 342 | new AttributeValue { N = ttl_deadline }, new AttributeValue { S = ttl_window }); 343 | //Instead of Refresh, call Get to have the same result 344 | _cache.Get(key); 345 | var resp = await GetItemAsync(key); 346 | //window stays the same 347 | Assert.Equal(resp.Item[DynamoDBDistributedCache.TTL_WINDOW].S, ttl_window); 348 | //refresh moves TTL_DATE to be equal to TTL_DEADLINE 349 | Assert.Equal(resp.Item[DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME].N, resp.Item[DynamoDBDistributedCache.TTL_DEADLINE].N); 350 | //deadline stays the same 351 | Assert.Equal(resp.Item[DynamoDBDistributedCache.TTL_DEADLINE].N, ttl_deadline); 352 | } 353 | 354 | public static string RandomString() 355 | { 356 | var random = new Random(); 357 | var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 358 | return new string(Enumerable.Repeat(chars, 8) 359 | .Select(s => s[random.Next(s.Length)]).ToArray()); 360 | } 361 | 362 | private Task GetItemAsync(string key) 363 | { 364 | return _client.GetItemAsync(new GetItemRequest 365 | { 366 | TableName = _tableName, 367 | Key = new Dictionary 368 | { 369 | { 370 | DynamoDBTableCreator.DEFAULT_PARTITION_KEY, new AttributeValue{S = Utilities.FormatPartitionKey(key, null)} 371 | } 372 | } 373 | }); 374 | } 375 | 376 | private void ManualPut(string key, byte[] value, AttributeValue ttl, AttributeValue deadline, AttributeValue window) 377 | { 378 | _client.PutItemAsync(new PutItemRequest 379 | { 380 | TableName = _tableName, 381 | Item = new Dictionary 382 | { 383 | { 384 | DynamoDBTableCreator.DEFAULT_PARTITION_KEY, new AttributeValue { S = Utilities.FormatPartitionKey(key, null)} 385 | }, 386 | { 387 | DynamoDBDistributedCache.VALUE_KEY, new AttributeValue {B = new MemoryStream(value)} 388 | }, 389 | { 390 | DynamoDBDistributedCache.DEFAULT_TTL_ATTRIBUTE_NAME, ttl 391 | }, 392 | { 393 | DynamoDBDistributedCache.TTL_DEADLINE, deadline 394 | }, 395 | { 396 | DynamoDBDistributedCache.TTL_WINDOW, window 397 | } 398 | } 399 | }).GetAwaiter().GetResult(); 400 | } 401 | 402 | public class CacheFixture : IDisposable 403 | { 404 | public readonly DynamoDBDistributedCache _cache; 405 | private readonly AmazonDynamoDBClient _client = new(); 406 | 407 | public CacheFixture() 408 | { 409 | var serviceContainer = new ServiceCollection(); 410 | serviceContainer.AddAWSDynamoDBDistributedCache(options => 411 | { 412 | options.TableName = DynamoDBDistributedCacheCRUDTests._tableName; 413 | options.CreateTableIfNotExists = true; 414 | }); 415 | var provider = ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(serviceContainer); 416 | _cache = (DynamoDBDistributedCache)provider.GetService()!; 417 | 418 | // Force the table being created before any test run 419 | _cache.Get(Guid.NewGuid().ToString()); 420 | } 421 | 422 | public void Dispose() 423 | { 424 | var isActive = false; 425 | while (!isActive) 426 | { 427 | var descRequest = new DescribeTableRequest 428 | { 429 | TableName = _tableName 430 | }; 431 | var descResponse = _client.DescribeTableAsync(descRequest).GetAwaiter().GetResult(); 432 | var tableStatus = descResponse.Table.TableStatus; 433 | 434 | if (tableStatus == TableStatus.ACTIVE) 435 | isActive = true; 436 | } 437 | _client.DeleteTableAsync(_tableName).Wait(); 438 | var exists = true; 439 | while (exists) 440 | { 441 | var task = _client.ListTablesAsync(); 442 | var resp = task.Result; 443 | if (!resp.TableNames.Contains(_tableName)) 444 | { 445 | exists = false; 446 | } 447 | } 448 | } 449 | } 450 | } 451 | } 452 | --------------------------------------------------------------------------------