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

C#: [WIP] CSharp Client #3354

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft

C#: [WIP] CSharp Client #3354

wants to merge 30 commits into from

Conversation

X39
Copy link

@X39 X39 commented Mar 10, 2025

About

This PR introduces a substantial refactoring of the C# client, reshaping its structure and approach to better align with long-term maintainability and usability. Key changes include:

  • A clearer folder structure, introducing a dedicated rust folder rather than mixing Rust and C# within a single lib directory.
  • Adoption of Aspire for testing (leveraging Docker for test environments).
  • A foundational "base client" targeting .NET Standard.
  • Introduction of specialized value structures for output values, improving support for non-string scenarios.
  • A new connection-string format.
  • A dedicated Hosting library with Aspire support.
  • Various other improvements aimed at streamlining development and integration.

Context

I want to acknowledge that this PR is quite far-reaching and significantly alters the existing implementation. It initially started as a small proof of concept intended for a Reddit post but quickly evolved into a broader redesign. Given its scope, I’d like to open the discussion for feedback—on both technical direction and overall approach—to ensure alignment with the goals of the C# contributors.

Remaining Work

  • Validate that the roadmap outlined in the ReadMe.MD aligns with contributor objectives.
  • Complete the remaining command implementations.
  • Ensure the Rust setup within .csproj functions correctly across all platforms.
  • Adjust Rust setup in .csproj to use appropriate folder structures and determine whether Cargo should generate multiple binaries or GitHub Actions should manage multiple glide_rs library targets.
  • Finalize the Rust output Value:
    • Implement Attribute mapping.
    • Implement BigNumber mapping.
    • Implement Push mapping.
    • Optimize from_redis to allocate a single memory block for efficient FFI data transfer.
  • Transition logging from glide_rs to .NET by integrating a logging callback, allowing .NET-side configuration.
  • Modify Glide ABI to support OpenTelemetry spans, ensuring seamless integration with frameworks like ASP.NET Core.
  • Improve Rust error messages for better debugging and usability.
  • Implement a Rust input Parameter structure.
  • Conduct performance profiling and optimizations.

Key Considerations for Merge

  • Ensure integration with the existing infrastructure:
    • Adapt the benchmarking setup to work with the benchmarks section at root.
    • Update the csharp.yml GitHub Action workflow to accommodate Aspire-based testing, which may require adjustments to the test fixture.

X39 added 22 commits March 5, 2025 21:41
Added the implementation of CSharp Native Client in conjunction with the Rust library.

New implementation is, as of now, capable to handle client creation and command invocation.
Also introduced logging at various levels to help with debugging.
As of now, a few still remain unimplemented. Those are, albeit required, not implemented because i do not know what theese actually express, making FFI implementation complicated. So i need to educate myself on theese first
Introduces a new blocking `SendCommand` method in both the C# and Rust layers, with associated tests and FFI wiring.
# Conflicts:
#	csharp/lib/BaseClient.cs
#	csharp/lib/Internals/Message.cs
#	csharp/lib/glide.csproj
#	csharp/rust/src/lib.rs
#	csharp/tests/Integration/TestConfiguration.cs
#	csharp/tests/tests.csproj
Moved all files of `Valkey.Glide.AppHost` from the root to the `sources` folder for better project organization. Updated references in the solution and unit test project to reflect the new path structure. No functional changes were made to the application.
Refactored connection interface to utilize the new `ConnectionRequest` model, introducing enhanced configurability for TLS, periodic checks, retries, and more. Added multiple structs and enums under `InterOp` namespace for detailed request handling.
Introduced `ConnectionConfigBuilder` and its associated extensions to simplify and enhance the process of configuring `ConnectionRequest` instances.
Introduced the `Valkey.Glide.Hosting` library for managing connections using a connection string. This includes parsing logic, service collection extensions for DI, and comprehensive unit tests to ensure implementation correctness. Updated documentation and project files to integrate the new library.
Implemented functionality for executing GET and SET commands with extension methods in the Glide client. Added unit tests to ensure correct behavior for both existing and non-existent keys, as well as simple set operations.
Introduced AspireSample.AppHost, AspireSample.Worker, and AspireSample.ServiceDefaults projects to demonstrate distributed application patterns. These include configurations for OpenTelemetry, resilience, health checks, and service discovery. Updated solution file to include the newly added projects.
@X39 X39 requested a review from a team as a code owner March 10, 2025 23:02
@X39 X39 changed the title [WIP] CSharp Client C#: [WIP] CSharp Client Mar 10, 2025
@Yury-Fridlyand Yury-Fridlyand requested a review from Sa1Gur March 11, 2025 00:45
@Yury-Fridlyand Yury-Fridlyand added the C# C# wrapper label Mar 11, 2025
This updates the Glide library with a comprehensive suite of Redis set commands, supporting various combinations like conditional and expiration-based operations. It also introduces specialized exceptions (e.g., `GlideSetCommandFailedException`) for improved error handling and debugging. Unit testing dependencies and helper methods are updated to reflect these enhancements. A todo was added to add tests for every call against a real valkey instance.
@nihohit
Copy link
Contributor

