diff --git a/Cargo.lock b/Cargo.lock
index 5397271ce..0f98a3684 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -557,6 +557,7 @@ dependencies = [
  "schemars",
  "serde",
  "serde_json",
+ "tokio",
 ]
 
 [[package]]
diff --git a/crates/bitwarden-c/src/c.rs b/crates/bitwarden-c/src/c.rs
index 158c38025..76fed0073 100644
--- a/crates/bitwarden-c/src/c.rs
+++ b/crates/bitwarden-c/src/c.rs
@@ -1,6 +1,11 @@
-use std::{ffi::CStr, os::raw::c_char, str};
+use std::{
+    ffi::{CStr, CString},
+    os::raw::c_char,
+    str,
+};
 
 use bitwarden_json::client::Client;
+use tokio::task::JoinHandle;
 
 use crate::{box_ptr, ffi_ref};
 
@@ -28,6 +33,46 @@ pub extern "C" fn run_command(c_str_ptr: *const c_char, client_ptr: *const CClie
     }
 }
 
+type OnCompletedCallback = unsafe extern "C" fn(result: *mut c_char) -> ();
+
+#[no_mangle]
+pub extern "C" fn run_command_async(
+    c_str_ptr: *const c_char,
+    client_ptr: *const CClient,
+    on_completed_callback: OnCompletedCallback,
+    is_cancellable: bool,
+) -> *mut JoinHandle<()> {
+    let client = unsafe { ffi_ref!(client_ptr) };
+    let input_str = str::from_utf8(unsafe { CStr::from_ptr(c_str_ptr) }.to_bytes())
+        .expect("Input should be a valid string")
+        // Languages may assume that the string is collectable as soon as this method exits
+        // but it's not since the request will be run in the background
+        // so we need to make our own copy.
+        .to_owned();
+
+    let join_handle = client.runtime.spawn(async move {
+        let result = client.client.run_command(input_str.as_str()).await;
+        let str_result = match std::ffi::CString::new(result) {
+            Ok(cstr) => cstr.into_raw(),
+            Err(_) => panic!("failed to return comment result: null encountered"),
+        };
+
+        // run completed function
+        unsafe {
+            on_completed_callback(str_result);
+            let _ = CString::from_raw(str_result);
+        }
+    });
+
+    // We only want to box the join handle the caller has said that they may want to cancel,
+    // essentially promising to us that they will take care of the returned pointer.
+    if is_cancellable {
+        box_ptr!(join_handle)
+    } else {
+        std::ptr::null_mut()
+    }
+}
+
 // Init client, potential leak! You need to call free_mem after this!
 #[no_mangle]
 pub extern "C" fn init(c_str_ptr: *const c_char) -> *mut CClient {
@@ -56,3 +101,15 @@ pub extern "C" fn init(c_str_ptr: *const c_char) -> *mut CClient {
 pub extern "C" fn free_mem(client_ptr: *mut CClient) {
     std::mem::drop(unsafe { Box::from_raw(client_ptr) });
 }
+
+#[no_mangle]
+pub extern "C" fn abort_and_free_handle(join_handle_ptr: *mut tokio::task::JoinHandle<()>) -> () {
+    let join_handle = unsafe { Box::from_raw(join_handle_ptr) };
+    join_handle.abort();
+    std::mem::drop(join_handle);
+}
+
+#[no_mangle]
+pub extern "C" fn free_handle(join_handle_ptr: *mut tokio::task::JoinHandle<()>) -> () {
+    std::mem::drop(unsafe { Box::from_raw(join_handle_ptr) });
+}
diff --git a/crates/bitwarden-json/Cargo.toml b/crates/bitwarden-json/Cargo.toml
index ed1b39580..dd703b9d7 100644
--- a/crates/bitwarden-json/Cargo.toml
+++ b/crates/bitwarden-json/Cargo.toml
@@ -26,5 +26,8 @@ schemars = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
 
+[target.'cfg(debug_assertions)'.dependencies]
+tokio = { version = "1.36.0", features = ["time"] }
+
 [lints]
 workspace = true
diff --git a/crates/bitwarden-json/src/client.rs b/crates/bitwarden-json/src/client.rs
index dc84c559d..55c08c2e5 100644
--- a/crates/bitwarden-json/src/client.rs
+++ b/crates/bitwarden-json/src/client.rs
@@ -99,6 +99,34 @@ impl Client {
                     client.generator().password(req).into_string()
                 }
             },
+            #[cfg(debug_assertions)]
+            Command::Debug(cmd) => {
+                use bitwarden::Error;
+
+                use crate::command::DebugCommand;
+
+                match cmd {
+                    DebugCommand::CancellationTest { duration_millis } => {
+                        use tokio::time::sleep;
+                        let duration = std::time::Duration::from_millis(duration_millis);
+                        sleep(duration).await;
+                        println!("After wait #1");
+                        sleep(duration).await;
+                        println!("After wait #2");
+                        sleep(duration).await;
+                        println!("After wait #3");
+                        Ok::<i32, Error>(42).into_string()
+                    }
+                    DebugCommand::ErrorTest {} => {
+                        use bitwarden::Error;
+
+                        Err::<i32, Error>(Error::Internal(std::borrow::Cow::Borrowed(
+                            "This is an error.",
+                        )))
+                        .into_string()
+                    }
+                }
+            }
         }
     }
 
