You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/08/17 07:16:31 UTC

(opendal) branch main updated: feat(bindings/dotnet): add DeleteOptions with delete functions (#8098)

This is an automated email from the ASF dual-hosted git repository.

Xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new 04364ea323 feat(bindings/dotnet): add DeleteOptions with delete functions (#8098)
04364ea323 is described below

commit 04364ea32390796730099203a211357100bc517c
Author: Fatorin <sk...@gmail.com>
AuthorDate: Mon Aug 17 15:16:25 2026 +0800

    feat(bindings/dotnet): add DeleteOptions with delete functions (#8098)
    
    * feat(bindings/dotnet): add DeleteOptions
    
    * refactor(bindings/dotnet): implement RemoveAll on top of DeleteOptions
---
 .../OpenDAL.Tests/Behavior/DeleteBehaviorTest.cs   |  70 ++++++++-
 .../Behavior/RemoveAllBehaviorTest.cs              |   4 +-
 bindings/dotnet/OpenDAL/NativeMethods.cs           |  48 +++---
 bindings/dotnet/OpenDAL/Operator.cs                |  83 +++++-----
 bindings/dotnet/OpenDAL/Options/DeleteOptions.cs   |  56 +++++++
 bindings/dotnet/src/operator.rs                    | 168 +++++++++------------
 bindings/dotnet/src/options.rs                     |   9 ++
 7 files changed, 273 insertions(+), 165 deletions(-)

diff --git a/bindings/dotnet/OpenDAL.Tests/Behavior/DeleteBehaviorTest.cs b/bindings/dotnet/OpenDAL.Tests/Behavior/DeleteBehaviorTest.cs
index 7bab4c2861..43ff615c9d 100644
--- a/bindings/dotnet/OpenDAL.Tests/Behavior/DeleteBehaviorTest.cs
+++ b/bindings/dotnet/OpenDAL.Tests/Behavior/DeleteBehaviorTest.cs
@@ -17,6 +17,8 @@
  * under the License.
  */
 
+using OpenDAL.Options;
+
 namespace OpenDAL.Tests;
 
 [Collection("BehaviorOperator")]
@@ -57,7 +59,7 @@ public sealed class DeleteBehaviorTest : BehaviorTestBase
         var path = NewPath("delete-async");
 
         await Op.WriteAsync(path, RandomBytes(12), CT);
-        await Op.DeleteAsync(path, CT);
+        await Op.DeleteAsync(path, cancellationToken: CT);
 
         var ex = await Assert.ThrowsAsync<OpenDALException>(() => Op.ReadAsync(path, CT));
         Assert.True(IsMissingError(ex));
@@ -82,6 +84,70 @@ public sealed class DeleteBehaviorTest : BehaviorTestBase
             return;
         }
 
-        await Op.DeleteAsync(NewPath("delete-missing-async"), CT);
+        await Op.DeleteAsync(NewPath("delete-missing-async"), cancellationToken: CT);
+    }
+
+    [Fact]
+    public async Task DeleteBehavior_WithVersion_RemovesVersionPermanentlyAsync()
+    {
+        if (!Supports(c => c.DeleteWithVersion && c.StatWithVersion && c.Write))
+        {
+            return;
+        }
+
+        var path = NewPath("delete-version");
+        await Op.WriteAsync(path, RandomBytes(16), CT);
+
+        var version = (await Op.StatAsync(path, CT)).Version;
+        Assert.NotNull(version);
+
+        await Op.DeleteAsync(path, cancellationToken: CT);
+        var missing = await Assert.ThrowsAsync<OpenDALException>(() => Op.StatAsync(path, CT));
+        Assert.True(IsMissingError(missing));
+
+        var archived = await Op.StatAsync(path, new StatOptions { Version = version }, CT);
+        Assert.Equal(version, archived.Version);
+
+        await Op.DeleteAsync(path, new DeleteOptions { Version = version }, CT);
+
+        var gone = await Assert.ThrowsAsync<OpenDALException>(
+            () => Op.StatAsync(path, new StatOptions { Version = version }, CT));
+        Assert.Equal(ErrorCode.NotFound, gone.Code);
+    }
+
+    [Fact]
+    public void DeleteBehavior_WithRecursive_RemovesTree()
+    {
+        if (!Supports(c => c.DeleteWithRecursive && c.Write && c.List))
+        {
+            return;
+        }
+
+        var dir = NewPath("delete-recursive") + "/";
+        Op.Write($"{dir}a.txt", RandomBytes(16));
+        Op.Write($"{dir}nested/b.txt", RandomBytes(16));
+
+        Op.Delete(dir, new DeleteOptions { Recursive = true });
+
+        Assert.Empty(Op.List(dir, new ListOptions { Recursive = true }));
+    }
+
+    [Fact]
+    public void DeleteBehavior_WithVersionFromAnotherObject_IsAllowed()
+    {
+        if (!Supports(c => c.DeleteWithVersion && c.StatWithVersion && c.Write))
+        {
+            return;
+        }
+
+        var donor = NewPath("delete-version-donor");
+        Op.Write(donor, RandomBytes(8));
+        var foreignVersion = Op.Stat(donor).Version;
+        Assert.NotNull(foreignVersion);
+
+        var target = NewPath("delete-version-target");
+        Op.Write(target, RandomBytes(8));
+
+        Op.Delete(target, new DeleteOptions { Version = foreignVersion });
     }
 }