nihohit commented Mar 11, 2025

(I'm not a maintainer)
Hi, can you please post before/after benchmarks, to show how this rewrite affects perf?


# ReSharper properties
resharper_align_multiline_binary_patterns = true
resharper_align_multiline_calls_chain = true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you elaborate what is the goal of this change? I mean moving to ReSharper properties. Also consider having separate PR for this, please

Copy link
Author

Choose a reason for hiding this comment

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

As mentioned, this started as a small PoC, and these are essentially my configuration files. I haven’t changed them back yet. I can do so if needed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

As an Open Source project, we should not take dependencies on proprietary tools like resharper. We should assume that many of our developers will not have licenses for this tool. Additionally, there are many CSharp and DotNet settings which we should not be eliminating without a deeper discussion.

Copy link
Author

@X39 X39 Mar 11, 2025

Choose a reason for hiding this comment

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

Rider is no longer paid but just as free as visual studio community (for FOSS, license differences still exist)


# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUnit
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you elaborate on this changes also? Also consider having separate PR for this, please

Copy link
Author

Choose a reason for hiding this comment

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

As mentioned, this started as a small PoC. I achieved this by completely deleting the old folder and replacing everything, as seen in fb1afe5.

The .gitignore is a merge of these default ones:

Let me know if any adjustments are needed or if i should roll back :)


<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="9.1.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
Copy link
Collaborator

Choose a reason for hiding this comment

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

Using BenchmarkDotNet is nice touch. Do you see how to make it in line with other benchmark testing for different packages? I would say it is better to keep them in line

Copy link
Author

Choose a reason for hiding this comment

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

There’s definitely a way to make it align. However, I haven’t been able to run the full repository benchmarks yet. That said, I also haven’t spent much time on that part so far.