diff --git a/crates/bitwarden-json/src/command.rs b/crates/bitwarden-json/src/command.rs
index 7483b90cf..132925b0c 100644
--- a/crates/bitwarden-json/src/command.rs
+++ b/crates/bitwarden-json/src/command.rs
@@ -79,6 +79,8 @@ pub enum Command {
     Projects(ProjectsCommand),
     #[cfg(feature = "secrets")]
     Generators(GeneratorsCommand),
+    #[cfg(debug_assertions)]
+    Debug(DebugCommand),
 }
 
 #[cfg(feature = "secrets")]
@@ -188,3 +190,11 @@ pub enum GeneratorsCommand {
     /// Returns: [String]
     GeneratePassword(PasswordGeneratorRequest),
 }
+
+#[cfg(debug_assertions)]
+#[derive(Serialize, Deserialize, JsonSchema, Debug)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub enum DebugCommand {
+    CancellationTest { duration_millis: u64 },
+    ErrorTest {},
+}
diff --git a/languages/csharp/Bitwarden.Sdk.Samples/Bitwarden.Sdk.Samples.csproj b/languages/csharp/Bitwarden.Sdk.Samples/Bitwarden.Sdk.Samples.csproj
index 5b189d8ca..ab616ab83 100644
--- a/languages/csharp/Bitwarden.Sdk.Samples/Bitwarden.Sdk.Samples.csproj
+++ b/languages/csharp/Bitwarden.Sdk.Samples/Bitwarden.Sdk.Samples.csproj
@@ -2,7 +2,7 @@
 
     <PropertyGroup>
         <OutputType>Exe</OutputType>
-        <TargetFramework>net6.0</TargetFramework>
+        <TargetFramework>net8.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>
diff --git a/languages/csharp/Bitwarden.Sdk.Samples/Program.cs b/languages/csharp/Bitwarden.Sdk.Samples/Program.cs
index e80d12017..64837ee76 100644
--- a/languages/csharp/Bitwarden.Sdk.Samples/Program.cs
+++ b/languages/csharp/Bitwarden.Sdk.Samples/Program.cs
@@ -1,4 +1,4 @@
-using Bitwarden.Sdk;
+using Bitwarden.Sdk;
 
 // Get environment variables
 var identityUrl = Environment.GetEnvironmentVariable("IDENTITY_URL")!;
@@ -15,10 +15,10 @@
 });
 
 // Authenticate
-bitwardenClient.Auth.LoginAccessToken(accessToken, stateFile);
+await bitwardenClient.Auth.LoginAccessTokenAsync(accessToken, stateFile);
 
 // Projects List
-var projectsList = bitwardenClient.Projects.List(organizationId).Data;
+var projectsList = (await bitwardenClient.Projects.ListAsync(organizationId)).Data;
 Console.WriteLine("A list of all projects:");
 foreach (ProjectResponse pr in projectsList)
 {
@@ -30,9 +30,9 @@
 
 // Projects Create, Update, & Get
 Console.WriteLine("Creating and updating a project");
-var projectResponse = bitwardenClient.Projects.Create(organizationId, "NewTestProject");
-projectResponse = bitwardenClient.Projects.Update(organizationId, projectResponse.Id, "NewTestProject Renamed");
-projectResponse = bitwardenClient.Projects.Get(projectResponse.Id);
+var projectResponse = await bitwardenClient.Projects.CreateAsync(organizationId, "NewTestProject");
+projectResponse = await bitwardenClient.Projects.UpdateAsync(organizationId, projectResponse.Id, "NewTestProject Renamed");
+projectResponse = await bitwardenClient.Projects.GetAsync(projectResponse.Id);
 Console.WriteLine("Here is the project we created and updated:");
 Console.WriteLine(projectResponse.Name);
 
@@ -40,7 +40,7 @@
 Console.ReadLine();
 
 // Secrets list
-var secretsList = bitwardenClient.Secrets.List(organizationId).Data;
+var secretsList = (await bitwardenClient.Secrets.ListAsync(organizationId)).Data;
 Console.WriteLine("A list of all secrets:");
 foreach (SecretIdentifierResponse sr in secretsList)
 {
@@ -52,9 +52,9 @@
 
 // Secrets Create, Update, Get
 Console.WriteLine("Creating and updating a secret");
-var secretResponse = bitwardenClient.Secrets.Create(organizationId, "New Secret", "the secret value", "the secret note", new[] { projectResponse.Id });
-secretResponse = bitwardenClient.Secrets.Update(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id });
-secretResponse = bitwardenClient.Secrets.Get(secretResponse.Id);
+var secretResponse = await bitwardenClient.Secrets.CreateAsync(organizationId, "New Secret", "the secret value", "the secret note", new[] { projectResponse.Id });
+secretResponse = await bitwardenClient.Secrets.UpdateAsync(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id });
+secretResponse = await bitwardenClient.Secrets.GetAsync(secretResponse.Id);
 Console.WriteLine("Here is the secret we created and updated:");
 Console.WriteLine(secretResponse.Key);
 
