diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 82256e43..910584a0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -30,13 +30,15 @@ jobs: dotnet-version: "6.0.x" - name: Verify README generation - uses: momentohq/standards-and-practices/github-actions/oss-readme-template@gh-actions-v1 + uses: momentohq/standards-and-practices/github-actions/oss-readme-template@gh-actions-v2 with: project_status: official project_stability: stable project_type: sdk sdk_language: .NET - usage_example_path: ./examples/MomentoUsage/Program.cs + template_file: ./README.template.md + output_file: ./README.md + dev_docs_slug: dotnet - name: Commitlint and Other Shared Build Steps uses: momentohq/standards-and-practices/github-actions/shared-build@gh-actions-v1 @@ -68,22 +70,35 @@ jobs: with: dotnet-version: "6.0.x" - - name: Run samples - id: validation + - name: Run MomentoApplication example + id: validation-momentoapplication continue-on-error: true + working-directory: ./examples/MomentoApplication + run: dotnet run + + - name: Run doc API snippets + id: validation-docexampleapis + continue-on-error: true + working-directory: ./examples/DocExampleApis + run: dotnet run + + - name: Test example failure + id: test-example-failure run: | - pushd examples - dotnet build MomentoApplication - dotnet run --project MomentoApplication - popd + if [[ "${{ steps.validation-momentoapplication.outcome }}" == "failure" || "${{ steps.validation-docexampleapis.outcome }}" == "failure" ]] + then + echo "failure=true" >> $GITHUB_OUTPUT + else + echo "failure=false" >> $GITHUB_OUTPUT + fi - name: Send CI failure mail - if: ${{ steps.validation.outcome == 'failure' }} + if: ${{ steps.test-example-failure.outputs.failure == 'true' }} uses: momentohq/standards-and-practices/github-actions/error-email-action@gh-actions-v1 with: username: ${{secrets.MOMENTO_ROBOT_GMAIL_USERNAME}} password: ${{secrets.MOMENTO_ROBOT_GMAIL_PASSWORD}} - name: Flag Job Failure - if: ${{ steps.validation.outcome == 'failure' }} + if: ${{ steps.test-example-failure.outputs.failure == 'true' }} run: exit 1 diff --git a/.github/workflows/on-push-to-main-branch.yaml b/.github/workflows/on-push-to-main-branch.yaml index 3f57668f..3ef97e4f 100644 --- a/.github/workflows/on-push-to-main-branch.yaml +++ b/.github/workflows/on-push-to-main-branch.yaml @@ -42,10 +42,12 @@ jobs: run: dotnet test -f ${{ matrix.target-framework }} tests/Integration/Momento.Sdk.Tests - name: Generate README - uses: momentohq/standards-and-practices/github-actions/generate-and-commit-oss-readme@gh-actions-v1 + uses: momentohq/standards-and-practices/github-actions/generate-and-commit-oss-readme@gh-actions-v2 with: project_status: official project_stability: stable project_type: sdk sdk_language: .NET - usage_example_path: ./examples/MomentoUsage/Program.cs + template_file: ./README.template.md + output_file: ./README.md + dev_docs_slug: dotnet diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff49a1a9..1ae8818d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,19 +1,19 @@ -TODO: add more info here. +# Running tests -To run unit tests: +Unless you are testing older .NET runtimes on Windows, you should run the tests against the newer runtimes as follows: ``` -dotnet test tests/Unit/Momento.Sdk.Tests +make test-net6 ``` -To run integration tests: +To test against older .NET runtimes run: ``` -dotnet test tests/Integration/Momento.Sdk.Tests +make test-net-framework ``` To run specific tests: ``` -dotnet test --filter "FullyQualifiedName~CacheDataTest" +dotnet test -f net6.0 --filter "FullyQualifiedName~CacheDataTest" ``` diff --git a/Makefile b/Makefile index 2550e18d..15c45e8f 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,24 @@ test: @dotnet test +.PHONY: test-net6 +## Run unit and integration tests on the .NET 6.0 runtime +test-net6: + @dotnet test -f net6.0 + + +.PHONY: test-net-framework +## Run unit and integration tests on the .NET Framework runtime +test-net-framework: + @dotnet test -f net461 + + +.PHONY: run-examples +## Run example applications and snippets +run-examples: + @dotnet run --project examples/MomentoApplication + @dotnet run --project examples/DocExampleApis + # See for explanation. .PHONY: help help: diff --git a/README.md b/README.md index 0fee22f9..347ddac5 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,29 @@ - + logo [![project status](https://momentohq.github.io/standards-and-practices/badges/project-status-official.svg)](https://github.com/momentohq/standards-and-practices/blob/main/docs/momento-on-github.md) -[![project stability](https://momentohq.github.io/standards-and-practices/badges/project-stability-stable.svg)](https://github.com/momentohq/standards-and-practices/blob/main/docs/momento-on-github.md) +[![project stability](https://momentohq.github.io/standards-and-practices/badges/project-stability-stable.svg)](https://github.com/momentohq/standards-and-practices/blob/main/docs/momento-on-github.md) # Momento .NET Client Library +Momento Cache is a fast, simple, pay-as-you-go caching solution without any of the operational overhead +required by traditional caching solutions. This repo contains the source code for the Momento .NET client library. -.NET client SDK for Momento Serverless Cache: a fast, simple, pay-as-you-go caching solution without -any of the operational overhead required by traditional caching solutions! - - +* Website: [https://www.gomomento.com/](https://www.gomomento.com/) +* Momento Documentation: [https://docs.momentohq.com/](https://docs.momentohq.com/) +* Getting Started: [https://docs.momentohq.com/getting-started](https://docs.momentohq.com/getting-started) +* .NET SDK Documentation: [https://docs.momentohq.com/develop/sdks/dotnet](https://docs.momentohq.com/develop/sdks/dotnet) +* Discuss: [Momento Discord](https://discord.gg/3HkAKjUZGq) Japanese: [日本語](README.ja.md) -## Getting Started :running: - -### Requirements - -You will need the [`dotnet` runtime and command line tools](https://dotnet.microsoft.com/en-us/download). After installing them, you should have the `dotnet` command on your PATH. - -**IDE Notes**: You will most likely want an IDE that supports .NET development, such as [Microsoft Visual Studio](https://visualstudio.microsoft.com/vs), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Microsoft Visual Studio Code](https://code.visualstudio.com/). - -### Examples - -Ready to dive right in? Just check out the [examples](./examples/README.md) directory for complete, working examples of -how to use the SDK. - -### Momento Response Types - -The return values of the methods on the Momento `CacheClient` class are designed to allow you to use your -IDE to help you easily discover all the possible responses, including errors. We use [pattern matching](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching) to distinguish between different types of responses, -which means that you can get compile-time safety when interacting with the API, rather than having bugs sneak in at runtime. - -Here's an example: - -```csharp -CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); -if (getResponse is CacheGetResponse.Hit hitResponse) -{ - Console.WriteLine($"Looked up value: {hitResponse.ValueString}, Stored value: {VALUE}"); -} -else if (getResponse is CacheGetResponse.Error getError) -{ - Console.WriteLine($"Error getting value: {getError.Message}"); -} -``` - -See the [Error Handling](#error-handling) section below for more details. - -### Installation +## Packages -To create a new .NET project and add the Momento client library as a dependency: +The Momento Dotnet SDK package is available on nuget: [momentohq/client-sdk-dotnet](https://www.nuget.org/packages/Momento.Sdk). -```bash -mkdir my-momento-dotnet-project -cd my-momento-dotnet-project -dotnet new console -dotnet add package Momento.Sdk -``` - -### Usage +## Usage Here is a quickstart you can use in your own project: @@ -113,71 +74,18 @@ using (ICacheClient client = new CacheClient(Configurations.Laptop.V1(), authPro Note that the above code requires an environment variable named MOMENTO_AUTH_TOKEN which must be set to a valid [Momento authentication token](https://docs.momentohq.com/docs/getting-started#obtain-an-auth-token). -### Error Handling +## Getting Started and Documentation -Error that occur in calls to CacheClient methods are surfaced to developers as part of the return values of -the calls, as opposed to by throwing exceptions. This makes them more visible, and allows your IDE to be more -helpful in ensuring that you've handled the ones you care about. (For more on our philosophy about this, see our -blog post on why [Exceptions are bugs](https://www.gomomento.com/blog/exceptions-are-bugs). And send us any -feedback you have!) +Documentation is available on the [Momento Docs website](https://docs.momentohq.com). -The preferred way of interpreting the return values from CacheClient methods is using [Pattern matching](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching). Here's a quick example: +## Examples -```csharp -CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); -if (getResponse is CacheGetResponse.Hit hitResponse) -{ - Console.WriteLine($"\nLooked up value: {hitResponse.ValueString}, Stored value: {VALUE}"); -} else { - // you can handle other cases via pattern matching in `else if` blocks, or a default case - // via the `else` block. For each return value your IDE should be able to give you code - // completion indicating the other possible types; in this case, `CacheGetResponse.Miss` and - // `CacheGetResponse.Error`. -} -``` - -Using this approach, you get a type-safe `hitResponse` object in the case of a cache hit. But if the cache read -results in a Miss or an error, you'll also get a type-safe object that you can use to get more info about what happened. - -In cases where you get an error response, `Error` types will always include an `ErrorCode` that you can use to check -the error type: - -```csharp -CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); -if (getResponse is CacheGetResponse.Error errorResponse) -{ - if (errorResponse.ErrorCode == MomentoErrorCode.TIMEOUT_ERROR) { - // this would represent a client-side timeout, and you could fall back to your original data source - } -} -``` - -Note that, outside of CacheClient responses, exceptions can occur and should be handled as usual. For example, trying -to instantiate a CacheClient with an invalid authentication token will result in an IllegalArgumentException being thrown. - -### Tuning - -Momento client-libraries provide pre-built configuration bundles out-of-the-box. We want to do the hard work of -tuning for different environments for you, so that you can focus on the things that are unique to your business. -(We even have a blog series about it! [Shockingly simple: Cache clients that do the hard work for you](https://www.gomomento.com/blog/shockingly-simple-cache-clients-that-do-the-hard-work-for-you)) - -You can find the pre-built configurations in our `Configurations` namespace. Some of the pre-built configurations that -you might be interested in: - -- `Configurations.Laptop` - this one is a development environment, just for poking around. It has relaxed timeouts - and assumes that your network latencies might be a bit high. -- `Configurations.InRegion.Default` - provides defaults suitable for an environment where your client is running in the same region as the Momento - service. It has more aggressive timeouts and retry behavior than the Laptop config. -- `Configurations.InRegion.LowLatency` - This config prioritizes keeping p99.9 latencies as low as possible, potentially sacrificing - some throughput to achieve this. Use this configuration if the most important factor is to ensure that cache - unavailability doesn't force unacceptably high latencies for your own application. +Ready to dive right in? Just check out the [examples](./examples/README.md) directory for complete, working examples of +how to use the SDK. -We hope that these configurations will meet the needs of most users, but if you find them lacking in any way, please -open a github issue, or contact us at `support@momentohq.com`. We would love to hear about your use case so that we -can fix or extend the pre-built configs to support it. +## Developing -If you do need to customize your configuration beyond what our pre-builts provide, see the -[Advanced Configuration Guide](./docs/advanced-config.md). +If you are interested in contributing to the SDK, please see the [CONTRIBUTING](./CONTRIBUTING.md) docs. ---------------------------------------------------------------------------------------- For more info, visit our website at [https://gomomento.com](https://gomomento.com)! diff --git a/README.old.md b/README.old.md new file mode 100644 index 00000000..59da2f6f --- /dev/null +++ b/README.old.md @@ -0,0 +1,184 @@ + + + +logo + +[![project status](https://momentohq.github.io/standards-and-practices/badges/project-status-official.svg)](https://github.com/momentohq/standards-and-practices/blob/main/docs/momento-on-github.md) +[![project stability](https://momentohq.github.io/standards-and-practices/badges/project-stability-stable.svg)](https://github.com/momentohq/standards-and-practices/blob/main/docs/momento-on-github.md) + +# Momento .NET Client Library + + +.NET client SDK for Momento Serverless Cache: a fast, simple, pay-as-you-go caching solution without +any of the operational overhead required by traditional caching solutions! + + + +Japanese: [日本語](README.ja.md) + +## Getting Started :running: + +### Requirements + +You will need the [`dotnet` runtime and command line tools](https://dotnet.microsoft.com/en-us/download). After installing them, you should have the `dotnet` command on your PATH. + +**IDE Notes**: You will most likely want an IDE that supports .NET development, such as [Microsoft Visual Studio](https://visualstudio.microsoft.com/vs), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Microsoft Visual Studio Code](https://code.visualstudio.com/). + +### Examples + +Ready to dive right in? Just check out the [examples](./examples/README.md) directory for complete, working examples of +how to use the SDK. + +### Momento Response Types + +The return values of the methods on the Momento `CacheClient` class are designed to allow you to use your +IDE to help you easily discover all the possible responses, including errors. We use [pattern matching](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching) to distinguish between different types of responses, +which means that you can get compile-time safety when interacting with the API, rather than having bugs sneak in at runtime. + +Here's an example: + +```csharp +CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); +if (getResponse is CacheGetResponse.Hit hitResponse) +{ + Console.WriteLine($"Looked up value: {hitResponse.ValueString}, Stored value: {VALUE}"); +} +else if (getResponse is CacheGetResponse.Error getError) +{ + Console.WriteLine($"Error getting value: {getError.Message}"); +} +``` + +See the [Error Handling](#error-handling) section below for more details. + +### Installation + +To create a new .NET project and add the Momento client library as a dependency: + +```bash +mkdir my-momento-dotnet-project +cd my-momento-dotnet-project +dotnet new console +dotnet add package Momento.Sdk +``` + +### Usage + +Here is a quickstart you can use in your own project: + +```csharp +using System; +using Momento.Sdk; +using Momento.Sdk.Auth; +using Momento.Sdk.Config; +using Momento.Sdk.Responses; + +ICredentialProvider authProvider = new EnvMomentoTokenProvider("MOMENTO_AUTH_TOKEN"); +const string CACHE_NAME = "cache"; +const string KEY = "MyKey"; +const string VALUE = "MyData"; +TimeSpan DEFAULT_TTL = TimeSpan.FromSeconds(60); + +using (ICacheClient client = new CacheClient(Configurations.Laptop.V1(), authProvider, DEFAULT_TTL)) +{ + var createCacheResponse = await client.CreateCacheAsync(CACHE_NAME); + if (createCacheResponse is CreateCacheResponse.Error createError) + { + Console.WriteLine($"Error creating cache: {createError.Message}. Exiting."); + Environment.Exit(1); + } + + Console.WriteLine($"Setting key: {KEY} with value: {VALUE}"); + var setResponse = await client.SetAsync(CACHE_NAME, KEY, VALUE); + if (setResponse is CacheSetResponse.Error setError) + { + Console.WriteLine($"Error setting value: {setError.Message}. Exiting."); + Environment.Exit(1); + } + + Console.WriteLine($"Get value for key: {KEY}"); + CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); + if (getResponse is CacheGetResponse.Hit hitResponse) + { + Console.WriteLine($"Looked up value: {hitResponse.ValueString}, Stored value: {VALUE}"); + } + else if (getResponse is CacheGetResponse.Error getError) + { + Console.WriteLine($"Error getting value: {getError.Message}"); + } +} + +``` + +Note that the above code requires an environment variable named MOMENTO_AUTH_TOKEN which must +be set to a valid [Momento authentication token](https://docs.momentohq.com/docs/getting-started#obtain-an-auth-token). + +### Error Handling + +Error that occur in calls to CacheClient methods are surfaced to developers as part of the return values of +the calls, as opposed to by throwing exceptions. This makes them more visible, and allows your IDE to be more +helpful in ensuring that you've handled the ones you care about. (For more on our philosophy about this, see our +blog post on why [Exceptions are bugs](https://www.gomomento.com/blog/exceptions-are-bugs). And send us any +feedback you have!) + +The preferred way of interpreting the return values from CacheClient methods is using [Pattern matching](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching). Here's a quick example: + +```csharp +CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); +if (getResponse is CacheGetResponse.Hit hitResponse) +{ + Console.WriteLine($"\nLooked up value: {hitResponse.ValueString}, Stored value: {VALUE}"); +} else { + // you can handle other cases via pattern matching in `else if` blocks, or a default case + // via the `else` block. For each return value your IDE should be able to give you code + // completion indicating the other possible types; in this case, `CacheGetResponse.Miss` and + // `CacheGetResponse.Error`. +} +``` + +Using this approach, you get a type-safe `hitResponse` object in the case of a cache hit. But if the cache read +results in a Miss or an error, you'll also get a type-safe object that you can use to get more info about what happened. + +In cases where you get an error response, `Error` types will always include an `ErrorCode` that you can use to check +the error type: + +```csharp +CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); +if (getResponse is CacheGetResponse.Error errorResponse) +{ + if (errorResponse.ErrorCode == MomentoErrorCode.TIMEOUT_ERROR) { + // this would represent a client-side timeout, and you could fall back to your original data source + } +} +``` + +Note that, outside of CacheClient responses, exceptions can occur and should be handled as usual. For example, trying +to instantiate a CacheClient with an invalid authentication token will result in an IllegalArgumentException being thrown. + +### Tuning + +Momento client-libraries provide pre-built configuration bundles out-of-the-box. We want to do the hard work of +tuning for different environments for you, so that you can focus on the things that are unique to your business. +(We even have a blog series about it! [Shockingly simple: Cache clients that do the hard work for you](https://www.gomomento.com/blog/shockingly-simple-cache-clients-that-do-the-hard-work-for-you)) + +You can find the pre-built configurations in our `Configurations` namespace. Some of the pre-built configurations that +you might be interested in: + +- `Configurations.Laptop` - this one is a development environment, just for poking around. It has relaxed timeouts + and assumes that your network latencies might be a bit high. +- `Configurations.InRegion.Default` - provides defaults suitable for an environment where your client is running in the same region as the Momento + service. It has more aggressive timeouts and retry behavior than the Laptop config. +- `Configurations.InRegion.LowLatency` - This config prioritizes keeping p99.9 latencies as low as possible, potentially sacrificing + some throughput to achieve this. Use this configuration if the most important factor is to ensure that cache + unavailability doesn't force unacceptably high latencies for your own application. + +We hope that these configurations will meet the needs of most users, but if you find them lacking in any way, please +open a github issue, or contact us at `support@momentohq.com`. We would love to hear about your use case so that we +can fix or extend the pre-built configs to support it. + +If you do need to customize your configuration beyond what our pre-builts provide, see the +[Advanced Configuration Guide](./docs/advanced-config.md). + +---------------------------------------------------------------------------------------- +For more info, visit our website at [https://gomomento.com](https://gomomento.com)! + diff --git a/README.template.md b/README.template.md index 1c96ee45..a28eb5c5 100644 --- a/README.template.md +++ b/README.template.md @@ -2,127 +2,32 @@ Japanese: [日本語](README.ja.md) -## Getting Started :running: +## Packages -### Requirements +The Momento Dotnet SDK package is available on nuget: [momentohq/client-sdk-dotnet](https://www.nuget.org/packages/Momento.Sdk). -You will need the [`dotnet` runtime and command line tools](https://dotnet.microsoft.com/en-us/download). After installing them, you should have the `dotnet` command on your PATH. - -**IDE Notes**: You will most likely want an IDE that supports .NET development, such as [Microsoft Visual Studio](https://visualstudio.microsoft.com/vs), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Microsoft Visual Studio Code](https://code.visualstudio.com/). - -### Examples - -Ready to dive right in? Just check out the [examples](./examples/README.md) directory for complete, working examples of -how to use the SDK. - -### Momento Response Types - -The return values of the methods on the Momento `CacheClient` class are designed to allow you to use your -IDE to help you easily discover all the possible responses, including errors. We use [pattern matching](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching) to distinguish between different types of responses, -which means that you can get compile-time safety when interacting with the API, rather than having bugs sneak in at runtime. - -Here's an example: - -```csharp -CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); -if (getResponse is CacheGetResponse.Hit hitResponse) -{ - Console.WriteLine($"Looked up value: {hitResponse.ValueString}, Stored value: {VALUE}"); -} -else if (getResponse is CacheGetResponse.Error getError) -{ - Console.WriteLine($"Error getting value: {getError.Message}"); -} -``` - -See the [Error Handling](#error-handling) section below for more details. - -### Installation - -To create a new .NET project and add the Momento client library as a dependency: - -```bash -mkdir my-momento-dotnet-project -cd my-momento-dotnet-project -dotnet new console -dotnet add package Momento.Sdk -``` - -### Usage +## Usage Here is a quickstart you can use in your own project: ```csharp -{{ usageExampleCode }} +{% include "./examples/MomentoUsage/Program.cs" %} ``` Note that the above code requires an environment variable named MOMENTO_AUTH_TOKEN which must be set to a valid [Momento authentication token](https://docs.momentohq.com/docs/getting-started#obtain-an-auth-token). -### Error Handling - -Error that occur in calls to CacheClient methods are surfaced to developers as part of the return values of -the calls, as opposed to by throwing exceptions. This makes them more visible, and allows your IDE to be more -helpful in ensuring that you've handled the ones you care about. (For more on our philosophy about this, see our -blog post on why [Exceptions are bugs](https://www.gomomento.com/blog/exceptions-are-bugs). And send us any -feedback you have!) - -The preferred way of interpreting the return values from CacheClient methods is using [Pattern matching](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching). Here's a quick example: - -```csharp -CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); -if (getResponse is CacheGetResponse.Hit hitResponse) -{ - Console.WriteLine($"\nLooked up value: {hitResponse.ValueString}, Stored value: {VALUE}"); -} else { - // you can handle other cases via pattern matching in `else if` blocks, or a default case - // via the `else` block. For each return value your IDE should be able to give you code - // completion indicating the other possible types; in this case, `CacheGetResponse.Miss` and - // `CacheGetResponse.Error`. -} -``` +## Getting Started and Documentation -Using this approach, you get a type-safe `hitResponse` object in the case of a cache hit. But if the cache read -results in a Miss or an error, you'll also get a type-safe object that you can use to get more info about what happened. +Documentation is available on the [Momento Docs website](https://docs.momentohq.com). -In cases where you get an error response, `Error` types will always include an `ErrorCode` that you can use to check -the error type: +## Examples -```csharp -CacheGetResponse getResponse = await client.GetAsync(CACHE_NAME, KEY); -if (getResponse is CacheGetResponse.Error errorResponse) -{ - if (errorResponse.ErrorCode == MomentoErrorCode.TIMEOUT_ERROR) { - // this would represent a client-side timeout, and you could fall back to your original data source - } -} -``` - -Note that, outside of CacheClient responses, exceptions can occur and should be handled as usual. For example, trying -to instantiate a CacheClient with an invalid authentication token will result in an IllegalArgumentException being thrown. - -### Tuning - -Momento client-libraries provide pre-built configuration bundles out-of-the-box. We want to do the hard work of -tuning for different environments for you, so that you can focus on the things that are unique to your business. -(We even have a blog series about it! [Shockingly simple: Cache clients that do the hard work for you](https://www.gomomento.com/blog/shockingly-simple-cache-clients-that-do-the-hard-work-for-you)) - -You can find the pre-built configurations in our `Configurations` namespace. Some of the pre-built configurations that -you might be interested in: - -- `Configurations.Laptop` - this one is a development environment, just for poking around. It has relaxed timeouts - and assumes that your network latencies might be a bit high. -- `Configurations.InRegion.Default` - provides defaults suitable for an environment where your client is running in the same region as the Momento - service. It has more aggressive timeouts and retry behavior than the Laptop config. -- `Configurations.InRegion.LowLatency` - This config prioritizes keeping p99.9 latencies as low as possible, potentially sacrificing - some throughput to achieve this. Use this configuration if the most important factor is to ensure that cache - unavailability doesn't force unacceptably high latencies for your own application. +Ready to dive right in? Just check out the [examples](./examples/README.md) directory for complete, working examples of +how to use the SDK. -We hope that these configurations will meet the needs of most users, but if you find them lacking in any way, please -open a github issue, or contact us at `support@momentohq.com`. We would love to hear about your use case so that we -can fix or extend the pre-built configs to support it. +## Developing -If you do need to customize your configuration beyond what our pre-builts provide, see the -[Advanced Configuration Guide](./docs/advanced-config.md). +If you are interested in contributing to the SDK, please see the [CONTRIBUTING](./CONTRIBUTING.md) docs. {{ ossFooter }} diff --git a/examples/DocExampleApis/DocExampleApis.csproj b/examples/DocExampleApis/DocExampleApis.csproj new file mode 100644 index 00000000..cb517518 --- /dev/null +++ b/examples/DocExampleApis/DocExampleApis.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/examples/DocExampleApis/Program.cs b/examples/DocExampleApis/Program.cs new file mode 100644 index 00000000..87ca1d50 --- /dev/null +++ b/examples/DocExampleApis/Program.cs @@ -0,0 +1,125 @@ +using Momento.Sdk; +using Momento.Sdk.Auth; +using Momento.Sdk.Config; +using Momento.Sdk.Responses; + + +public class Program +{ + public static async Task Main(string[] args) + { + var config = Configurations.Laptop.V1(); + var client = new CacheClient(config, + new EnvMomentoTokenProvider("MOMENTO_AUTH_TOKEN"), + TimeSpan.FromSeconds(10)); + + await Example_API_CreateCache(client); + await Example_API_FlushCache(client); + await Example_API_DeleteCache(client); + await Example_API_ListCaches(client); + + await Example_API_CreateCache(client); + await Example_API_Set(client); + await Example_API_Get(client); + await Example_API_Delete(client); + } + + public static async Task Example_API_CreateCache(CacheClient cacheClient) + { + var result = await cacheClient.CreateCacheAsync("test-cache"); + if (result is CreateCacheResponse.Success) + { + Console.WriteLine("Cache 'test-cache' created"); + } + else if (result is CreateCacheResponse.CacheAlreadyExists) + { + Console.WriteLine("Cache 'test-cache' already exists"); + } + else if (result is CreateCacheResponse.Error error) + { + throw new Exception($"An error occurred while attempting to create cache 'test-cache': {error.ErrorCode}: {error}"); + } + } + + public static async Task Example_API_DeleteCache(CacheClient cacheClient) + { + var result = await cacheClient.DeleteCacheAsync("test-cache"); + if (result is DeleteCacheResponse.Success) + { + Console.WriteLine("Cache 'test-cache' deleted"); + } + else if (result is DeleteCacheResponse.Error error) + { + throw new Exception($"An error occurred while attempting to delete cache 'test-cache': {error.ErrorCode}: {error}"); + } + } + + public static async Task Example_API_ListCaches(CacheClient cacheClient) + { + var result = await cacheClient.ListCachesAsync(); + if (result is ListCachesResponse.Success success) + { + Console.WriteLine($"Caches:\n{string.Join("\n", success.Caches.Select(c => c.Name))}\n\n"); + } + else if (result is ListCachesResponse.Error error) + { + throw new Exception($"An error occurred while attempting to list caches: {error.ErrorCode}: {error}"); + } + } + + public static async Task Example_API_FlushCache(CacheClient cacheClient) + { + var result = await cacheClient.FlushCacheAsync("test-cache"); + if (result is FlushCacheResponse.Success) + { + Console.WriteLine("Cache 'test-cache' flushed"); + } + else if (result is FlushCacheResponse.Error error) + { + throw new Exception($"An error occurred while attempting to flush cache 'test-cache': {error.ErrorCode}: {error}"); + } + } + + public static async Task Example_API_Set(CacheClient cacheClient) + { + var result = await cacheClient.SetAsync("test-cache", "test-key", "test-value"); + if (result is CacheSetResponse.Success) + { + Console.WriteLine("Key 'test-key' stored successfully"); + } + else if (result is CacheSetResponse.Error error) + { + throw new Exception($"An error occurred while attempting to store key 'test-key' in cache 'test-cache': {error.ErrorCode}: {error}"); + } + } + + public static async Task Example_API_Get(CacheClient cacheClient) + { + var result = await cacheClient.GetAsync("test-cache", "test-key"); + if (result is CacheGetResponse.Hit hit) + { + Console.WriteLine($"Retrieved value for key 'test-key': {hit.ValueString}"); + } + else if (result is CacheGetResponse.Miss) + { + Console.WriteLine("Key 'test-key' was not found in cache 'test-cache'"); + } + else if (result is CacheGetResponse.Error error) + { + throw new Exception($"An error occurred while attempting to get key 'test-key' from cache 'test-cache': {error.ErrorCode}: {error}"); + } + } + + public static async Task Example_API_Delete(CacheClient cacheClient) + { + var result = await cacheClient.DeleteAsync("test-cache", "test-key"); + if (result is CacheDeleteResponse.Success) + { + Console.WriteLine("Key 'test-key' deleted successfully"); + } + else if (result is CacheDeleteResponse.Error error) + { + throw new Exception($"An error occurred while attempting to delete key 'test-key' from cache 'test-cache': {error.ErrorCode}: {error}"); + } + } +} diff --git a/examples/DocExampleFiles/DocExampleFiles.csproj b/examples/DocExampleFiles/DocExampleFiles.csproj new file mode 100644 index 00000000..40c60dd4 --- /dev/null +++ b/examples/DocExampleFiles/DocExampleFiles.csproj @@ -0,0 +1,10 @@ + + + + Exe + net6.0 + enable + enable + + + diff --git a/examples/DocExampleFiles/Program.cs b/examples/DocExampleFiles/Program.cs new file mode 100644 index 00000000..83fa4f4d --- /dev/null +++ b/examples/DocExampleFiles/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/examples/DocExampleFiles/README.md b/examples/DocExampleFiles/README.md new file mode 100644 index 00000000..4bbd2636 --- /dev/null +++ b/examples/DocExampleFiles/README.md @@ -0,0 +1,2 @@ +This directory contains files that will be pulled in to the docs site for use in code examples. +Use caution when modifying any files in this directory! diff --git a/examples/MomentoExamples.sln b/examples/MomentoExamples.sln index d351fe5b..d9e2618f 100644 --- a/examples/MomentoExamples.sln +++ b/examples/MomentoExamples.sln @@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MomentoUsage", "MomentoUsag EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DictionaryExample", "DictionaryExample\DictionaryExample.csproj", "{61F79DA8-BA7F-4F6E-B4EA-29DACF9C7571}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocExampleApis", "DocExampleApis\DocExampleApis.csproj", "{A6EDEC93-D935-435D-A4AE-55C73CB0C7A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocExampleFiles", "DocExampleFiles\DocExampleFiles.csproj", "{89667600-2BBC-4104-B046-D2A46254DEBF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +47,14 @@ Global {ABDFBF7A-9639-4449-91E3-4558C366D297}.Debug|Any CPU.Build.0 = Debug|Any CPU {ABDFBF7A-9639-4449-91E3-4558C366D297}.Release|Any CPU.ActiveCfg = Release|Any CPU {ABDFBF7A-9639-4449-91E3-4558C366D297}.Release|Any CPU.Build.0 = Release|Any CPU + {A6EDEC93-D935-435D-A4AE-55C73CB0C7A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6EDEC93-D935-435D-A4AE-55C73CB0C7A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6EDEC93-D935-435D-A4AE-55C73CB0C7A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6EDEC93-D935-435D-A4AE-55C73CB0C7A1}.Release|Any CPU.Build.0 = Release|Any CPU + {89667600-2BBC-4104-B046-D2A46254DEBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89667600-2BBC-4104-B046-D2A46254DEBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89667600-2BBC-4104-B046-D2A46254DEBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89667600-2BBC-4104-B046-D2A46254DEBF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Momento.Sdk/CacheClient.cs b/src/Momento.Sdk/CacheClient.cs index 9f2d837e..8cd31238 100644 --- a/src/Momento.Sdk/CacheClient.cs +++ b/src/Momento.Sdk/CacheClient.cs @@ -727,6 +727,22 @@ public async Task DictionaryRemoveFieldsAsy return await this.DataClient.DictionaryRemoveFieldsAsync(cacheName, dictionaryName, fields); } + /// + public async Task DictionaryLengthAsync(string cacheName, string dictionaryName) + { + try + { + Utils.ArgumentNotNull(cacheName, nameof(cacheName)); + Utils.ArgumentNotNull(dictionaryName, nameof(dictionaryName)); + } + catch (ArgumentNullException e) + { + return new CacheDictionaryLengthResponse.Error(new InvalidArgumentException(e.Message)); + } + + return await this.DataClient.DictionaryLengthAsync(cacheName, dictionaryName); + } + /// public async Task SetAddElementAsync(string cacheName, string setName, byte[] element, CollectionTtl ttl = default(CollectionTtl)) { diff --git a/src/Momento.Sdk/ICacheClient.cs b/src/Momento.Sdk/ICacheClient.cs index 305a6610..9c6316fc 100644 --- a/src/Momento.Sdk/ICacheClient.cs +++ b/src/Momento.Sdk/ICacheClient.cs @@ -452,6 +452,15 @@ public interface ICacheClient : IDisposable /// public Task DictionaryRemoveFieldsAsync(string cacheName, string dictionaryName, IEnumerable fields); + /// + /// Calculate the length of a dictionary in the cache. + /// + /// + /// Name of the cache to perform the lookup in. + /// The dictionary to calculate length. + /// Task representing the length of the dictionary. + public Task DictionaryLengthAsync(string cacheName, string dictionaryName); + /// /// Add an element to a set in the cache. /// diff --git a/src/Momento.Sdk/Internal/DataGrpcManager.cs b/src/Momento.Sdk/Internal/DataGrpcManager.cs index faab8831..d410b327 100644 --- a/src/Momento.Sdk/Internal/DataGrpcManager.cs +++ b/src/Momento.Sdk/Internal/DataGrpcManager.cs @@ -34,6 +34,8 @@ public interface IDataClient public Task<_DictionaryGetResponse> DictionaryGetAsync(_DictionaryGetRequest request, CallOptions callOptions); public Task<_DictionaryFetchResponse> DictionaryFetchAsync(_DictionaryFetchRequest request, CallOptions callOptions); public Task<_DictionaryDeleteResponse> DictionaryDeleteAsync(_DictionaryDeleteRequest request, CallOptions callOptions); + public Task<_DictionaryLengthResponse> DictionaryLengthAsync(_DictionaryLengthRequest request, CallOptions callOptions); + public Task<_SetUnionResponse> SetUnionAsync(_SetUnionRequest request, CallOptions callOptions); public Task<_SetDifferenceResponse> SetDifferenceAsync(_SetDifferenceRequest request, CallOptions callOptions); public Task<_SetFetchResponse> SetFetchAsync(_SetFetchRequest request, CallOptions callOptions); @@ -141,6 +143,12 @@ public async Task<_DictionaryDeleteResponse> DictionaryDeleteAsync(_DictionaryDe return await wrapped.ResponseAsync; } + public async Task<_DictionaryLengthResponse> DictionaryLengthAsync(_DictionaryLengthRequest request, CallOptions callOptions) + { + var wrapped = await _middlewares.WrapRequest(request, callOptions, (r, o) => _generatedClient.DictionaryLengthAsync(r, o)); + return await wrapped.ResponseAsync; + } + public async Task<_SetUnionResponse> SetUnionAsync(_SetUnionRequest request, CallOptions callOptions) { var wrapped = await _middlewares.WrapRequest(request, callOptions, (r, o) => _generatedClient.SetUnionAsync(r, o)); diff --git a/src/Momento.Sdk/Internal/Retry/DefaultRetryEligibilityStrategy.cs b/src/Momento.Sdk/Internal/Retry/DefaultRetryEligibilityStrategy.cs index d53af0bd..59ae807f 100644 --- a/src/Momento.Sdk/Internal/Retry/DefaultRetryEligibilityStrategy.cs +++ b/src/Momento.Sdk/Internal/Retry/DefaultRetryEligibilityStrategy.cs @@ -47,6 +47,7 @@ public class DefaultRetryEligibilityStrategy : IRetryEligibilityStrategy typeof(_DictionaryGetRequest), typeof(_DictionaryFetchRequest), typeof(_DictionaryDeleteRequest), + typeof(_DictionaryLengthRequest), typeof(_SetUnionRequest), typeof(_SetDifferenceRequest), typeof(_SetFetchRequest), diff --git a/src/Momento.Sdk/Internal/ScsDataClient.cs b/src/Momento.Sdk/Internal/ScsDataClient.cs index e97027eb..d691cf60 100644 --- a/src/Momento.Sdk/Internal/ScsDataClient.cs +++ b/src/Momento.Sdk/Internal/ScsDataClient.cs @@ -258,6 +258,11 @@ public async Task DictionaryRemoveFieldsAsy return await SendDictionaryRemoveFieldsAsync(cacheName, dictionaryName, fields.ToEnumerableByteString()); } + public async Task DictionaryLengthAsync(string cacheName, string dictionaryName) + { + return await SendDictionaryLengthAsync(cacheName, dictionaryName); + } + public async Task SetAddElementAsync(string cacheName, string setName, byte[] element, CollectionTtl ttl = default(CollectionTtl)) { return await SendSetAddElementAsync(cacheName, setName, element.ToSingletonByteString(), ttl); @@ -783,6 +788,31 @@ private async Task SendDictionaryRemoveFiel return this._logger.LogTraceCollectionRequestSuccess(REQUEST_TYPE_DICTIONARY_REMOVE_FIELDS, cacheName, dictionaryName, fields, null, new CacheDictionaryRemoveFieldsResponse.Success()); } + const string REQUEST_TYPE_DICTIONARY_LENGTH = "DICTIONARY_LENGTH"; + private async Task SendDictionaryLengthAsync(string cacheName, string dictionaryName) + { + _DictionaryLengthRequest request = new() { DictionaryName = dictionaryName.ToByteString() }; + _DictionaryLengthResponse response; + var metadata = MetadataWithCache(cacheName); + + try + { + this._logger.LogTraceExecutingCollectionRequest(REQUEST_TYPE_DICTIONARY_LENGTH, cacheName, dictionaryName); + response = await this.grpcManager.Client.DictionaryLengthAsync(request, new CallOptions(headers: MetadataWithCache(cacheName), deadline: CalculateDeadline())); + } + catch (Exception e) + { + return this._logger.LogTraceCollectionRequestError(REQUEST_TYPE_DICTIONARY_LENGTH, cacheName, dictionaryName, new CacheDictionaryLengthResponse.Error(_exceptionMapper.Convert(e, metadata))); + } + + if (response.DictionaryCase == _DictionaryLengthResponse.DictionaryOneofCase.Missing) + { + return this._logger.LogTraceCollectionRequestSuccess(REQUEST_TYPE_DICTIONARY_LENGTH, cacheName, dictionaryName, new CacheDictionaryLengthResponse.Miss()); + } + + return this._logger.LogTraceCollectionRequestSuccess(REQUEST_TYPE_DICTIONARY_LENGTH, cacheName, dictionaryName, new CacheDictionaryLengthResponse.Hit(response)); + } + const string REQUEST_TYPE_SET_ADD_ELEMENT = "SET_ADD_ELEMENT"; private async Task SendSetAddElementAsync(string cacheName, string setName, IEnumerable elements, CollectionTtl ttl) { @@ -1139,7 +1169,7 @@ private async Task SendListRetainAsync(string cacheName return this._logger.LogTraceCollectionRequestError(REQUEST_TYPE_LIST_RETAIN, cacheName, listName, new CacheListRetainResponse.Error(_exceptionMapper.Convert(e, metadata))); } - return this._logger.LogTraceCollectionRequestSuccess(REQUEST_TYPE_LIST_RETAIN, cacheName, listName, new CacheListRetainResponse.Success()); + return this._logger.LogTraceCollectionRequestSuccess(REQUEST_TYPE_LIST_RETAIN, cacheName, listName, new CacheListRetainResponse.Success(response)); } const string REQUEST_TYPE_LIST_REMOVE_VALUE = "LIST_REMOVE_VALUE"; @@ -1150,19 +1180,20 @@ private async Task SendListRemoveValueAsync(string ListName = listName.ToByteString(), AllElementsWithValue = value }; + _ListRemoveResponse response; var metadata = MetadataWithCache(cacheName); try { this._logger.LogTraceExecutingCollectionRequest(REQUEST_TYPE_LIST_REMOVE_VALUE, cacheName, listName, value, null); - await this.grpcManager.Client.ListRemoveAsync(request, new CallOptions(headers: MetadataWithCache(cacheName), deadline: CalculateDeadline())); + response = await this.grpcManager.Client.ListRemoveAsync(request, new CallOptions(headers: MetadataWithCache(cacheName), deadline: CalculateDeadline())); } catch (Exception e) { return this._logger.LogTraceCollectionRequestError(REQUEST_TYPE_LIST_REMOVE_VALUE, cacheName, listName, value, null, new CacheListRemoveValueResponse.Error(_exceptionMapper.Convert(e, metadata))); } - return this._logger.LogTraceCollectionRequestSuccess(REQUEST_TYPE_LIST_REMOVE_VALUE, cacheName, listName, value, null, new CacheListRemoveValueResponse.Success()); + return this._logger.LogTraceCollectionRequestSuccess(REQUEST_TYPE_LIST_REMOVE_VALUE, cacheName, listName, value, null, new CacheListRemoveValueResponse.Success(response)); } const string REQUEST_TYPE_LIST_LENGTH = "LIST_LENGTH"; diff --git a/src/Momento.Sdk/Momento.Sdk.csproj b/src/Momento.Sdk/Momento.Sdk.csproj index f15d9499..28e4ca1b 100644 --- a/src/Momento.Sdk/Momento.Sdk.csproj +++ b/src/Momento.Sdk/Momento.Sdk.csproj @@ -54,7 +54,7 @@ - + diff --git a/src/Momento.Sdk/Responses/CacheDictionaryLengthResponse.cs b/src/Momento.Sdk/Responses/CacheDictionaryLengthResponse.cs new file mode 100644 index 00000000..1673a8ce --- /dev/null +++ b/src/Momento.Sdk/Responses/CacheDictionaryLengthResponse.cs @@ -0,0 +1,105 @@ +using Momento.Protos.CacheClient; +using Momento.Sdk.Exceptions; + +namespace Momento.Sdk.Responses; + +/// +/// Parent response type for a cache dictionary length request. The +/// response object is resolved to a type-safe object of one of +/// the following subtypes: +/// +/// CacheDictionaryLengthResponse.Hit +/// CacheDictionaryLengthResponse.Miss +/// CacheDictionaryLengthResponse.Error +/// +/// Pattern matching can be used to operate on the appropriate subtype. +/// For example: +/// +/// if (response is CacheDictionaryLengthResponse.Hit hitResponse) +/// { +/// return hitResponse.Length; +/// } +/// else if (response is CacheDictionaryLengthResponse.Miss missResponse) +/// { +/// // handle missResponse as appropriate +/// } +/// else if (response is CacheDictionaryLengthResponse.Error errorResponse) +/// { +/// // handle error as appropriate +/// } +/// else +/// { +/// // handle unexpected response +/// } +/// +/// +public abstract class CacheDictionaryLengthResponse +{ + /// + public class Hit : CacheDictionaryLengthResponse + { + /// + /// The length of the dictionary. If the dictionary is missing or empty, the result is zero. + /// + public int Length { get; private set; } = 0; + + /// + /// + /// + /// The cache response. + public Hit(_DictionaryLengthResponse response) + { + if(response.DictionaryCase == _DictionaryLengthResponse.DictionaryOneofCase.Found) { + Length = checked((int)response.Found.Length); + } + } + + /// + public override string ToString() + { + return $"{base.ToString()}: Length: {Length}"; + } + } + + /// + public class Miss : CacheDictionaryLengthResponse + { + + } + + /// + public class Error : CacheDictionaryLengthResponse, IError + { + private readonly SdkException _error; + + /// + public Error(SdkException error) + { + _error = error; + } + + /// + public SdkException InnerException + { + get => _error; + } + + /// + public MomentoErrorCode ErrorCode + { + get => _error.ErrorCode; + } + + /// + public string Message + { + get => $"{_error.MessageWrapper}: {_error.Message}"; + } + + /// + public override string ToString() + { + return $"{base.ToString()}: {this.Message}"; + } + } +} diff --git a/src/Momento.Sdk/Responses/CacheListPopBackResponse.cs b/src/Momento.Sdk/Responses/CacheListPopBackResponse.cs index 8f385aa7..f5baae13 100644 --- a/src/Momento.Sdk/Responses/CacheListPopBackResponse.cs +++ b/src/Momento.Sdk/Responses/CacheListPopBackResponse.cs @@ -44,6 +44,11 @@ public class Hit : CacheListPopBackResponse protected readonly ByteString value; #pragma warning restore 1591 + /// + /// The length of the list post-pop (and post-truncate, if that applies). + /// + public int ListLength { get; private set; } + /// /// /// @@ -51,6 +56,7 @@ public class Hit : CacheListPopBackResponse public Hit(_ListPopBackResponse response) { this.value = response.Found.Back; + ListLength = checked((int)response.Found.ListLength); } /// @@ -69,7 +75,7 @@ public byte[] ValueByteArray /// public override string ToString() { - return $"{base.ToString()}: ValueString: \"{ValueString.Truncate()}\" ValueByteArray: \"{ValueByteArray.ToPrettyHexString().Truncate()}\""; + return $"{base.ToString()}: ValueString: \"{ValueString.Truncate()}\" ValueByteArray: \"{ValueByteArray.ToPrettyHexString().Truncate()}\" ListLength: {ListLength}"; } } diff --git a/src/Momento.Sdk/Responses/CacheListPopFrontResponse.cs b/src/Momento.Sdk/Responses/CacheListPopFrontResponse.cs index ec74aefd..a56a304e 100644 --- a/src/Momento.Sdk/Responses/CacheListPopFrontResponse.cs +++ b/src/Momento.Sdk/Responses/CacheListPopFrontResponse.cs @@ -44,6 +44,11 @@ public class Hit : CacheListPopFrontResponse protected readonly ByteString value; #pragma warning restore 1591 + /// + /// The length of the list post-pop (and post-truncate, if that applies). + /// + public int ListLength { get; private set; } + /// /// /// @@ -51,6 +56,7 @@ public class Hit : CacheListPopFrontResponse public Hit(_ListPopFrontResponse response) { this.value = response.Found.Front; + ListLength = checked((int)response.Found.ListLength); } /// @@ -69,7 +75,7 @@ public byte[] ValueByteArray /// public override string ToString() { - return $"{base.ToString()}: ValueString: \"{ValueString.Truncate()}\" ValueByteArray: \"{ValueByteArray.ToPrettyHexString().Truncate()}\""; + return $"{base.ToString()}: ValueString: \"{ValueString.Truncate()}\" ValueByteArray: \"{ValueByteArray.ToPrettyHexString().Truncate()}\" ListLength: {ListLength}"; } } diff --git a/src/Momento.Sdk/Responses/CacheListRemoveValueResponse.cs b/src/Momento.Sdk/Responses/CacheListRemoveValueResponse.cs index 0165993c..9ca52b94 100644 --- a/src/Momento.Sdk/Responses/CacheListRemoveValueResponse.cs +++ b/src/Momento.Sdk/Responses/CacheListRemoveValueResponse.cs @@ -1,4 +1,5 @@ -using Momento.Sdk.Exceptions; +using Momento.Protos.CacheClient; +using Momento.Sdk.Exceptions; namespace Momento.Sdk.Responses; @@ -32,6 +33,27 @@ public abstract class CacheListRemoveValueResponse /// public class Success : CacheListRemoveValueResponse { + /// + /// The length of the list post-remove operation. + /// + public int ListLength { get; private set; } + + /// + /// + /// + /// The cache response + public Success(_ListRemoveResponse response) + { + if (response.ListCase == _ListRemoveResponse.ListOneofCase.Found) { + ListLength = checked((int)response.Found.ListLength); + } + } + + /// + public override string ToString() + { + return $"{base.ToString()}: ListLength: {ListLength}"; + } } /// diff --git a/src/Momento.Sdk/Responses/CacheListRetainResponse.cs b/src/Momento.Sdk/Responses/CacheListRetainResponse.cs index f6b23b3b..2c59dd91 100644 --- a/src/Momento.Sdk/Responses/CacheListRetainResponse.cs +++ b/src/Momento.Sdk/Responses/CacheListRetainResponse.cs @@ -1,4 +1,5 @@ -using Momento.Sdk.Exceptions; +using Momento.Protos.CacheClient; +using Momento.Sdk.Exceptions; namespace Momento.Sdk.Responses; @@ -32,6 +33,25 @@ public abstract class CacheListRetainResponse /// public class Success : CacheListRetainResponse { + /// + /// The length of the list post-retain operation. + /// + public int ListLength { get; private set; } + + /// + /// + /// + /// The cache response + public Success(_ListRetainResponse response) + { + ListLength = checked((int)response.Found.ListLength); + } + + /// + public override string ToString() + { + return $"{base.ToString()}: ListLength: {ListLength}"; + } } /// diff --git a/tests/Integration/Momento.Sdk.Tests/DictionaryTest.cs b/tests/Integration/Momento.Sdk.Tests/DictionaryTest.cs index 9122712b..4a7bce2e 100644 --- a/tests/Integration/Momento.Sdk.Tests/DictionaryTest.cs +++ b/tests/Integration/Momento.Sdk.Tests/DictionaryTest.cs @@ -1118,4 +1118,27 @@ public async Task DictionaryRemoveFieldsAsync_FieldsAreString_HappyPath() Assert.True((await client.DictionaryGetFieldAsync(cacheName, dictionaryName, fields[1])) is CacheDictionaryGetFieldResponse.Miss); Assert.True((await client.DictionaryGetFieldAsync(cacheName, dictionaryName, otherField)) is CacheDictionaryGetFieldResponse.Hit); } + + [Fact] + public async Task DictionaryLengthAsync_DictionaryIsMissing() + { + var dictionaryName = Utils.NewGuidString(); + CacheDictionaryLengthResponse response = await client.DictionaryLengthAsync(cacheName, dictionaryName); + Assert.True(response is CacheDictionaryLengthResponse.Miss, $"Unexpected response: {response}"); + } + + [Fact] + public async Task DictionaryLengthAsync_HappyPath() + { + var dictionaryName = Utils.NewGuidString(); + var field = Utils.NewGuidByteArray(); + var value = Utils.NewGuidByteArray(); + + await client.DictionarySetFieldAsync(cacheName, dictionaryName, field, value); + + CacheDictionaryLengthResponse lengthResponse = await client.DictionaryLengthAsync(cacheName, dictionaryName); + Assert.True(lengthResponse is CacheDictionaryLengthResponse.Hit, $"Unexpected response: {lengthResponse}"); + var hitResponse = (CacheDictionaryLengthResponse.Hit)lengthResponse; + Assert.Equal(1, hitResponse.Length); + } } diff --git a/tests/Integration/Momento.Sdk.Tests/ListTest.cs b/tests/Integration/Momento.Sdk.Tests/ListTest.cs index f01a2ccc..8f010921 100644 --- a/tests/Integration/Momento.Sdk.Tests/ListTest.cs +++ b/tests/Integration/Momento.Sdk.Tests/ListTest.cs @@ -279,6 +279,32 @@ public async Task ListConcatenateFrontFetch_ValueIsByteArray_HappyPath() Assert.Contains(value, list); } } + + [Fact] + public async Task CacheListRetainReponse_ToString_HappyPath() + { + var listName = Utils.NewGuidString(); + string value1 = Utils.NewGuidString(); + string value2 = Utils.NewGuidString(); + string value3 = Utils.NewGuidString(); + string value4 = Utils.NewGuidString(); + string value5 = Utils.NewGuidString(); + string value6 = Utils.NewGuidString(); + string[] values = new string[] { value1, value2, value3, value4, value5, value6 }; + + var resetList = async () => + { + await client.DeleteAsync(cacheName, listName); + await client.ListConcatenateFrontAsync(cacheName, listName, values); + }; + + await resetList(); + CacheListRetainResponse response = await client.ListRetainAsync(cacheName, listName, 1, 4); + Assert.True(response is CacheListRetainResponse.Success, $"Unexpected response: {response}"); + var successResponse = (CacheListRetainResponse.Success)response; + Assert.Equal("Momento.Sdk.Responses.CacheListRetainResponse+Success: ListLength: 3", successResponse.ToString()); + } + [Fact] public async Task ListConcatenateFrontFetch_ValueIsByteArray_NoRefreshTtl() { @@ -1026,6 +1052,7 @@ public async Task ListPopFrontAsync_ValueIsByteArray_HappyPath() var hitResponse = (CacheListPopFrontResponse.Hit)response; Assert.Equal(value2, hitResponse.ValueByteArray); + Assert.Equal(1, hitResponse.ListLength); } [Fact] @@ -1042,6 +1069,7 @@ public async Task ListPopFrontAsync_ValueIsString_HappyPath() var hitResponse = (CacheListPopFrontResponse.Hit)response; Assert.Equal(value2, hitResponse.ValueString); + Assert.Equal(1, hitResponse.ListLength); } [Fact] @@ -1054,7 +1082,7 @@ public async Task CacheListPopFrontReponse_ToString_HappyPath() CacheListPopFrontResponse response = await client.ListPopFrontAsync(cacheName, listName); Assert.True(response is CacheListPopFrontResponse.Hit, $"Unexpected response: {response}"); var hitResponse = (CacheListPopFrontResponse.Hit)response; - Assert.Equal("Momento.Sdk.Responses.CacheListPopFrontResponse+Hit: ValueString: \"a\" ValueByteArray: \"61\"", hitResponse.ToString()); + Assert.Equal("Momento.Sdk.Responses.CacheListPopFrontResponse+Hit: ValueString: \"a\" ValueByteArray: \"61\" ListLength: 0", hitResponse.ToString()); } [Theory] @@ -1089,6 +1117,7 @@ public async Task ListPopBackAsync_ValueIsByteArray_HappyPath() var hitResponse = (CacheListPopBackResponse.Hit)response; Assert.Equal(value2, hitResponse.ValueByteArray); + Assert.Equal(1, hitResponse.ListLength); } [Fact] @@ -1105,6 +1134,7 @@ public async Task ListPopBackAsync_ValueIsString_HappyPath() var hitResponse = (CacheListPopBackResponse.Hit)response; Assert.Equal(value2, hitResponse.ValueString); + Assert.Equal(1, hitResponse.ListLength); } [Fact] @@ -1117,7 +1147,7 @@ public async Task CacheListPopBackReponse_ToString_HappyPath() CacheListPopBackResponse response = await client.ListPopBackAsync(cacheName, listName); Assert.True(response is CacheListPopBackResponse.Hit, $"Unexpected response: {response}"); var hitResponse = (CacheListPopBackResponse.Hit)response; - Assert.Equal("Momento.Sdk.Responses.CacheListPopBackResponse+Hit: ValueString: \"a\" ValueByteArray: \"61\"", hitResponse.ToString()); + Assert.Equal("Momento.Sdk.Responses.CacheListPopBackResponse+Hit: ValueString: \"a\" ValueByteArray: \"61\" ListLength: 0", hitResponse.ToString()); } [Theory] @@ -1173,7 +1203,6 @@ public async Task ListFetchAsync_HasContentByteArray_HappyPath() Assert.Contains(field1, hitResponse.ValueListByteArray!); Assert.Contains(field2, hitResponse.ValueListByteArray!); - Assert.Equal(2, hitResponse.ValueListByteArray!.Count); } [Fact] @@ -1217,7 +1246,10 @@ public async Task ListRemoveValueAsync_ValueIsByteArray_HappyPath() await client.ListPushBackAsync(cacheName, listName, valueOfInterest); // Remove value of interest - await client.ListRemoveValueAsync(cacheName, listName, valueOfInterest); + var removeResponse = await client.ListRemoveValueAsync(cacheName, listName, valueOfInterest); + Assert.True(removeResponse is CacheListRemoveValueResponse.Success, $"Unexpected response: {removeResponse}"); + var successRemoveResponse = (CacheListRemoveValueResponse.Success)removeResponse; + Assert.Equal(3, successRemoveResponse.ListLength); // Test not there var response = await client.ListFetchAsync(cacheName, listName); @@ -1237,7 +1269,10 @@ public async Task ListRemoveValueAsync_ValueIsByteArray_ValueNotPresentNoop() await client.ListPushBackAsync(cacheName, listName, value); } - await client.ListRemoveValueAsync(cacheName, listName, Utils.NewGuidByteArray()); + var removeResponse = await client.ListRemoveValueAsync(cacheName, listName, Utils.NewGuidByteArray()); + Assert.True(removeResponse is CacheListRemoveValueResponse.Success, $"Unexpected response: {removeResponse}"); + var successRemoveResponse = (CacheListRemoveValueResponse.Success)removeResponse; + Assert.Equal(3, successRemoveResponse.ListLength); var response = await client.ListFetchAsync(cacheName, listName); Assert.True(response is CacheListFetchResponse.Hit, $"Unexpected response: {response}"); @@ -1282,7 +1317,10 @@ public async Task ListRemoveValueAsync_ValueIsString_HappyPath() await client.ListPushBackAsync(cacheName, listName, valueOfInterest); // Remove value of interest - await client.ListRemoveValueAsync(cacheName, listName, valueOfInterest); + var removeResponse = await client.ListRemoveValueAsync(cacheName, listName, valueOfInterest); + Assert.True(removeResponse is CacheListRemoveValueResponse.Success, $"Unexpected response: {removeResponse}"); + var successRemoveResponse = (CacheListRemoveValueResponse.Success)removeResponse; + Assert.Equal(3, successRemoveResponse.ListLength); // Test not there var response = await client.ListFetchAsync(cacheName, listName); @@ -1302,7 +1340,10 @@ public async Task ListRemoveValueAsync_ValueIsByteString_ValueNotPresentNoop() await client.ListPushBackAsync(cacheName, listName, value); } - await client.ListRemoveValueAsync(cacheName, listName, Utils.NewGuidString()); + var removeResponse = await client.ListRemoveValueAsync(cacheName, listName, Utils.NewGuidString()); + Assert.True(removeResponse is CacheListRemoveValueResponse.Success, $"Unexpected response: {removeResponse}"); + var successRemoveResponse = (CacheListRemoveValueResponse.Success)removeResponse; + Assert.Equal(3, successRemoveResponse.ListLength); var response = await client.ListFetchAsync(cacheName, listName); Assert.True(response is CacheListFetchResponse.Hit, $"Unexpected response: {response}"); @@ -1319,6 +1360,23 @@ public async Task ListRemoveValueAsync_ValueIsString_ListNotThereNoop() Assert.True(await client.ListFetchAsync(cacheName, listName) is CacheListFetchResponse.Miss); } + [Fact] + public async Task CacheListRemoveValueReponse_ToString_HappyPath() + { + var listName = Utils.NewGuidString(); + var list = new List() { Utils.NewGuidString(), Utils.NewGuidString(), Utils.NewGuidString() }; + + foreach (var value in list) + { + await client.ListPushBackAsync(cacheName, listName, value); + } + + var removeResponse = await client.ListRemoveValueAsync(cacheName, listName, Utils.NewGuidString()); + Assert.True(removeResponse is CacheListRemoveValueResponse.Success, $"Unexpected response: {removeResponse}"); + var successResponse = (CacheListRemoveValueResponse.Success)removeResponse; + Assert.Equal("Momento.Sdk.Responses.CacheListRemoveValueResponse+Success: ListLength: 3", successResponse.ToString()); + } + [Theory] [InlineData(null, "my-list")] [InlineData("cache", null)]