Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spike: demo the storage api in C# #554

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions examples/StoreApi/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Momento.Sdk.StoreClient;

namespace Driver;

public class Program
{
public static async Task Main(string[] args)
{
var client = new StoreClient();
await client.CreateStoreAsync("my-store");

// Can use implicit cast to create StoreValue.
await client.PutAsync("my-store", "my-key", "my-value");
await client.PutAsync("my-store", "my-key", 42);
await client.PutAsync("my-store", "my-key", 3.14);
await client.PutAsync("my-store", "my-key", true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kewl


// The compiler will detect invalid casts.
// await client.PutAsync("my-store", "my-key", new string[] { "a", "b" });

// This demos usage where the developer is sure of the type.
{
var response = await client.GetAsync("my-store", "string-value");
if (response is StoreGet.Success success)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@malandis malandis Apr 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It rings a bell. I think C# 9 wasn't out at the time when I saw this or it was a preview. When thinking about this I consider the lowest common denominator.

{
// This uses an implicit cast from StoreValue to string.
string value = success.Value;
Console.WriteLine($"Success: {value}");
}
else if (response is StoreGet.Error error)
{
Console.WriteLine($"Error: {error.Message}");
}
}

// A more cautious developer would handle the unfortunate case where the cast fails.
{
var response = await client.GetAsync("my-store", "bool-value");
if (response is StoreGet.Success success)
{
try
{
// This uses an implicit cast from StoreValue to long, but it's actually a bool.
long value = success.Value;
Console.WriteLine($"Success: {value}");
}
catch (InvalidCastException)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one makes me want to raise the same question I raised on your JS PR about whether we should be modeling Success/Error as algebraic types, or just return the value directly and throw an exception for errors. In this case, that would simplify the code down to a try/catch block with multiple catch clauses, as opposed to an if+try.

Probably it'd be too inconsistent to make that dramatic of a change for this Client given that it will be living in the same SDK, but I'm just trying to think through what I would advocate for here if it was a greenfield product.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: the JS API. I like the shorthand but there's forward compatibility concerns.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what sense? Are you talking about the case where the server changes in the future to support additional response types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind -- I was confusing the inner value type with the Success object. You're saying we can return the Success object or bust. We are free to add more data to Success if we do that.

{
Console.WriteLine($"Error: Invalid cast to long. Type was actually {success.Value.Type}");
}
}
else if (response is StoreGet.Error error)
{
Console.WriteLine($"Error: {error.Message}");
}
}

// This demos usage where the developer wants to do something specific based on the type.
{
foreach (var key in new string[] { "string-value", "int-value", "double-value", "bool-value", "unknown-value" })
{
var response = await client.GetAsync("my-store", key);
if (response is StoreGet.Success success)
{
var message = success.Value.Type switch
{
StoreValueType.String => "string",
StoreValueType.Integer64 => "integer",
StoreValueType.Double => "double",
StoreValueType.Bool => "boolean",
_ => "unknown type :("
};
Console.WriteLine($"Success: {key} is a {message}.");
}
else if (response is StoreGet.Error error)
{
Console.WriteLine($"Error when getting key {key}: {error.Message}");
}
}
}

await client.DeleteStoreAsync("my-store");
}
}

10 changes: 10 additions & 0 deletions examples/StoreApi/StoreApi.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
25 changes: 25 additions & 0 deletions examples/StoreApi/StoreApi.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StoreApi", "StoreApi.csproj", "{BAF8AA3E-54B8-483D-8A86-F4F9E8639ED5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BAF8AA3E-54B8-483D-8A86-F4F9E8639ED5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BAF8AA3E-54B8-483D-8A86-F4F9E8639ED5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BAF8AA3E-54B8-483D-8A86-F4F9E8639ED5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BAF8AA3E-54B8-483D-8A86-F4F9E8639ED5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D39D433C-744B-4390-AF23-E642AFC9E1A7}
EndGlobalSection
EndGlobal
59 changes: 59 additions & 0 deletions examples/StoreApi/StoreClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace Momento.Sdk.StoreClient;

public class StoreClient
{
public StoreClient()
{
}

public async Task CreateStoreAsync(string storeName)
{
// These are so the compiler doesn't complain no awaits.
await Task.Delay(0);
}

public async Task DeleteStoreAsync(string storeName)
{
await Task.Delay(0);
}

public async Task ListStoresAsync()
{
await Task.Delay(0);
}

public async Task<StoreGetResponse> GetAsync(string storeName, string key)
{
await Task.Delay(0);
if (key == "string-value")
{
return new StoreGet.Success("string-value");
}
else if (key == "int-value")
{
return new StoreGet.Success(42);
}
else if (key == "double-value")
{
return new StoreGet.Success(3.14);
}
else if (key == "bool-value")
{
return new StoreGet.Success(true);
}
else
{
return new StoreGet.Error("Key not found.");
}
}

public async Task PutAsync(string storeName, string key, StoreValue value)
{
await Task.Delay(0);
}

public async Task DeleteAsync(string storeName, string key)
{
await Task.Delay(0);
}
}
28 changes: 28 additions & 0 deletions examples/StoreApi/StoreGet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Momento.Sdk.StoreClient;

public abstract class StoreGetResponse
{
}

public abstract class StoreGet
{
public class Success : StoreGetResponse
{
public StoreValue Value { get; }

public Success(StoreValue value)
{
Value = value;
}
}

public class Error : StoreGetResponse
{
public string Message { get; }

public Error(string message)
{
Message = message;
}
}
}
49 changes: 49 additions & 0 deletions examples/StoreApi/StoreValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace Momento.Sdk.StoreClient;

public enum StoreValueType
{
String,
Integer64,
Double,
Bool
}

public class StoreValue
{
private object _value;
public StoreValueType Type { get; }

public StoreValue(object value, StoreValueType type)
{
_value = value;
Type = type;
}

public StoreValue(string value) : this(value, StoreValueType.String)
{
}

public StoreValue(long value) : this(value, StoreValueType.Integer64)
{
}

public StoreValue(double value) : this(value, StoreValueType.Double)
{
}

public StoreValue(bool value) : this(value, StoreValueType.Bool)
{
}

// implicit cast from the various types into StoreValue
public static implicit operator StoreValue(string value) => new StoreValue(value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theoretically the right-hand-side of these could be a code block, correct? Allowing us to put in some logging or throw a different error type if we decide to, etc.?

Not saying we should change it from what you have here, just wanted to understand our options.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The right-hand side here is just a shorthand when the function body is a single statement. We can have a multi-statement block instead.

public static implicit operator StoreValue(long value) => new StoreValue(value);
public static implicit operator StoreValue(double value) => new StoreValue(value);
public static implicit operator StoreValue(bool value) => new StoreValue(value);

// implicit cast from StoreValue into the various types
public static implicit operator string(StoreValue value) => (string)value._value;
public static implicit operator long(StoreValue value) => (long)value._value;
public static implicit operator double(StoreValue value) => (double)value._value;
public static implicit operator bool(StoreValue value) => (bool)value._value;
}
Loading