public async Task InitializeAsync()
{
var appHost = await DistributedApplicationTestingBuilder.CreateAsync<Projects.Valkey_Glide_AppHost>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

If i remember correctly var wasn't recommended by current project code style. Why do we think it is required code style change?

Copy link
Author

Choose a reason for hiding this comment

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

As mentioned, this started as a small PoC, and these are essentially my configuration files; I haven’t changed them back yet. I can do so if needed.

That said, I’d like to point out that this approach aligns better with Rust’s typical style, where variables are mostly declared as let variable = value rather than let variable: Type = value. This follows modern coding practices by leveraging automatic type inference.

Copy link
Collaborator

Choose a reason for hiding this comment

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

While using idiomatic coding patterns is desired, we are also trying to keep various language wrappers aligned to the degree possible. We have maintainers who have depth in one or more of the client languages and following different coding standards for each language makes the transition and understanding of each client more difficult. While type inference is good for experienced developers, it can complicate understanding the code for developers who might not be familiar with a specific language.

WaitBehavior.StopOnResourceUnavailable
);
if (resourceEvent.Snapshot.HealthStatus is not HealthStatus.Healthy)
throw new Exception("Cache is not healthy, aspire initialization failed.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's throw something more concrete

Copy link
Author

Choose a reason for hiding this comment

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

I'm open to suggestions! ^^

I just realized that I mistakenly used the resource name instead of the container type + name. Rephrasing it to something like: "Aspire initialization failed for Valkey Container: 'cache' is not Healthy." seems like a good improvement.


namespace Valkey.Glide.InterOp.Native;

[SuppressMessage("ReSharper", "InconsistentNaming")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the benefit of having ReSharper specific suppressers?

Copy link
Author

Choose a reason for hiding this comment

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

You're right; i missed the justification.

Rider (or ReSharper) flags this because the naming matches exactly and doesn’t follow the default project-style naming. However, this is intentional since these are FFI structures, which should be kept closely aligned for consistency.

<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is the benefit of using NSubstitute?

Copy link
Author

Choose a reason for hiding this comment

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

It’s a mocking framework, primarily used to easily mock IGlideClient for now, making syntax validation more straightforward.

The key benefit is that it eliminates the need to manually create fully functional mocking classes.

@Sa1Gur
Copy link
Collaborator

Sa1Gur commented Mar 11, 2025

I would say it would be easier to review if we would have different PR for different changes.
Number of new dependencies was introduced - each one separate change
Folder structure changed - another
Code style change - also separate change

Review 30% and will take a look on the rest later.

@X39
Copy link
Author

X39 commented Mar 11, 2025

#3354 (comment)

(I'm not a maintainer) Hi, can you please post before/after benchmarks, to show how this rewrite affects perf?

I'm having trouble running the benchmark script due to my setup - I’m on Windows without Go, Java, or Python installed. It’s on my to-do list, but I first need a way to run the benchmark in a container or possibly a VM.

#3354 (comment)

I would say it would be easier to review if we would have different PR for different changes. Number of new dependencies was introduced - each one separate change Folder structure changed - another Code style change - also separate change

Review 30% and will take a look on the rest later.

The reason everything is in one PR is that it originally started as a Proof of Concept. I can try to split it up, but I won’t be able to preserve the commit messages since the PR initially came from a quick "here's how I’d do it" Reddit comment blanking the folder first.

@nihohit
Copy link
Contributor

nihohit commented Mar 11, 2025

The benchmark script has flags to determine which language to run, so you don't need go or Java. I think you'll only need javascript, to run some of the preparation scripts


# ReSharper properties
resharper_align_multiline_binary_patterns = true
resharper_align_multiline_calls_chain = true
Copy link
Collaborator

Choose a reason for hiding this comment

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

As an Open Source project, we should not take dependencies on proprietary tools like resharper. We should assume that many of our developers will not have licenses for this tool. Additionally, there are many CSharp and DotNet settings which we should not be eliminating without a deeper discussion.

@@ -88,10 +88,24 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up dotnet ${{ matrix.dotnet }}
- name: Install Node on self-hosted runner
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure I understand the need for Node in this pipeline. Can you elaborate on the need?

Copy link
Author

@X39 X39 Mar 11, 2025

Choose a reason for hiding this comment

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

Copied this over from a different PR (#3323),

{
Marshal.WriteByte(ptr, 0); // Is allocated on heap
for (var i = 0; i < utf8.Length; i++)
Marshal.WriteByte(ptr + i + 1, utf8Ptr[i]);

Check failure

Code scanning / CodeQL

Unvalidated local pointer arithmetic Critical

Unvalidated pointer arithmetic from virtual method
GetBytes
.
{
Marshal.WriteByte(ptr, 1); // Is allocated on buffer (potentially stack)
for (var i = 0; i < utf8.Length; i++)
Marshal.WriteByte(ptr + i + 1, utf8Ptr[i]);

Check failure

Code scanning / CodeQL

Unvalidated local pointer arithmetic Critical

Unvalidated pointer arithmetic from virtual method
GetBytes
.
@Yury-Fridlyand
Copy link
Collaborator

Hi @X39, thank you so much for your contribution.
I agree with mentioned above about the need in splitting the PR. I also found few other things I'd like to highlight before I'm going into the depths of code.

  1. .Net targeting and supported C# versions. The client should support .net8.0 (C# 12) 100% and probably .net6.0 (C# 10). There are ongoing discussions in that regard. .net9.0 won't have LTS, so we cannot rely on it.
  2. 3rd party dependencies. They are allowed, but they should comply with the whitelist - all they way down through the dependency tree. We have a tool that verifies that on every PR (ORT tool), but it is not activated for C#, because it wasn't planned to have any dependency.
  3. Client API. It is required to have them as much similar as possible. A user should be able to switch from one language to another with minimal code changes. For sure, we have come language specific restrictions, but other than that they are identical. Docs are just copy-pasted and adopted.
    It is especially relevant for commands with optional parameters (options). It is not scalable to do 2/4/8/16 signatures for a command with 1..4 optional arguments. Have a look at geosearch - it should be reflected by 2 client APIs, just because it has different return types, but not 22.

I'll do my best to help you with your code. I'll start with requirements and project standards - unfortunately, not all of them are easily accessible for the public.

You also need to downmerge in your branch some other fixes I'm working on once we complete them - routing (#3353) and handling binary strings (#3347). You can see the project plan for C# client in #216.

@X39
Copy link
Author

X39 commented Mar 11, 2025

Feel free to close this PR or keep reviewing it as-is so I can integrate the changes once a final decision is made.

Over the weekend, I'll be splitting up the branch and creating sequential PRs that build on each other. I’ll aim to cover everything by then! :)

@Yury-Fridlyand Yury-Fridlyand marked this pull request as draft March 11, 2025 22:31
@asafpamzn
Copy link
Collaborator

Thanks a lot @X39 for this huge contribution we are very happy about it.
Lets see what is the best way to merge it in.
@Yury-Fridlyand and the other maintainers will help you to merge it in.

@X39 X39 mentioned this pull request Mar 12, 2025
@avifenesh
Copy link
Member

Well, I'm impressed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C# C# wrapper
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants