├── 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 | 
2 |
3 | # AWS .NET Distributed Cache Provider [ ](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 |
--------------------------------------------------------------------------------