@@ -62,12 +62,12 @@
 Console.ReadLine();
 
 // Secrets GetByIds
-var secretsResponse = bitwardenClient.Secrets.GetByIds(new[] { secretResponse.Id });
+var secretsResponse = await bitwardenClient.Secrets.GetByIdsAsync(new[] { secretResponse.Id });
 
 // Secrets Sync
-var syncResponse = bitwardenClient.Secrets.Sync(organizationId, null);
+var syncResponse = await bitwardenClient.Secrets.SyncAsync(organizationId, null);
 
 // Secrets & Projects Delete
 Console.WriteLine("Deleting our secret and project");
-bitwardenClient.Secrets.Delete(new[] { secretResponse.Id });
-bitwardenClient.Projects.Delete(new[] { projectResponse.Id });
+await bitwardenClient.Secrets.DeleteAsync(new[] { secretResponse.Id });
+await bitwardenClient.Projects.DeleteAsync(new[] { projectResponse.Id });
diff --git a/languages/csharp/Bitwarden.Sdk.Tests/Bitwarden.Sdk.Tests.csproj b/languages/csharp/Bitwarden.Sdk.Tests/Bitwarden.Sdk.Tests.csproj
new file mode 100644
index 000000000..3dbd8c99e
--- /dev/null
+++ b/languages/csharp/Bitwarden.Sdk.Tests/Bitwarden.Sdk.Tests.csproj
@@ -0,0 +1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+
+    <IsPackable>false</IsPackable>
+    <IsTestProject>true</IsTestProject>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
+    <PackageReference Include="xunit" Version="2.4.2" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+    <PackageReference Include="coverlet.collector" Version="6.0.0">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Bitwarden.Sdk\Bitwarden.Sdk.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/languages/csharp/Bitwarden.Sdk.Tests/GlobalUsings.cs b/languages/csharp/Bitwarden.Sdk.Tests/GlobalUsings.cs
new file mode 100644
index 000000000..c802f4480
--- /dev/null
+++ b/languages/csharp/Bitwarden.Sdk.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
diff --git a/languages/csharp/Bitwarden.Sdk.Tests/InteropTests.cs b/languages/csharp/Bitwarden.Sdk.Tests/InteropTests.cs
new file mode 100644
index 000000000..d6903d58d
--- /dev/null
+++ b/languages/csharp/Bitwarden.Sdk.Tests/InteropTests.cs
@@ -0,0 +1,35 @@
+using Bitwarden.Sdk;
+using System.Diagnostics;
+
+namespace Bitwarden.Sdk.Tests;
+
+public class InteropTests
+{
+    [Fact]
+    public async void CancelingTest_ThrowsTaskCanceledException()
+    {
+        var client = new BitwardenClient();
+
+        var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250));
+
+        await Assert.ThrowsAsync<TaskCanceledException>(async () => await client.CancellationTestAsync(cts.Token));
+    }
+
+    [Fact]
+    public async void NoCancel_TaskCompletesSuccessfully()
+    {
+        var client = new BitwardenClient();
+
+        var result = await client.CancellationTestAsync(CancellationToken.None);
+        Assert.Equal(42, result);
+    }
+
+    [Fact]
+    public async void Error_ThrowsException()
+    {
+        var client = new BitwardenClient();
+
+        var bitwardenException = await Assert.ThrowsAsync<BitwardenException>(async () => await client.ErrorTestAsync());
+        Assert.Equal("Internal error: This is an error.", bitwardenException.Message);
+    }
+}
diff --git a/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs b/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs
new file mode 100644
index 000000000..6432f9ea1
--- /dev/null
+++ b/languages/csharp/Bitwarden.Sdk.Tests/SampleTests.cs
@@ -0,0 +1,56 @@
+namespace Bitwarden.Sdk.Tests;
+
+public class SampleTests
+{
+    [SecretsManagerFact]
+    public async Task RunSample_Works()
+    {
+        // Get environment variables
+        var identityUrl = Environment.GetEnvironmentVariable("IDENTITY_URL")!;
+        var apiUrl = Environment.GetEnvironmentVariable("API_URL")!;
+        var organizationId = Guid.Parse(Environment.GetEnvironmentVariable("ORGANIZATION_ID")!);
+        var accessToken = Environment.GetEnvironmentVariable("ACCESS_TOKEN")!;
+        var stateFile = Environment.GetEnvironmentVariable("STATE_FILE")!;
+
+        // Create the SDK Client
+        using var bitwardenClient = new BitwardenClient(new BitwardenSettings
+        {
+            ApiUrl = apiUrl,
+            IdentityUrl = identityUrl
+        });
+
+        // Authenticate
+        await bitwardenClient.Auth.LoginAccessTokenAsync(accessToken, stateFile);
+
+        // Projects Create, Update, & Get
+        var projectResponse = await bitwardenClient.Projects.CreateAsync(organizationId, "NewTestProject");
+        projectResponse = await bitwardenClient.Projects.UpdateAsync(organizationId, projectResponse.Id, "NewTestProject Renamed");
+        projectResponse = await bitwardenClient.Projects.GetAsync(projectResponse.Id);
+
+        Assert.Equal("NewTestProject Renamed", projectResponse.Name);
+
+        var projectList = await bitwardenClient.Projects.ListAsync(organizationId);
+
+        Assert.True(projectList.Data.Count() >= 1);
+
+        // Secrets list
+        var secretsList = await bitwardenClient.Secrets.ListAsync(organizationId);
+
+        // Secrets Create, Update, Get
+        var secretResponse = await bitwardenClient.Secrets.CreateAsync(organizationId, "New Secret", "the secret value", "the secret note", new[] { projectResponse.Id });
+        secretResponse = await bitwardenClient.Secrets.UpdateAsync(organizationId, secretResponse.Id, "New Secret Name", "the secret value", "the secret note", new[] { projectResponse.Id });
+        secretResponse = await bitwardenClient.Secrets.GetAsync(secretResponse.Id);
+
+        Assert.Equal("New Secret Name", secretResponse.Key);
+
+        // Secrets GetByIds
+        var secretsResponse = await bitwardenClient.Secrets.GetByIdsAsync(new[] { secretResponse.Id });
+
+        // Secrets Sync
+        var syncResponse = await bitwardenClient.Secrets.SyncAsync(organizationId, null);
+
+        // Secrets & Projects Delete
+        await bitwardenClient.Secrets.DeleteAsync(new[] { secretResponse.Id });
+        await bitwardenClient.Projects.DeleteAsync(new[] { projectResponse.Id });
+    }
+}
diff --git a/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs b/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs
new file mode 100644
index 000000000..e5bcebfc2
--- /dev/null
+++ b/languages/csharp/Bitwarden.Sdk.Tests/SecretsManagerFact.cs
@@ -0,0 +1,54 @@
+namespace Bitwarden.Sdk.Tests;
+
+public class SecretsManagerFactAttribute : FactAttribute
+{
+    public SecretsManagerFactAttribute()
+    {
+        if (!TryGetEnvironment("IDENTITY_URL", out var identityUrl))
+        {
+            Skip = "Environment variable IDENTITY_URL was not provided.";
+        }
+
+        if (!Uri.TryCreate(identityUrl, UriKind.Absolute, out _))
+        {
+            Skip = $"The identity url {identityUrl} provided in IDENTITY_URL is not a valid URL.";
+        }
+
+        if (!TryGetEnvironment("API_URL", out var apiUrl))
+        {
+            Skip = "Environment variable API_URL was not provided.";
+        }
+
+        if (!Uri.TryCreate(apiUrl, UriKind.Absolute, out _))
+        {
+            Skip = $"The identity url {apiUrl} provided in API_URL is not a valid URL.";
+        }
+
+        if (!TryGetEnvironment("ORGANIZATION_ID", out var organizationId))
+        {
+            Skip = "Environment variable ORGANIZATION_ID was not provided.";
+        }
+
+        if (!Guid.TryParse(organizationId, out _))
+        {
+            Skip = $"The organization id {organizationId} provided in ORGANIZATION_ID is not a valid GUID.";
+        }
+
+        if (!TryGetEnvironment("ACCESS_TOKEN", out _))
+        {
+            Skip = "Environment variable ACCESS_TOKEN was not provided.";
+        }
+    }
+
+    private static bool TryGetEnvironment(string variable, out string value)
+    {
+        value = Environment.GetEnvironmentVariable(variable);
+
+        if (string.IsNullOrWhiteSpace(value))
+        {
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/languages/csharp/Bitwarden.Sdk/AuthClient.cs b/languages/csharp/Bitwarden.Sdk/AuthClient.cs
index e801f2aee..7b7f25d2b 100644
--- a/languages/csharp/Bitwarden.Sdk/AuthClient.cs
+++ b/languages/csharp/Bitwarden.Sdk/AuthClient.cs
@@ -9,10 +9,10 @@ internal AuthClient(CommandRunner commandRunner)
         _commandRunner = commandRunner;
     }
 
-    public void LoginAccessToken(string accessToken, string stateFile = "")
+    public async Task LoginAccessTokenAsync(string accessToken, string stateFile = "", CancellationToken cancellationToken = default)
     {
         var command = new Command { LoginAccessToken = new AccessTokenLoginRequest { AccessToken = accessToken, StateFile = stateFile } };
-        var response = _commandRunner.RunCommand<ResponseForApiKeyLoginResponse>(command);
+        var response = await _commandRunner.RunCommandAsync<ResponseForApiKeyLoginResponse>(command, cancellationToken);
         if (response is not { Success: true })
         {
             throw new BitwardenAuthException(response != null ? response.ErrorMessage : "Login failed");
diff --git a/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj b/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj
index a7c82e4b5..2dd16dd82 100644
--- a/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj
+++ b/languages/csharp/Bitwarden.Sdk/Bitwarden.Sdk.csproj
@@ -1,17 +1,19 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net6.0</TargetFramework>
+    <TargetFramework>net8.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <RootNamespace>Bitwarden.Sdk</RootNamespace>
-
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <Title>Bitwarden Secrets Manager SDK</Title>
     <Authors>Bitwarden Inc.</Authors>
     <Description>.NET bindings for interacting with the Bitwarden Secrets Manager</Description>
     <Copyright>Bitwarden Inc.</Copyright>
     <Product>SDK</Product>
 
+    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
+
     <RepositoryUrl>https://github.com/bitwarden/sdk/tree/main/languages/csharp</RepositoryUrl>
     <RepositoryType>Git</RepositoryType>
 
@@ -74,4 +76,4 @@
       <PackagePath>runtimes/win-x64/native</PackagePath>
     </Content>
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs b/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs
new file mode 100644
index 000000000..0b1e95409
--- /dev/null
+++ b/languages/csharp/Bitwarden.Sdk/BitwardenClient.Debug.cs
@@ -0,0 +1,58 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+
+namespace Bitwarden.Sdk;
+
+[Obsolete("DebugCommand is intended for tests only, using any of these commands will throw errors in production code.")]
+[EditorBrowsable(EditorBrowsableState.Never)]
+partial class DebugCommand
+{
+
+}
+
+#if DEBUG
+public sealed partial class BitwardenClient
+{
+    public async Task<int> CancellationTestAsync(CancellationToken token)
+    {
+        var result = await _commandRunner.RunCommandAsync<JsonElement>(
+            new Command
+            {
+                Debug = new DebugCommand
+                {
+                    CancellationTest = new CancellationTest
+                    {
+                        DurationMillis = 200,
+                    },
+                },
+            }, token);
+
+        return ParseResult(result).GetInt32();
+    }
+
+    public async Task<int> ErrorTestAsync()
+    {
+        var result = await _commandRunner.RunCommandAsync<JsonElement>(
+            new Command
+            {
+                Debug = new DebugCommand
+                {
+                    ErrorTest = new ErrorTest(),
+                },
+            }, CancellationToken.None);
+
+        return ParseResult(result).GetInt32();
+    }
+
+    private JsonElement ParseResult(JsonElement result)
+    {
+        if (result.GetProperty("success").GetBoolean())
+        {
+            return result.GetProperty("data");
+        }
+
+        throw new BitwardenException(result.GetProperty("errorMessage").GetString());
+    }
+}
+#endif
diff --git a/languages/csharp/Bitwarden.Sdk/BitwardenClient.cs b/languages/csharp/Bitwarden.Sdk/BitwardenClient.cs
index 2f10e0cf9..636a100b1 100644
--- a/languages/csharp/Bitwarden.Sdk/BitwardenClient.cs
+++ b/languages/csharp/Bitwarden.Sdk/BitwardenClient.cs
@@ -1,6 +1,8 @@
-namespace Bitwarden.Sdk;
+using System.Text.Json;
 
-public sealed class BitwardenClient : IDisposable
+namespace Bitwarden.Sdk;
+
+public sealed partial class BitwardenClient : IDisposable
 {
     private readonly CommandRunner _commandRunner;
     private readonly BitwardenSafeHandle _handle;
diff --git a/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs b/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs
index ada399401..e18d3976b 100644
--- a/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs
+++ b/languages/csharp/Bitwarden.Sdk/BitwardenLibrary.cs
@@ -2,20 +2,71 @@
 
 namespace Bitwarden.Sdk;
 
-internal static class BitwardenLibrary
+internal static partial class BitwardenLibrary
 {
-    [DllImport("bitwarden_c", CallingConvention = CallingConvention.Cdecl)]
-    private static extern BitwardenSafeHandle init(string settings);
+    [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)]
+    private static partial BitwardenSafeHandle init(string settings);
 
-    [DllImport("bitwarden_c", CallingConvention = CallingConvention.Cdecl)]
-    private static extern void free_mem(IntPtr handle);
+    [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)]
+    private static partial void free_mem(IntPtr handle);
 
-    [DllImport("bitwarden_c", CallingConvention = CallingConvention.Cdecl)]
-    private static extern string run_command(string json, BitwardenSafeHandle handle);
+    [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)]
+    private static partial string run_command(string json, BitwardenSafeHandle handle);
+
+    internal delegate void OnCompleteCallback(IntPtr json);
+
+    [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)]
+    private static partial IntPtr run_command_async(string json,
+        BitwardenSafeHandle handle,
+        OnCompleteCallback onCompletedCallback,
+        [MarshalAs(UnmanagedType.U1)] bool isCancellable);
+
+    [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)]
+    private static partial void abort_and_free_handle(IntPtr joinHandle);
+
+    [LibraryImport("bitwarden_c", StringMarshalling = StringMarshalling.Utf8)]
+    private static partial void free_handle(IntPtr joinHandle);
 
     internal static BitwardenSafeHandle Init(string settings) => init(settings);
 
     internal static void FreeMemory(IntPtr handle) => free_mem(handle);
 
     internal static string RunCommand(string json, BitwardenSafeHandle handle) => run_command(json, handle);
+
+    internal static Task<string> RunCommandAsync(string json, BitwardenSafeHandle handle, CancellationToken cancellationToken)
+    {
+        cancellationToken.ThrowIfCancellationRequested();
+        var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+        IntPtr abortPointer = IntPtr.Zero;
+
+        try
+        {
+
+            abortPointer = run_command_async(json, handle, (resultPointer) =>
+            {
+                var stringResult = Marshal.PtrToStringUTF8(resultPointer);
+                tcs.SetResult(stringResult);
+
+                if (abortPointer != IntPtr.Zero)
+                {
+                    free_handle(abortPointer);
+                }
+            }, cancellationToken.CanBeCanceled);
+        }
+        catch (Exception ex)
+        {
+            tcs.SetException(ex);
+        }
+
+        cancellationToken.Register((state) =>
+        {
+            // This register delegate will never be called unless the token is cancelable
+            // therefore we know that the abortPointer is a valid pointer.
+            abort_and_free_handle((IntPtr)state);
+            tcs.SetCanceled(cancellationToken);
+        }, abortPointer);
+
+        return tcs.Task;
+    }
 }
diff --git a/languages/csharp/Bitwarden.Sdk/CommandRunner.cs b/languages/csharp/Bitwarden.Sdk/CommandRunner.cs
index fbd6b7e31..a62e0877e 100644
--- a/languages/csharp/Bitwarden.Sdk/CommandRunner.cs
+++ b/languages/csharp/Bitwarden.Sdk/CommandRunner.cs
@@ -17,4 +17,17 @@ internal CommandRunner(BitwardenSafeHandle handle)
         var result = BitwardenLibrary.RunCommand(req, _handle);
         return JsonSerializer.Deserialize<T>(result, Converter.Settings);
     }
+
+    internal async Task<T?> RunCommandAsync<T>(Command command, CancellationToken cancellationToken)
+    {
+        var req = JsonSerializer.Serialize(command, Converter.Settings);
+        var result = await BitwardenLibrary.RunCommandAsync(req, _handle, cancellationToken);
+        return JsonSerializer.Deserialize<T>(result, Converter.Settings);
+    }
+
+    internal async Task<T?> RunCommandAsync<T>(string command, CancellationToken cancellationToken)
+    {
+        var result = await BitwardenLibrary.RunCommandAsync(command, _handle, cancellationToken);
+        return JsonSerializer.Deserialize<T>(result, Converter.Settings);
+    }
 }
diff --git a/languages/csharp/Bitwarden.Sdk/ProjectsClient.cs b/languages/csharp/Bitwarden.Sdk/ProjectsClient.cs
index 47a419364..efa651518 100644
--- a/languages/csharp/Bitwarden.Sdk/ProjectsClient.cs
+++ b/languages/csharp/Bitwarden.Sdk/ProjectsClient.cs
@@ -9,10 +9,10 @@ internal ProjectsClient(CommandRunner commandRunner)
         _commandRunner = commandRunner;
     }
 
-    public ProjectResponse Get(Guid id)
+    public async Task<ProjectResponse> GetAsync(Guid id, CancellationToken cancellationToken = default)
     {
         var command = new Command { Projects = new ProjectsCommand { Get = new ProjectGetRequest { Id = id } } };
-        var result = _commandRunner.RunCommand<ResponseForProjectResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForProjectResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -22,7 +22,7 @@ public ProjectResponse Get(Guid id)
         throw new BitwardenException(result != null ? result.ErrorMessage : "Project not found");
     }
 
-    public ProjectResponse Create(Guid organizationId, string name)
+    public async Task<ProjectResponse> CreateAsync(Guid organizationId, string name, CancellationToken cancellationToken = default)
     {
         var command = new Command
         {
@@ -31,7 +31,7 @@ public ProjectResponse Create(Guid organizationId, string name)
                 Create = new ProjectCreateRequest { OrganizationId = organizationId, Name = name }
             }
         };
-        var result = _commandRunner.RunCommand<ResponseForProjectResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForProjectResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -41,7 +41,7 @@ public ProjectResponse Create(Guid organizationId, string name)
         throw new BitwardenException(result != null ? result.ErrorMessage : "Project create failed");
     }
 
-    public ProjectResponse Update(Guid organizationId, Guid id, string name)
+    public async Task<ProjectResponse> UpdateAsync(Guid organizationId, Guid id, string name, CancellationToken cancellationToken = default)
     {
         var command = new Command
         {
@@ -50,7 +50,7 @@ public ProjectResponse Update(Guid organizationId, Guid id, string name)
                 Update = new ProjectPutRequest { Id = id, OrganizationId = organizationId, Name = name }
             }
         };
-        var result = _commandRunner.RunCommand<ResponseForProjectResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForProjectResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -60,13 +60,13 @@ public ProjectResponse Update(Guid organizationId, Guid id, string name)
         throw new BitwardenException(result != null ? result.ErrorMessage : "Project update failed");
     }
 
-    public ProjectsDeleteResponse Delete(Guid[] ids)
+    public async Task<ProjectsDeleteResponse> DeleteAsync(Guid[] ids, CancellationToken cancellationToken = default)
     {
         var command = new Command
         {
             Projects = new ProjectsCommand { Delete = new ProjectsDeleteRequest { Ids = ids } }
         };
-        var result = _commandRunner.RunCommand<ResponseForProjectsDeleteResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForProjectsDeleteResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -76,13 +76,13 @@ public ProjectsDeleteResponse Delete(Guid[] ids)
         throw new BitwardenException(result != null ? result.ErrorMessage : "Project delete failed");
     }
 
-    public ProjectsResponse List(Guid organizationId)
+    public async Task<ProjectsResponse> ListAsync(Guid organizationId, CancellationToken cancellationToken = default)
     {
         var command = new Command
         {
             Projects = new ProjectsCommand { List = new ProjectsListRequest { OrganizationId = organizationId } }
         };
-        var result = _commandRunner.RunCommand<ResponseForProjectsResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForProjectsResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
diff --git a/languages/csharp/Bitwarden.Sdk/SecretsClient.cs b/languages/csharp/Bitwarden.Sdk/SecretsClient.cs
index 5dd77fc6b..e5e8cbc26 100644
--- a/languages/csharp/Bitwarden.Sdk/SecretsClient.cs
+++ b/languages/csharp/Bitwarden.Sdk/SecretsClient.cs
@@ -9,10 +9,10 @@ internal SecretsClient(CommandRunner commandRunner)
         _commandRunner = commandRunner;
     }
 
-    public SecretResponse Get(Guid id)
+    public async Task<SecretResponse> GetAsync(Guid id, CancellationToken cancellationToken = default)
     {
         var command = new Command { Secrets = new SecretsCommand { Get = new SecretGetRequest { Id = id } } };
-        var result = _commandRunner.RunCommand<ResponseForSecretResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForSecretResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -22,10 +22,10 @@ public SecretResponse Get(Guid id)
         throw new BitwardenException(result != null ? result.ErrorMessage : "Secret not found");
     }
 
-    public SecretsResponse GetByIds(Guid[] ids)
+    public async Task<SecretsResponse> GetByIdsAsync(Guid[] ids, CancellationToken cancellationToken = default)
     {
         var command = new Command { Secrets = new SecretsCommand { GetByIds = new SecretsGetRequest { Ids = ids } } };
-        var result = _commandRunner.RunCommand<ResponseForSecretsResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForSecretsResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -35,7 +35,7 @@ public SecretsResponse GetByIds(Guid[] ids)
         throw new BitwardenException(result != null ? result.ErrorMessage : "Secret not found");
     }
 
-    public SecretResponse Create(Guid organizationId, string key, string value, string note, Guid[] projectIds)
+    public async Task<SecretResponse> CreateAsync(Guid organizationId, string key, string value, string note, Guid[] projectIds, CancellationToken cancellationToken = default)
     {
         var command = new Command
         {
@@ -52,7 +52,7 @@ public SecretResponse Create(Guid organizationId, string key, string value, stri
             }
         };
 
-        var result = _commandRunner.RunCommand<ResponseForSecretResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForSecretResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -62,7 +62,7 @@ public SecretResponse Create(Guid organizationId, string key, string value, stri
         throw new BitwardenException(result != null ? result.ErrorMessage : "Secret create failed");
     }
 
-    public SecretResponse Update(Guid organizationId, Guid id, string key, string value, string note, Guid[] projectIds)
+    public async Task<SecretResponse> UpdateAsync(Guid organizationId, Guid id, string key, string value, string note, Guid[] projectIds, CancellationToken cancellationToken = default)
     {
         var command = new Command
         {
@@ -80,7 +80,7 @@ public SecretResponse Update(Guid organizationId, Guid id, string key, string va
             }
         };
 
-        var result = _commandRunner.RunCommand<ResponseForSecretResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForSecretResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -90,10 +90,10 @@ public SecretResponse Update(Guid organizationId, Guid id, string key, string va
         throw new BitwardenException(result != null ? result.ErrorMessage : "Secret update failed");
     }
 
-    public SecretsDeleteResponse Delete(Guid[] ids)
+    public async Task<SecretsDeleteResponse> DeleteAsync(Guid[] ids, CancellationToken cancellationToken = default)
     {
         var command = new Command { Secrets = new SecretsCommand { Delete = new SecretsDeleteRequest { Ids = ids } } };
-        var result = _commandRunner.RunCommand<ResponseForSecretsDeleteResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForSecretsDeleteResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -103,13 +103,13 @@ public SecretsDeleteResponse Delete(Guid[] ids)
         throw new BitwardenException(result != null ? result.ErrorMessage : "Secrets delete failed");
     }
 
-    public SecretIdentifiersResponse List(Guid organizationId)
+    public async Task<SecretIdentifiersResponse> ListAsync(Guid organizationId, CancellationToken cancellationToken = default)
     {
         var command = new Command
         {
             Secrets = new SecretsCommand { List = new SecretIdentifiersRequest { OrganizationId = organizationId } }
         };
-        var result = _commandRunner.RunCommand<ResponseForSecretIdentifiersResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForSecretIdentifiersResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
@@ -119,7 +119,7 @@ public SecretIdentifiersResponse List(Guid organizationId)
         throw new BitwardenException(result != null ? result.ErrorMessage : "No secrets for given organization");
     }
 
-    public SecretsSyncResponse Sync(Guid organizationId, DateTimeOffset? lastSyncedDate)
+    public async Task<SecretsSyncResponse> SyncAsync(Guid organizationId, DateTimeOffset? lastSyncedDate, CancellationToken cancellationToken = default)
     {
         var command = new Command
         {
@@ -133,7 +133,7 @@ public SecretsSyncResponse Sync(Guid organizationId, DateTimeOffset? lastSyncedD
             }
         };
 
-        var result = _commandRunner.RunCommand<ResponseForSecretsSyncResponse>(command);
+        var result = await _commandRunner.RunCommandAsync<ResponseForSecretsSyncResponse>(command, cancellationToken);
 
         if (result is { Success: true })
         {
diff --git a/languages/csharp/Bitwarden.sln b/languages/csharp/Bitwarden.sln
index 4cf8d147f..d57218979 100644
--- a/languages/csharp/Bitwarden.sln
+++ b/languages/csharp/Bitwarden.sln
@@ -1,9 +1,12 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
+# 
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Sdk", "Bitwarden.Sdk\Bitwarden.Sdk.csproj", "{DADE59E5-E573-430A-8EB2-BC21D8E8C1D3}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Sdk.Samples", "Bitwarden.Sdk.Samples\Bitwarden.Sdk.Samples.csproj", "{CA9F8EDC-643F-4624-AC00-F741E1F30CA4}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bitwarden.Sdk.Tests", "Bitwarden.Sdk.Tests\Bitwarden.Sdk.Tests.csproj", "{6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -18,5 +21,9 @@ Global
 		{CA9F8EDC-643F-4624-AC00-F741E1F30CA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{CA9F8EDC-643F-4624-AC00-F741E1F30CA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{CA9F8EDC-643F-4624-AC00-F741E1F30CA4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6E62CBBF-E9E6-4661-A3DC-D89C18E15A89}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 EndGlobal