diff --git a/bindings/dotnet/OpenDAL.Tests/Behavior/RemoveAllBehaviorTest.cs b/bindings/dotnet/OpenDAL.Tests/Behavior/RemoveAllBehaviorTest.cs
index 86ce57ad2f..16995c459a 100644
--- a/bindings/dotnet/OpenDAL.Tests/Behavior/RemoveAllBehaviorTest.cs
+++ b/bindings/dotnet/OpenDAL.Tests/Behavior/RemoveAllBehaviorTest.cs
@@ -43,7 +43,7 @@ public sealed class RemoveAllBehaviorTest : BehaviorTestBase
         Op.Write($"{dir}nested/b.txt", RandomBytes(16));
         Op.RemoveAll(dir);
 
-        var entries = Op.List(dir);
+        var entries = Op.List(dir, new Options.ListOptions { Recursive = true });
         Assert.Empty(entries);
     }
 
@@ -61,7 +61,7 @@ public sealed class RemoveAllBehaviorTest : BehaviorTestBase
         await Op.WriteAsync($"{dir}nested/b.txt", RandomBytes(16), CT);
         await Op.RemoveAllAsync(dir, CT);
 
-        var entries = await Op.ListAsync(dir, new OpenDAL.Options.ListOptions(), CT);
+        var entries = await Op.ListAsync(dir, new Options.ListOptions { Recursive = true }, CT);
         Assert.Empty(entries);
     }
 }
diff --git a/bindings/dotnet/OpenDAL/NativeMethods.cs b/bindings/dotnet/OpenDAL/NativeMethods.cs
index b8dfd7e794..ede85949df 100644
--- a/bindings/dotnet/OpenDAL/NativeMethods.cs
+++ b/bindings/dotnet/OpenDAL/NativeMethods.cs
@@ -130,6 +130,22 @@ internal partial class NativeMethods
 
     #endregion
 
+    #region DeleteOption
+
+    [LibraryImport(__DllName, EntryPoint = "delete_option_build", StringMarshalling = StringMarshalling.Utf8)]
+    [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+    internal static partial OpenDALOptionsResult delete_option_build(
+        [In] string[] keys,
+        [In] string[] values,
+        nuint len
+    );
+
+    [LibraryImport(__DllName, EntryPoint = "delete_option_free")]
+    [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+    internal static partial void delete_option_free(IntPtr options);
+
+    #endregion
+
     #endregion
 
     #region Layer
@@ -340,18 +356,20 @@ internal partial class NativeMethods
 
     #region Delete
 
-    [LibraryImport(__DllName, EntryPoint = "operator_delete", StringMarshalling = StringMarshalling.Utf8)]
+    [LibraryImport(__DllName, EntryPoint = "operator_delete_with_options", StringMarshalling = StringMarshalling.Utf8)]
     [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
-    internal static partial OpenDALResult operator_delete(
+    internal static partial OpenDALResult operator_delete_with_options(
         Operator op,
-        string path
+        string path,
+        IntPtr options
     );
 
-    [LibraryImport(__DllName, EntryPoint = "operator_delete_async", StringMarshalling = StringMarshalling.Utf8)]
+    [LibraryImport(__DllName, EntryPoint = "operator_delete_with_options_async", StringMarshalling = StringMarshalling.Utf8)]
     [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
-    internal static unsafe partial OpenDALResult operator_delete_async(
+    internal static unsafe partial OpenDALResult operator_delete_with_options_async(
         Operator op,
         string path,
+        IntPtr options,
         delegate* unmanaged[Cdecl]<long, OpenDALResult, void> callback,
         long context
     );
@@ -422,26 +440,6 @@ internal partial class NativeMethods
 
     #endregion
 
-    #region RemoveAll
-
-    [LibraryImport(__DllName, EntryPoint = "operator_remove_all", StringMarshalling = StringMarshalling.Utf8)]
-    [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
-    internal static partial OpenDALResult operator_remove_all(
-        Operator op,
-        string path
-    );
-
-    [LibraryImport(__DllName, EntryPoint = "operator_remove_all_async", StringMarshalling = StringMarshalling.Utf8)]
-    [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
-    internal static unsafe partial OpenDALResult operator_remove_all_async(
-        Operator op,
-        string path,
-        delegate* unmanaged[Cdecl]<long, OpenDALResult, void> callback,
-        long context
-    );
-
-    #endregion
-
     #region Presign
 
     [LibraryImport(__DllName, EntryPoint = "operator_presign_read_async", StringMarshalling = StringMarshalling.Utf8)]
diff --git a/bindings/dotnet/OpenDAL/Operator.cs b/bindings/dotnet/OpenDAL/Operator.cs
index 2071673bb7..93dd0ce251 100644
--- a/bindings/dotnet/OpenDAL/Operator.cs
+++ b/bindings/dotnet/OpenDAL/Operator.cs
@@ -766,10 +766,12 @@ public partial class Operator : SafeHandle
     /// Deletes the file at the specified path.
     /// </summary>
     /// <param name="path">Target path in the configured backend.</param>
-    public void Delete(string path)
+    /// <param name="options">Additional delete options.</param>
+    public void Delete(string path, DeleteOptions? options = null)
     {
         ObjectDisposedException.ThrowIf(IsInvalid, this);
-        var result = NativeMethods.operator_delete(this, path);
+        using var nativeOptionsHandle = options?.BuildNativeOptionsHandle();
+        var result = NativeMethods.operator_delete_with_options(this, path, GetOptionsHandle(nativeOptionsHandle));
         ThrowIfErrorAndRelease(result);
     }
 
@@ -777,21 +779,23 @@ public partial class Operator : SafeHandle
     /// Deletes the file at the specified path asynchronously.
     /// </summary>
     /// <param name="path">Target path in the configured backend.</param>
+    /// <param name="options">Additional delete options.</param>
     /// <param name="cancellationToken">Cancellation token for the managed task.</param>
     /// <returns>A task that completes when the native callback reports completion.</returns>
-    public Task DeleteAsync(string path, CancellationToken cancellationToken = default)
+    public Task DeleteAsync(string path, DeleteOptions? options = null, CancellationToken cancellationToken = default)
     {
         ObjectDisposedException.ThrowIf(IsInvalid, this);
 
-        return SubmitAsyncOperation(SubmitDeleteAsync, cancellationToken);
+        return SubmitAsyncOperation(options, SubmitDeleteAsync, cancellationToken);
 
-        OpenDALResult SubmitDeleteAsync(long context)
+        OpenDALResult SubmitDeleteAsync(long context, IntPtr optionsHandle)
         {
             unsafe
             {
-                return NativeMethods.operator_delete_async(
+                return NativeMethods.operator_delete_with_options_async(
                     this,
                     path,
+                    optionsHandle,
                     &OnDeleteCompleted,
                     context
                 );
@@ -928,9 +932,8 @@ public partial class Operator : SafeHandle
     /// <param name="path">Target path in the configured backend.</param>
     public void RemoveAll(string path)
     {
-        ObjectDisposedException.ThrowIf(IsInvalid, this);
-        var result = NativeMethods.operator_remove_all(this, path);
-        ThrowIfErrorAndRelease(result);
+        var options = new DeleteOptions { Recursive = true };
+        Delete(path, options);
     }
 
     /// <summary>
@@ -941,22 +944,8 @@ public partial class Operator : SafeHandle
     /// <returns>A task that completes when the native callback reports completion.</returns>
     public Task RemoveAllAsync(string path, CancellationToken cancellationToken = default)
     {
-        ObjectDisposedException.ThrowIf(IsInvalid, this);
-
-        return SubmitAsyncOperation(SubmitRemoveAllAsync, cancellationToken);
-
-        OpenDALResult SubmitRemoveAllAsync(long context)
-        {
-            unsafe
-            {
-                return NativeMethods.operator_remove_all_async(
-                    this,
-                    path,
-                    &OnRemoveAllCompleted,
-                    context
-                );
-            }
-        }
+        var options = new DeleteOptions { Recursive = true };
+        return DeleteAsync(path, options, cancellationToken);
     }
 
     /// <summary>
@@ -1291,6 +1280,39 @@ public partial class Operator : SafeHandle
         }
     }
 
+    /// <summary>
+    /// Submits a native async operation and binds it to a managed task completion source.
+    /// </summary>
+    /// <typeparam name="TOptions">Managed options type.</typeparam>
+    /// <param name="options">Optional managed options for this operation.</param>
+    /// <param name="submit">Submission delegate that invokes the native async API.</param>
+    /// <param name="cancellationToken">Cancellation token for managed task observation.</param>
+    /// <returns>A task completed by the corresponding native callback.</returns>
+    /// <exception cref="OperationCanceledException"><paramref name="cancellationToken"/> is already canceled.</exception>
+    /// <exception cref="OpenDALException">Native submission returns an immediate error.</exception>
+    internal static Task SubmitAsyncOperation<TOptions>(
+        TOptions? options,
+        Func<long, IntPtr, OpenDALResult> submit,
+        CancellationToken cancellationToken)
+        where TOptions : class, IOptions
+    {
+        cancellationToken.ThrowIfCancellationRequested();
+        var context = AsyncStateRegistry.Register<bool>(out var asyncState);
+        try
+        {
+            using var nativeOptionsHandle = options?.BuildNativeOptionsHandle();
+            var submitResult = submit(context, GetOptionsHandle(nativeOptionsHandle));
+            ThrowIfErrorAndRelease(submitResult);
+            asyncState.BindCancellation(cancellationToken);
+            return asyncState.Completion.Task;
+        }
+        catch
+        {
+            AsyncStateRegistry.Unregister(context);
+            throw;
+        }
+    }
+
     /// <summary>
     /// Submits a native async operation without options and binds it to a managed task completion source.
     /// </summary>
@@ -1549,17 +1571,6 @@ public partial class Operator : SafeHandle
         CompleteAsyncCallback(context, result);
     }
 
-    /// <summary>
-    /// Native callback invoked when an asynchronous remove-all operation finishes.
-    /// </summary>
-    /// <param name="context">Opaque async state context previously registered by <see cref="AsyncStateRegistry"/>.</param>
-    /// <param name="result">Remove-all completion result returned by the native layer.</param>
-    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
-    private static void OnRemoveAllCompleted(long context, OpenDALResult result)
-    {
-        CompleteAsyncCallback(context, result);
-    }
-
     /// <summary>
     /// Native callback invoked when an asynchronous presign-read operation finishes.
     /// </summary>
diff --git a/bindings/dotnet/OpenDAL/Options/DeleteOptions.cs b/bindings/dotnet/OpenDAL/Options/DeleteOptions.cs
new file mode 100644
index 0000000000..dcb9ebfc47
--- /dev/null
+++ b/bindings/dotnet/OpenDAL/Options/DeleteOptions.cs
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+using OpenDAL.Options.Abstractions;
+
+namespace OpenDAL.Options;
+
+/// <summary>
+/// Options for delete operations.
+/// </summary>
+public sealed class DeleteOptions : IOptions
+{
+    /// <summary>
+    /// The version of the file to delete.
+    /// </summary>
+    public string? Version { get; init; }
+
+    /// <summary>
+    /// Whether to delete the target recursively.
+    /// </summary>
+    /// <remarks>
+    /// If `false`, behaves like the traditional single-object delete.
+    /// If `true`, all entries under the path (or sharing the prefix for file-like paths) will be removed.
+    /// </remarks>
+    public bool Recursive { get; init; }
+
+    public NativeOptionsHandle BuildNativeOptionsHandle()
+    {
+        var nativeOptions = new NativeOptionsBuilder()
+            .AddString("version", Version)
+            .AddBoolTrue("recursive", Recursive)
+            .Build();
+
+        return NativeOptionsBuilder.BuildNativeOptionsHandle(
+            nativeOptions,
+            NativeMethods.delete_option_build,
+            NativeMethods.delete_option_free
+        );
+    }
+}
\ No newline at end of file
diff --git a/bindings/dotnet/src/operator.rs b/bindings/dotnet/src/operator.rs
index 1b705bea10..fbda6e4287 100644
--- a/bindings/dotnet/src/operator.rs
+++ b/bindings/dotnet/src/operator.rs
@@ -22,7 +22,10 @@ use crate::{
     executor::executor_or_default,
     metadata::OpendalMetadata,
     operator_info::{OpendalOperatorInfo, into_operator_info},
-    options::{parse_list_options, parse_read_options, parse_stat_options, parse_write_options},
+    options::{
+        parse_delete_options, parse_list_options, parse_read_options, parse_stat_options,
+        parse_write_options,
+    },
     presign::into_presigned_request_ptr,
     result::{
         OpendalEntryListResult, OpendalMetadataResult, OpendalOperatorInfoResult,
@@ -267,6 +270,42 @@ pub unsafe extern "C" fn list_option_free(options: *mut opendal::options::ListOp
     }
 }
 
+/// Build delete options from raw C string key/value arrays.
+///
+/// On success, the returned pointer must be released by `delete_option_free`.
+/// # Safety
+///
+/// - When `len > 0`, `keys` and `values` must be non-null pointers to arrays
+///   containing at least `len` C-string pointers.
+/// - Each key/value entry must be a valid null-terminated UTF-8 string.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn delete_option_build(
+    keys: *const *const c_char,
+    values: *const *const c_char,
+    len: usize,
+) -> OpendalOptionsResult {
+    match unsafe { collect_options(keys, values, len) }
+        .and_then(|values| parse_delete_options(&values))
+    {
+        Ok(options) => OpendalOptionsResult::ok(Box::into_raw(Box::new(options)) as *mut c_void),
+        Err(error) => OpendalOptionsResult::from_error(error),
+    }
+}
+
+/// # Safety
+///
+/// - `options` must be null or a pointer returned by `delete_option_build`.
+/// - This function must be called at most once for the same pointer.
+#[unsafe(no_mangle)]
+pub unsafe extern "C" fn delete_option_free(options: *mut opendal::options::DeleteOptions) {
+    if options.is_null() {
+        return;
+    }
+    unsafe {
+        drop(Box::from_raw(options));
+    }
+}
+
 /// Construct an OpenDAL operator instance from a scheme and key/value options.
 ///
 /// Returns a pointer that must be released with `operator_free`.
@@ -623,59 +662,71 @@ fn operator_duplicate_inner(op_handle: *const OperatorHandle) -> Result<*mut c_v
     Ok(Box::into_raw(Box::new(handle.with_operator(handle.operator()))) as *mut c_void)
 }
 
-/// Delete `path` synchronously.
+/// Delete `path` synchronously with options.
 /// # Safety
 ///
 /// - `op_handle` must be a valid operator pointer from `operator_construct`.
 /// - `path` must be a valid null-terminated UTF-8 string.
+/// - `options` must be null or a pointer returned by `delete_option_build`.
 #[unsafe(no_mangle)]
-pub extern "C" fn operator_delete(
+pub extern "C" fn operator_delete_with_options(
     op_handle: *const OperatorHandle,
     path: *const c_char,
+    options: *const opendal::options::DeleteOptions,
 ) -> OpendalResult {
-    match operator_delete_inner(op_handle, path) {
+    match operator_delete_with_options_inner(op_handle, path, options) {
         Ok(()) => OpendalResult::ok(),
         Err(error) => OpendalResult::from_error(error),
     }
 }
 
-fn operator_delete_inner(
+fn operator_delete_with_options_inner(
     op_handle: *const OperatorHandle,
     path: *const c_char,
+    options: *const opendal::options::DeleteOptions,
 ) -> Result<(), OpenDALError> {
     let handle = require_op_handle(op_handle)?;
     let executor = handle.executor.clone();
     let path = require_cstr(path, "path")?;
 
+    let options = if options.is_null() {
+        opendal::options::DeleteOptions::default()
+    } else {
+        unsafe { (&*options).clone() }
+    };
+
     executor
-        .block_on(handle.delete(path))
+        .block_on(handle.delete_options(path, options))
         .map_err(OpenDALError::from_opendal_error)
 }
 
-/// Delete `path` asynchronously.
+/// Delete `path` asynchronously with options.
 ///
 /// The callback is invoked exactly once with the final result.
 /// # Safety
 ///
 /// - `op_handle` must be a valid operator pointer from `operator_construct`.
 /// - `path` must be a valid null-terminated UTF-8 string.
+/// - `options` must be null or a pointer returned by `delete_option_build`.
 /// - `callback` must be a valid function pointer and remain callable until invoked.
 #[unsafe(no_mangle)]
-pub extern "C" fn operator_delete_async(
+pub extern "C" fn operator_delete_with_options_async(
     op_handle: *const OperatorHandle,
     path: *const c_char,
+    options: *const opendal::options::DeleteOptions,
     callback: Option<WriteCallback>,
     context: i64,
 ) -> OpendalResult {
-    match operator_delete_async_inner(op_handle, path, callback, context) {
+    match operator_delete_with_options_async_inner(op_handle, path, options, callback, context) {
         Ok(()) => OpendalResult::ok(),
         Err(error) => OpendalResult::from_error(error),
     }
 }
 
-fn operator_delete_async_inner(
+fn operator_delete_with_options_async_inner(
     op_handle: *const OperatorHandle,
     path: *const c_char,
+    options: *const opendal::options::DeleteOptions,
     callback: Option<WriteCallback>,
     context: i64,
 ) -> Result<(), OpenDALError> {
@@ -684,10 +735,16 @@ fn operator_delete_async_inner(
     let path = require_cstr(path, "path")?.to_string();
     let callback = require_callback(callback)?;
 
+    let options = if options.is_null() {
+        opendal::options::DeleteOptions::default()
+    } else {
+        unsafe { (&*options).clone() }
+    };
+
     let op = handle.operator();
     executor.spawn(async move {
         let result = op
-            .delete(&path)
+            .delete_options(&path, options)
             .await
             .map_err(OpenDALError::from_opendal_error);
 
@@ -957,95 +1014,6 @@ fn operator_rename_async_inner(
     Ok(())
 }
 
-/// Remove `path` recursively synchronously.
-/// # Safety
-///
-/// - `op_handle` must be a valid operator pointer from `operator_construct`.
-/// - `path` must be a valid null-terminated UTF-8 string.
-#[unsafe(no_mangle)]
-pub extern "C" fn operator_remove_all(
-    op_handle: *const OperatorHandle,
-    path: *const c_char,
-) -> OpendalResult {
-    match operator_remove_all_inner(op_handle, path) {
-        Ok(()) => OpendalResult::ok(),
-        Err(error) => OpendalResult::from_error(error),
-    }
-}
-
-fn operator_remove_all_inner(
-    op_handle: *const OperatorHandle,
-    path: *const c_char,
-) -> Result<(), OpenDALError> {
-    let handle = require_op_handle(op_handle)?;
-    let executor = handle.executor.clone();
-    let path = require_cstr(path, "path")?;
-
-    let options = opendal::options::DeleteOptions {
-        recursive: true,
-        ..Default::default()
-    };
-
-    executor
-        .block_on(handle.delete_options(path, options))
-        .map_err(OpenDALError::from_opendal_error)
-}
-
-/// Remove `path` recursively asynchronously.
-///
-/// The callback is invoked exactly once with the final result.
-/// # Safety
-///
-/// - `op_handle` must be a valid operator pointer from `operator_construct`.
-/// - `path` must be a valid null-terminated UTF-8 string.
-/// - `callback` must be a valid function pointer and remain callable until invoked.
-#[unsafe(no_mangle)]
-pub extern "C" fn operator_remove_all_async(
-    op_handle: *const OperatorHandle,
-    path: *const c_char,
-    callback: Option<WriteCallback>,
-    context: i64,
-) -> OpendalResult {
-    match operator_remove_all_async_inner(op_handle, path, callback, context) {
-        Ok(()) => OpendalResult::ok(),
-        Err(error) => OpendalResult::from_error(error),
-    }
-}
-
-fn operator_remove_all_async_inner(
-    op_handle: *const OperatorHandle,
-    path: *const c_char,
-    callback: Option<WriteCallback>,
-    context: i64,
-) -> Result<(), OpenDALError> {
-    let handle = require_op_handle(op_handle)?;
-    let executor = handle.executor.clone();
-    let path = require_cstr(path, "path")?.to_string();
-    let callback = require_callback(callback)?;
-    let options = opendal::options::DeleteOptions {
-        recursive: true,
-        ..Default::default()
-    };
-
-    let op = handle.operator();
-    executor.spawn(async move {
-        let result = op
-            .delete_options(&path, options)
-            .await
-            .map_err(OpenDALError::from_opendal_error);
-
-        callback(
-            context,
-            match result {
-                Ok(()) => OpendalResult::ok(),
-                Err(error) => OpendalResult::from_error(error),
-            },
-        );
-    });
-
-    Ok(())
-}
-
 /// Presign read asynchronously.
 ///
 /// The callback is invoked exactly once with the final result.
diff --git a/bindings/dotnet/src/options.rs b/bindings/dotnet/src/options.rs
index 3648f8a93c..0a84e0eadf 100644
--- a/bindings/dotnet/src/options.rs
+++ b/bindings/dotnet/src/options.rs
@@ -210,3 +210,12 @@ pub fn parse_list_options(
 
     Ok(options)
 }
+
+pub fn parse_delete_options(
+    values: &HashMap<String, String>,
+) -> Result<opendal::options::DeleteOptions, OpenDALError> {
+    Ok(opendal::options::DeleteOptions {
+        version: parse_string(values, "version"),
+        recursive: parse_bool(values, "recursive")?.unwrap_or(false),
+    })
+}